@noatgnu/cupcake-vanilla 1.4.4 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -3,7 +3,7 @@ import { Injectable, inject, signal, computed, effect, untracked, NgZone, EventE
|
|
|
3
3
|
import * as i3 from '@noatgnu/cupcake-core';
|
|
4
4
|
import { BaseApiService, WebSocketService, NotificationService as NotificationService$1, SiteConfigService, LabGroupService, ToastService, AsyncTaskMonitorService, TaskStatus, TaskType, ResourceVisibility, AuthService, ConfirmDialogService } from '@noatgnu/cupcake-core';
|
|
5
5
|
import { map, switchMap, tap as tap$1, takeUntil } from 'rxjs/operators';
|
|
6
|
-
import { of, tap, map as map$1, catchError, expand, EMPTY, reduce, throwError, from, Observable, Subject, NEVER, debounceTime, distinctUntilChanged, filter, switchMap as switchMap$1, firstValueFrom, takeUntil as takeUntil$1 } from 'rxjs';
|
|
6
|
+
import { of, tap, map as map$1, catchError, expand, EMPTY, reduce, throwError, from, Observable, Subject, NEVER, debounceTime, distinctUntilChanged, filter, switchMap as switchMap$1, merge, firstValueFrom, takeUntil as takeUntil$1 } from 'rxjs';
|
|
7
7
|
import jsSHA from 'jssha';
|
|
8
8
|
import * as i1$1 from '@angular/common';
|
|
9
9
|
import { CommonModule } from '@angular/common';
|
|
@@ -38,6 +38,8 @@ var OntologyType;
|
|
|
38
38
|
OntologyType["PSI_MS"] = "psi_ms";
|
|
39
39
|
OntologyType["BTO"] = "bto";
|
|
40
40
|
OntologyType["DOID"] = "doid";
|
|
41
|
+
OntologyType["PROTOCOLS_IO_REAGENT"] = "protocols_io_reagent";
|
|
42
|
+
OntologyType["PROTOCOLS_IO_UNIT"] = "protocols_io_unit";
|
|
41
43
|
})(OntologyType || (OntologyType = {}));
|
|
42
44
|
const OntologyTypeLabels = {
|
|
43
45
|
[OntologyType.NONE]: 'None',
|
|
@@ -54,7 +56,9 @@ const OntologyTypeLabels = {
|
|
|
54
56
|
[OntologyType.MS_UNIQUE_VOCABULARIES]: 'MS Unique Vocabularies',
|
|
55
57
|
[OntologyType.PSI_MS]: 'PSI-MS Controlled Vocabulary',
|
|
56
58
|
[OntologyType.BTO]: 'BTO Tissue',
|
|
57
|
-
[OntologyType.DOID]: 'Disease Ontology (DOID)'
|
|
59
|
+
[OntologyType.DOID]: 'Disease Ontology (DOID)',
|
|
60
|
+
[OntologyType.PROTOCOLS_IO_REAGENT]: 'protocols.io Reagent',
|
|
61
|
+
[OntologyType.PROTOCOLS_IO_UNIT]: 'protocols.io Unit'
|
|
58
62
|
};
|
|
59
63
|
var TemplatePermissionLevel;
|
|
60
64
|
(function (TemplatePermissionLevel) {
|
|
@@ -163,6 +167,8 @@ const ONTOLOGY_TYPE_CONFIGS = [
|
|
|
163
167
|
{ value: OntologyType.PSI_MS, label: 'PSI-MS', customFilters: undefined },
|
|
164
168
|
{ value: OntologyType.BTO, label: 'BTO Tissue (BRENDA)', customFilters: undefined },
|
|
165
169
|
{ value: OntologyType.DOID, label: 'Disease Ontology (DOID)', customFilters: undefined },
|
|
170
|
+
{ value: OntologyType.PROTOCOLS_IO_REAGENT, label: 'Reagent (protocols.io)', customFilters: undefined },
|
|
171
|
+
{ value: OntologyType.PROTOCOLS_IO_UNIT, label: 'Unit (protocols.io)', customFilters: undefined },
|
|
166
172
|
// MS Unique Vocabularies with specific term_type filters
|
|
167
173
|
{
|
|
168
174
|
value: OntologyType.MS_UNIQUE_VOCABULARIES,
|
|
@@ -1613,6 +1619,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
|
|
|
1613
1619
|
}], ctorParameters: () => [] });
|
|
1614
1620
|
|
|
1615
1621
|
class OntologySearchService extends BaseApiService {
|
|
1622
|
+
websocket = inject(WebSocketService);
|
|
1616
1623
|
/**
|
|
1617
1624
|
* Get ontology suggestions across all sources for SDRF validation
|
|
1618
1625
|
*
|
|
@@ -1673,6 +1680,13 @@ class OntologySearchService extends BaseApiService {
|
|
|
1673
1680
|
limit
|
|
1674
1681
|
});
|
|
1675
1682
|
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Notifications that results from an external platform, such as protocols.io, were added to
|
|
1685
|
+
* the local vocabulary after a search answered with remotePending.
|
|
1686
|
+
*/
|
|
1687
|
+
vocabularyUpdates() {
|
|
1688
|
+
return this.websocket.filterMessages('external_vocabulary.updated');
|
|
1689
|
+
}
|
|
1676
1690
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: OntologySearchService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
1677
1691
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: OntologySearchService, providedIn: 'root' });
|
|
1678
1692
|
}
|
|
@@ -4195,6 +4209,7 @@ class MetadataValueEditModal {
|
|
|
4195
4209
|
...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
|
|
4196
4210
|
isLoadingSuggestions = signal(false, /* @ts-ignore */
|
|
4197
4211
|
...(ngDevMode ? [{ debugName: "isLoadingSuggestions" }] : /* istanbul ignore next */ []));
|
|
4212
|
+
lastSearchTerm = '';
|
|
4198
4213
|
searchType = signal('icontains', /* @ts-ignore */
|
|
4199
4214
|
...(ngDevMode ? [{ debugName: "searchType" }] : /* istanbul ignore next */ []));
|
|
4200
4215
|
isLoadingFavorites = signal(false, /* @ts-ignore */
|
|
@@ -4335,7 +4350,13 @@ class MetadataValueEditModal {
|
|
|
4335
4350
|
}
|
|
4336
4351
|
// Ontology autocomplete methods
|
|
4337
4352
|
searchOntology = (text$) => {
|
|
4338
|
-
|
|
4353
|
+
const typed$ = text$.pipe(debounceTime(200), distinctUntilChanged());
|
|
4354
|
+
const refreshed$ = this.ontologySearchService.vocabularyUpdates().pipe(filter(update => update.ontology_type === this.config?.ontologyType &&
|
|
4355
|
+
update.search_term.toLowerCase() === this.lastSearchTerm.trim().toLowerCase()), map$1(() => this.lastSearchTerm));
|
|
4356
|
+
return merge(typed$, refreshed$).pipe(tap(term => {
|
|
4357
|
+
this.lastSearchTerm = term;
|
|
4358
|
+
this.isLoadingSuggestions.set(true);
|
|
4359
|
+
}), switchMap$1(term => {
|
|
4339
4360
|
if (term.length < 2 || !this.hasOntologyType) {
|
|
4340
4361
|
this.isLoadingSuggestions.set(false);
|
|
4341
4362
|
return of([]);
|
|
@@ -5067,6 +5088,8 @@ class AsyncTaskUIService {
|
|
|
5067
5088
|
[TaskType.TRANSCRIBE_AUDIO]: 'Audio transcription',
|
|
5068
5089
|
[TaskType.TRANSCRIBE_VIDEO]: 'Video transcription',
|
|
5069
5090
|
[TaskType.VALIDATE_SDRF_FILE]: 'SDRF file validation',
|
|
5091
|
+
[TaskType.EXPORT_PROTOCOLS_IO]: 'protocols.io export',
|
|
5092
|
+
[TaskType.EXPORT_RUN_PROTOCOLS_IO]: 'protocols.io session export',
|
|
5070
5093
|
};
|
|
5071
5094
|
return typeMap[taskType] || taskType;
|
|
5072
5095
|
}
|
|
@@ -9992,12 +10015,18 @@ class MetadataTableDetails {
|
|
|
9992
10015
|
if (tableId) {
|
|
9993
10016
|
this.router.navigate(['/metadata-tables', tableId, 'list']);
|
|
9994
10017
|
}
|
|
10018
|
+
else {
|
|
10019
|
+
this.viewMode.set('list');
|
|
10020
|
+
}
|
|
9995
10021
|
}
|
|
9996
10022
|
switchToTableMode() {
|
|
9997
10023
|
const tableId = this.tableId();
|
|
9998
10024
|
if (tableId) {
|
|
9999
10025
|
this.router.navigate(['/metadata-tables', tableId, 'table']);
|
|
10000
10026
|
}
|
|
10027
|
+
else {
|
|
10028
|
+
this.viewMode.set('table');
|
|
10029
|
+
}
|
|
10001
10030
|
}
|
|
10002
10031
|
onPageChange(page) {
|
|
10003
10032
|
this.currentPage.set(page);
|
|
@@ -10976,11 +11005,11 @@ class MetadataTableDetails {
|
|
|
10976
11005
|
});
|
|
10977
11006
|
}
|
|
10978
11007
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: MetadataTableDetails, deps: [{ token: i1$2.ActivatedRoute }, { token: i1$2.Router }, { token: MetadataTableService }, { token: AsyncExportService }, { token: ChunkedUploadService }, { token: SamplePoolService }, { token: MetadataColumnService }, { token: i3.ToastService }, { token: AsyncTaskUIService }, { token: i2.NgbModal }, { token: OfficialColumnCacheService }, { token: i3.ConfirmDialogService }], target: i0.ɵɵFactoryTarget.Component });
|
|
10979
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: MetadataTableDetails, isStandalone: true, selector: "ccv-metadata-table-details", inputs: { readonlyMode: "readonlyMode", preloadedTable: "preloadedTable" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }, { propertyName: "excelInput", first: true, predicate: ["excelInput"], descendants: true }], ngImport: i0, template: "<div class=\"metadata-table-details-container\">\r\n <!-- Header Section with Breadcrumbs -->\r\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\r\n <div class=\"container-fluid\">\r\n <div class=\"d-flex align-items-center justify-content-between w-100 flex-wrap gap-2\">\r\n <div class=\"d-flex align-items-center flex-wrap gap-2\">\r\n <!-- Breadcrumb Navigation -->\r\n <nav aria-label=\"breadcrumb\" class=\"me-3\">\r\n <ol class=\"breadcrumb mb-0\">\r\n @if (!readonlyMode) {\r\n <li class=\"breadcrumb-item\">\r\n <a routerLink=\"/metadata-tables\" class=\"text-decoration-none\">\r\n <i class=\"bi bi-table me-1\"></i>Metadata Tables\r\n </a>\r\n </li>\r\n }\r\n <li class=\"breadcrumb-item active\" aria-current=\"page\">\r\n @if (table()) {\r\n {{ table()!.name }}\r\n } @else {\r\n Table Details\r\n }\r\n </li>\r\n </ol>\r\n </nav>\r\n @if (readonlyMode) {\r\n <span class=\"badge bg-secondary ms-2\">\r\n <i class=\"bi bi-eye me-1\"></i>Read-only\r\n </span>\r\n }\r\n\r\n <!-- Table Status Badge -->\r\n @if (table()) {\r\n <span class=\"badge ms-2\" [class]=\"getTableStatusClass()\">\r\n {{ getTableStatusBadge() }}\r\n </span>\r\n }\r\n </div>\r\n\r\n <!-- Action Buttons -->\r\n <div class=\"d-flex gap-2 flex-wrap\">\r\n @if (!readonlyMode) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-secondary btn-sm\"\r\n (click)=\"navigateBack()\"\r\n title=\"Back to Tables\">\r\n <i class=\"bi bi-arrow-left me-1\"></i>Back\r\n </button>\r\n }\r\n\r\n <!-- View Mode Toggle -->\r\n @if (hasColumns()) {\r\n <div class=\"btn-group\" role=\"group\">\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm\"\r\n [class]=\"viewMode() === 'list' ? 'btn-primary' : 'btn-outline-primary'\"\r\n (click)=\"switchToListMode()\"\r\n title=\"Column List View\">\r\n <i class=\"bi bi-list-ul me-1\"></i>List\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm\"\r\n [class]=\"viewMode() === 'table' ? 'btn-primary' : 'btn-outline-primary'\"\r\n (click)=\"switchToTableMode()\"\r\n title=\"Table Data View\">\r\n <i class=\"bi bi-table me-1\"></i>Table\r\n </button>\r\n </div>\r\n }\r\n <!-- Import Dropdown -->\r\n @if (table() && effectiveCanEdit()) {\r\n <div class=\"btn-group\" ngbDropdown>\r\n <!-- Hidden file inputs -->\r\n <input\r\n #fileInput\r\n type=\"file\"\r\n accept=\".txt,.tsv\"\r\n style=\"display: none;\"\r\n (change)=\"importSdrf($event)\">\r\n <input\r\n #excelInput\r\n type=\"file\"\r\n accept=\".xlsx,.xls\"\r\n style=\"display: none;\"\r\n (change)=\"importExcel($event)\">\r\n\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-success btn-sm dropdown-toggle\"\r\n ngbDropdownToggle\r\n [disabled]=\"isLoading()\"\r\n title=\"Import Data\">\r\n <i class=\"bi bi-upload me-1\"></i>Import\r\n </button>\r\n <div class=\"dropdown-menu\" ngbDropdownMenu>\r\n <div class=\"dropdown-item\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"form-check form-switch\">\r\n <input\r\n class=\"form-check-input\"\r\n type=\"checkbox\"\r\n id=\"overrideSampleCountToggle\"\r\n [checked]=\"overrideSampleCount()\"\r\n (change)=\"overrideSampleCount.set(!overrideSampleCount())\">\r\n <label class=\"form-check-label small\" for=\"overrideSampleCountToggle\" title=\"Use the sample count from the imported file instead of the current table count\">Override count</label>\r\n </div>\r\n </div>\r\n <div class=\"dropdown-item\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"form-check form-switch\">\r\n <input\r\n class=\"form-check-input\"\r\n type=\"checkbox\"\r\n id=\"applySchemaTemplatesToggle\"\r\n [checked]=\"applySchemaTemplates()\"\r\n (change)=\"applySchemaTemplates.set(!applySchemaTemplates())\">\r\n <label class=\"form-check-label small\" for=\"applySchemaTemplatesToggle\" title=\"Pre-populate empty table columns from the official schema declared in the SDRF file\">Apply schema</label>\r\n </div>\r\n </div>\r\n <div class=\"dropdown-divider\"></div>\r\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerFileInput(); $event.preventDefault()\">\r\n <i class=\"bi bi-file-earmark-text me-2\"></i>Import SDRF File\r\n </a>\r\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerExcelInput(); $event.preventDefault()\">\r\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Import Excel Template\r\n </a>\r\n <div class=\"dropdown-divider\"></div>\r\n <a class=\"dropdown-item\" href=\"#\" (click)=\"openColumnOverrideImportModal(); $event.preventDefault()\">\r\n <i class=\"bi bi-file-earmark-arrow-up me-2\"></i>Import / Override Columns\r\n </a>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Export Dropdown -->\r\n @if (table() && hasColumns() && !readonlyMode) {\r\n <div class=\"btn-group\" ngbDropdown>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-info btn-sm dropdown-toggle\"\r\n ngbDropdownToggle\r\n [disabled]=\"isLoading()\"\r\n title=\"Export Table\">\r\n <i class=\"bi bi-download me-1\"></i>Export\r\n </button>\r\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\r\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('sdrf'); $event.preventDefault()\">\r\n <i class=\"bi bi-file-earmark-text me-2\"></i>Export as SDRF\r\n </a>\r\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('excel'); $event.preventDefault()\">\r\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Export as Excel\r\n </a>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Share Link Dropdown -->\r\n @if (table() && effectiveCanEdit()) {\r\n <div class=\"btn-group\" ngbDropdown>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm dropdown-toggle\"\r\n [class]=\"shareUrl() ? 'btn-success' : 'btn-outline-secondary'\"\r\n ngbDropdownToggle\r\n title=\"Share Table\">\r\n <i class=\"bi bi-share me-1\"></i>Share\r\n </button>\r\n <div class=\"dropdown-menu dropdown-menu-end p-2\" ngbDropdownMenu style=\"min-width: 320px;\">\r\n @if (shareUrl()) {\r\n <div class=\"mb-2\">\r\n <label class=\"form-label small fw-semibold mb-1\">Share link</label>\r\n <div class=\"input-group input-group-sm\">\r\n <input type=\"text\" class=\"form-control form-control-sm font-monospace\"\r\n [value]=\"shareUrl()\" readonly>\r\n <button class=\"btn btn-outline-secondary btn-sm\" type=\"button\"\r\n (click)=\"copyShareLink()\"\r\n title=\"Copy link\">\r\n <i class=\"bi bi-clipboard\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n <div class=\"d-flex gap-2\">\r\n <button class=\"btn btn-outline-primary btn-sm flex-fill\"\r\n (click)=\"generateShareLink()\"\r\n title=\"Regenerate link (invalidates current link)\">\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Regenerate\r\n </button>\r\n <button class=\"btn btn-outline-danger btn-sm flex-fill\"\r\n (click)=\"revokeShareLink()\"\r\n title=\"Revoke link\">\r\n <i class=\"bi bi-x-circle me-1\"></i>Revoke\r\n </button>\r\n </div>\r\n } @else {\r\n <p class=\"text-muted small mb-2\">No share link. Generate one to let anyone view this table without logging in.</p>\r\n <button class=\"btn btn-primary btn-sm w-100\"\r\n (click)=\"generateShareLink()\">\r\n <i class=\"bi bi-link-45deg me-1\"></i>Generate Share Link\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Edit Button -->\r\n @if (table() && effectiveCanEdit()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-primary btn-sm\"\r\n (click)=\"editTable()\"\r\n [disabled]=\"isLoading()\"\r\n title=\"Edit Table\">\r\n <i class=\"bi bi-pencil me-1\"></i>Edit\r\n </button>\r\n }\r\n\r\n <!-- Reorder Columns Button -->\r\n @if (table() && effectiveCanEdit() && hasColumns()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-warning btn-sm\"\r\n (click)=\"reorderColumns()\"\r\n [disabled]=\"isLoading()\"\r\n title=\"Reorder Columns by Schema\">\r\n <i class=\"bi bi-arrows-move me-1\"></i>Reorder\r\n </button>\r\n }\r\n\r\n <!-- Create Pool Button -->\r\n @if (table() && effectiveCanEdit() && hasColumns()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-success btn-sm\"\r\n (click)=\"createPool()\"\r\n [disabled]=\"isLoading()\"\r\n title=\"Create Sample Pool\">\r\n <i class=\"bi bi-plus-circle me-1\"></i>Create Pool\r\n </button>\r\n }\r\n\r\n <!-- More Actions Dropdown -->\r\n @if (table() && effectiveCanEdit()) {\r\n <div class=\"btn-group\" ngbDropdown>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-danger btn-sm dropdown-toggle\"\r\n ngbDropdownToggle\r\n [disabled]=\"isLoading()\"\r\n aria-label=\"More actions\">\r\n <i class=\"bi bi-three-dots-vertical\" aria-hidden=\"true\"></i>\r\n </button>\r\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\r\n <a class=\"dropdown-item text-danger\" href=\"#\" (click)=\"deleteTable(); $event.preventDefault()\">\r\n <i class=\"bi bi-trash me-2\"></i>Delete Table\r\n </a>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </nav>\r\n\r\n <!-- Loading Spinner -->\r\n @if (isLoading()) {\r\n <div class=\"d-flex justify-content-center align-items-center\" style=\"height: 400px;\">\r\n <div class=\"spinner-border text-primary\" role=\"status\">\r\n <span class=\"visually-hidden\">Loading...</span>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Table Details Content -->\r\n @if (table() && !isLoading()) {\r\n <div class=\"table-details-content p-4\">\r\n\r\n <!-- Table Information Card -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <div class=\"d-flex align-items-center justify-content-between\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-info-circle me-2\"></i>Table Information\r\n </h5>\r\n <small class=\"text-muted\">\r\n Created {{ table()!.createdAt | date:'medium' }}\r\n </small>\r\n </div>\r\n </div>\r\n <div class=\"card-body\">\r\n <div class=\"row\">\r\n <div class=\"col-md-8\">\r\n <h3 class=\"fw-bold mb-3\">{{ table()!.name }}</h3>\r\n @if (table()!.description) {\r\n <p class=\"text-muted mb-3\">{{ table()!.description }}</p>\r\n }\r\n <div class=\"row\">\r\n <div class=\"col-md-6\">\r\n <div class=\"d-flex align-items-center mb-2\">\r\n <i class=\"bi bi-hash text-muted me-2\"></i>\r\n <span class=\"fw-semibold me-2\">Sample Count:</span>\r\n <span class=\"badge bg-primary\">{{ table()!.sampleCount }}</span>\r\n </div>\r\n <div class=\"d-flex align-items-center mb-2\">\r\n <i class=\"bi bi-columns-gap text-muted me-2\"></i>\r\n <span class=\"fw-semibold me-2\">Columns:</span>\r\n <span class=\"badge bg-info\">{{ table()!.columns?.length || 0 }}</span>\r\n </div>\r\n </div>\r\n <div class=\"col-md-6\">\r\n @if (table()!.ownerUsername) {\r\n <div class=\"d-flex align-items-center mb-2\">\r\n <i class=\"bi bi-person text-muted me-2\"></i>\r\n <span class=\"fw-semibold me-2\">Owner:</span>\r\n <span>{{ table()!.ownerUsername }}</span>\r\n </div>\r\n }\r\n @if (table()!.labGroupName) {\r\n <div class=\"d-flex align-items-center mb-2\">\r\n <i class=\"bi bi-people text-muted me-2\"></i>\r\n <span class=\"fw-semibold me-2\">Lab Group:</span>\r\n <span>{{ table()!.labGroupName }}</span>\r\n </div>\r\n }\r\n <div class=\"d-flex align-items-center mb-2\">\r\n <i class=\"bi bi-tag text-muted me-2\"></i>\r\n <span class=\"fw-semibold me-2\">Version:</span>\r\n <span class=\"badge bg-light text-dark\">{{ table()!.version || 'v1.0' }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"col-md-4 text-end\">\r\n <div class=\"d-flex flex-column gap-2\">\r\n @if (table()!.isLocked) {\r\n <span class=\"badge bg-warning\">\r\n <i class=\"bi bi-lock me-1\"></i>Locked\r\n </span>\r\n }\r\n @if (table()!.isPublished) {\r\n <span class=\"badge bg-success\">\r\n <i class=\"bi bi-cloud-check me-1\"></i>Published\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Sample Pool Table -->\r\n @if (viewMode() === 'table' && hasPools()) {\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-layers me-2\"></i>Sample Pool Table\r\n <span class=\"badge bg-warning ms-2\">{{ sortedPools().length }} pools</span>\r\n </h5>\r\n </div>\r\n <div class=\"card-body p-0\">\r\n <div class=\"table-responsive\" appDualScroll>\r\n <table class=\"table table-hover mb-0\">\r\n <thead class=\"table-light\">\r\n <tr>\r\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">Pool</th>\r\n @for (column of sortedColumns(); track column.id) {\r\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\r\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\r\n <span>{{ getShortColumnName(column) }}</span>\r\n @if (effectiveCanEdit()) {\r\n <div class=\"header-actions\">\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\r\n (click)=\"editColumnValue(column)\"\r\n [title]=\"'Edit default value for ' + column.name\">\r\n <i class=\"bi bi-pencil\"></i>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n </th>\r\n }\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (poolRow of poolTableRows(); track poolRow._poolId) {\r\n <tr class=\"pool-row\" [class.editable]=\"effectiveCanEdit()\">\r\n <td class=\"text-muted fw-semibold\" \r\n [class.clickable]=\"effectiveCanEdit()\"\r\n (click)=\"effectiveCanEdit() ? editPool(poolRow._poolData) : null\"\r\n [title]=\"effectiveCanEdit() ? 'Click to edit pool: ' + poolRow._poolName : poolRow._poolName\">\r\n <i class=\"bi bi-layers me-2\"></i>{{ poolRow._poolName }}\r\n @if (effectiveCanEdit()) {\r\n <i class=\"bi bi-pencil-square ms-1 text-primary\" style=\"font-size: 0.8em;\"></i>\r\n }\r\n </td>\r\n @for (column of sortedColumns(); track column.id) {\r\n <td\r\n class=\"pool-cell\"\r\n [class.opacity-50]=\"column.hidden\"\r\n [attr.data-pool-id]=\"poolRow._poolId\"\r\n [attr.data-column-id]=\"column.id\"\r\n (click)=\"effectiveCanEdit() ? editPoolColumnValue(poolRow._poolData, column) : null\"\r\n [title]=\"effectiveCanEdit() ? 'Click to edit ' + column.name + ' for pool ' + poolRow._poolName : ''\">\r\n {{ poolRow['col_' + column.id] || '-' }}\r\n </td>\r\n }\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Columns/Table Section -->\r\n <div class=\"card\">\r\n <div class=\"card-header\">\r\n <div class=\"d-flex align-items-center justify-content-between flex-wrap gap-2\">\r\n <h5 class=\"mb-0\">\r\n @if (viewMode() === 'list') {\r\n <i class=\"bi bi-list-columns me-2\"></i>Table Columns\r\n } @else {\r\n <i class=\"bi bi-table me-2\"></i>Table Data\r\n }\r\n </h5>\r\n\r\n <!-- Column Filter -->\r\n @if (hasColumns()) {\r\n <div class=\"d-flex align-items-center flex-wrap gap-2\">\r\n <div class=\"input-group input-group-sm\" style=\"width: 250px; max-width: 100%;\">\r\n <span class=\"input-group-text\">\r\n <i class=\"bi bi-funnel text-muted\"></i>\r\n </span>\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n placeholder=\"Filter columns...\"\r\n [value]=\"columnFilter()\"\r\n (input)=\"onColumnFilterChange($any($event.target).value)\">\r\n @if (columnFilter()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-secondary\"\r\n (click)=\"clearColumnFilter()\"\r\n title=\"Clear filter\"\r\n aria-label=\"Clear column filter\">\r\n <i class=\"bi bi-x\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </div>\r\n @if (columnFilter()) {\r\n <small class=\"text-muted ms-2\">\r\n {{ filteredColumnCount }}/{{ totalColumnCount }}\r\n </small>\r\n }\r\n </div>\r\n }\r\n \r\n <div class=\"d-flex gap-2 align-items-center\">\r\n @if (viewMode() === 'list') {\r\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\r\n @if (table() && effectiveCanEdit()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-success btn-sm\"\r\n (click)=\"openAddColumnModal()\"\r\n [disabled]=\"isLoading()\"\r\n title=\"Add new column with automatic reordering\">\r\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\r\n </button>\r\n }\r\n @if (table() && effectiveCanEdit() && sortedColumns().length === 0) {\r\n <small class=\"text-muted\">Import SDRF to add columns</small>\r\n }\r\n } @else {\r\n <span class=\"badge bg-info\">{{ table()!.sampleCount }} samples</span>\r\n @if (hasPools()) {\r\n <span class=\"badge bg-warning\">{{ sortedPools().length }} pools</span>\r\n }\r\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\r\n @if (table() && effectiveCanEdit()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-success btn-sm\"\r\n (click)=\"openAddColumnModal()\"\r\n [disabled]=\"isLoading()\"\r\n title=\"Add new column with automatic reordering\">\r\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n @if (hasColumns()) {\r\n <!-- List Mode: Column Definitions -->\r\n @if (viewMode() === 'list') {\r\n <div class=\"table-responsive\" appDualScroll>\r\n <table class=\"table table-hover mb-0\">\r\n <thead>\r\n <tr>\r\n <th scope=\"col\" style=\"width: 40px;\">\r\n @if (effectiveCanEdit()) {\r\n <i class=\"bi bi-arrows-move text-muted\" title=\"Drag to reorder\"></i>\r\n }\r\n </th>\r\n <th scope=\"col\" style=\"width: 60px;\" \r\n class=\"sortable\" \r\n (click)=\"sortBy('position')\"\r\n [class.sorted]=\"isSorted('position')\">\r\n <div class=\"d-flex align-items-center justify-content-between\">\r\n <span>#</span>\r\n <i class=\"bi ms-1\" [class]=\"getSortIcon('position')\"></i>\r\n </div>\r\n </th>\r\n <th scope=\"col\" \r\n class=\"sortable\" \r\n (click)=\"sortBy('name')\"\r\n [class.sorted]=\"isSorted('name')\">\r\n <div class=\"d-flex align-items-center justify-content-between\">\r\n <span>Column Name</span>\r\n <i class=\"bi ms-1\" [class]=\"getSortIcon('name')\"></i>\r\n </div>\r\n </th>\r\n <th scope=\"col\" \r\n class=\"sortable\" \r\n (click)=\"sortBy('type')\"\r\n [class.sorted]=\"isSorted('type')\">\r\n <div class=\"d-flex align-items-center justify-content-between\">\r\n <span>Type</span>\r\n <i class=\"bi ms-1\" [class]=\"getSortIcon('type')\"></i>\r\n </div>\r\n </th>\r\n <th scope=\"col\" \r\n class=\"sortable\" \r\n (click)=\"sortBy('value')\"\r\n [class.sorted]=\"isSorted('value')\">\r\n <div class=\"d-flex align-items-center justify-content-between\">\r\n <span>Value</span>\r\n <i class=\"bi ms-1\" [class]=\"getSortIcon('value')\"></i>\r\n </div>\r\n </th>\r\n <th scope=\"col\">Properties</th>\r\n <th scope=\"col\" style=\"width: 100px;\">Actions</th>\r\n </tr>\r\n </thead>\r\n <tbody cdkDropList (cdkDropListDropped)=\"onColumnDrop($event)\" [cdkDropListData]=\"dragColumnsForList()\">\r\n @for (column of dragColumnsForList(); track column.id) {\r\n <tr cdkDrag [cdkDragData]=\"column\" [cdkDragDisabled]=\"!effectiveCanEdit()\">\r\n <td class=\"text-center\">\r\n @if (effectiveCanEdit()) {\r\n <i class=\"bi bi-grip-vertical drag-handle\" cdkDragHandle></i>\r\n }\r\n </td>\r\n <td class=\"text-muted fw-semibold\">\r\n {{ (column.columnPosition || 0) + 1 }}\r\n </td>\r\n <td>\r\n <div class=\"d-flex align-items-center\">\r\n <i class=\"me-2\" [class]=\"getColumnTypeIcon(column) + ' ' + getColumnTypeClass(column)\"></i>\r\n <span class=\"fw-semibold\">{{ column.name }}</span>\r\n </div>\r\n </td>\r\n <td>\r\n <span class=\"badge bg-light text-dark\">{{ column.type }}</span>\r\n </td>\r\n <td>\r\n <div>\r\n <!-- Default Value -->\r\n <div class=\"fw-semibold\">{{ getColumnValueDisplay(column) }}</div>\r\n\r\n <!-- Sample-specific Modifiers -->\r\n @if (hasModifiers(column)) {\r\n <div class=\"mt-1\">\r\n @for (modifierDisplay of getModifiersDisplay(column); track $index) {\r\n <small class=\"badge bg-light text-dark me-1 mb-1 d-inline-block\"\r\n style=\"font-size: 0.7rem;\"\r\n [title]=\"modifierDisplay\">\r\n {{ modifierDisplay }}\r\n </small>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </td>\r\n <td>\r\n <div class=\"d-flex gap-1 flex-wrap\">\r\n @if (column.mandatory) {\r\n <span class=\"badge bg-danger\" title=\"Required field\">Required</span>\r\n }\r\n @if (column.readonly) {\r\n <span class=\"badge bg-secondary\" title=\"Read-only field\">Read-only</span>\r\n }\r\n @if (column.hidden) {\r\n <span class=\"badge bg-warning\" title=\"Hidden field\">Hidden</span>\r\n }\r\n @if (column.autoGenerated) {\r\n <span class=\"badge bg-info\" title=\"Auto-generated field\">Auto</span>\r\n }\r\n @if (column.ontologyType) {\r\n <span class=\"badge bg-success\" title=\"Ontology: {{ column.ontologyType }}\">\r\n {{ column.ontologyType }}\r\n </span>\r\n }\r\n </div>\r\n </td>\r\n <td>\r\n <div class=\"btn-group btn-group-sm\">\r\n <button type=\"button\" class=\"btn btn-outline-primary\"\r\n (click)=\"viewColumnDetails(column)\"\r\n title=\"View column details\"\r\n [attr.aria-label]=\"'View details for ' + column.name\">\r\n <i class=\"bi bi-info-circle\" aria-hidden=\"true\"></i>\r\n </button>\r\n @if (effectiveCanEdit()) {\r\n <button type=\"button\"\r\n class=\"btn\"\r\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\r\n (click)=\"toggleColumnHidden(column)\"\r\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\"\r\n [attr.aria-label]=\"column.hidden ? 'Show ' + column.name : 'Hide ' + column.name\">\r\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\" aria-hidden=\"true\"></i>\r\n </button>\r\n <button type=\"button\" class=\"btn btn-outline-success\"\r\n (click)=\"editColumnValue(column)\"\r\n title=\"Edit default value\"\r\n [attr.aria-label]=\"'Edit default value for ' + column.name\">\r\n <i class=\"bi bi-pencil-square\" aria-hidden=\"true\"></i>\r\n </button>\r\n <button type=\"button\" class=\"btn btn-outline-info\"\r\n (click)=\"autofillColumnValues(column)\"\r\n title=\"Auto-fill values\"\r\n [attr.aria-label]=\"'Auto-fill values for ' + column.name\">\r\n <i class=\"bi bi-magic\" aria-hidden=\"true\"></i>\r\n </button>\r\n <button type=\"button\" class=\"btn btn-outline-secondary\"\r\n (click)=\"editColumnSettings(column)\"\r\n title=\"Edit column settings\"\r\n [attr.aria-label]=\"'Edit settings for ' + column.name\">\r\n <i class=\"bi bi-gear\" aria-hidden=\"true\"></i>\r\n </button>\r\n <button type=\"button\" class=\"btn btn-outline-info\"\r\n (click)=\"openColumnHistory(column)\"\r\n title=\"View column history\"\r\n [attr.aria-label]=\"'View history for ' + column.name\">\r\n <i class=\"bi bi-clock-history\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </div>\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n }\r\n\r\n <!-- Table Data -->\r\n @if (viewMode() === 'table') {\r\n <div class=\"card-body p-0\">\r\n <div class=\"table-responsive\" appDualScroll>\r\n <table class=\"table table-hover mb-0\">\r\n <thead class=\"table-light\">\r\n <tr>\r\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">#</th>\r\n @for (column of sortedColumns(); track column.id) {\r\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\r\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\r\n <span>{{ getShortColumnName(column) }}</span>\r\n @if (effectiveCanEdit()) {\r\n <div class=\"header-actions\">\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm header-edit-btn me-1\"\r\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\r\n (click)=\"toggleColumnHidden(column)\"\r\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\">\r\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\"></i>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\r\n (click)=\"editColumnValue(column)\"\r\n [title]=\"'Edit default value for ' + column.name\">\r\n <i class=\"bi bi-pencil\"></i>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm btn-outline-info header-edit-btn me-1\"\r\n (click)=\"autofillColumnValues(column)\"\r\n [title]=\"'Auto-fill values for ' + column.name\">\r\n <i class=\"bi bi-magic\"></i>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm btn-outline-danger header-edit-btn\"\r\n (click)=\"removeColumn(column)\"\r\n [title]=\"'Remove ' + column.name + ' column'\">\r\n <i class=\"bi bi-trash\"></i>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n </th>\r\n }\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (row of paginatedRows(); track row._sampleIndex) {\r\n <tr class=\"sample-row\" [class.editable]=\"effectiveCanEdit()\">\r\n <td class=\"text-muted fw-semibold\">{{ row._sampleIndex }}</td>\r\n @for (column of sortedColumns(); track column.id) {\r\n <td\r\n class=\"sample-cell\"\r\n [class.opacity-50]=\"column.hidden\"\r\n [attr.data-sample-index]=\"row._sampleIndex\"\r\n [attr.data-column-id]=\"column.id\"\r\n (click)=\"effectiveCanEdit() ? editSampleColumnValue(column, row._sampleIndex) : null\"\r\n [title]=\"effectiveCanEdit() ? 'Click to edit ' + column.name + ' for sample ' + row._sampleIndex : ''\">\r\n {{ row['col_' + column.id] || '-' }}\r\n </td>\r\n }\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n\r\n <!-- Table Mode Pagination -->\r\n @if (viewMode() === 'table' && totalPages() > 1) {\r\n <div class=\"d-flex justify-content-between align-items-center p-3 border-top\">\r\n <div class=\"d-flex align-items-center gap-2\">\r\n <span class=\"text-muted\">Rows per page:</span>\r\n <select class=\"form-select form-select-sm\" style=\"width: auto;\"\r\n [value]=\"pageSize()\"\r\n (change)=\"onPageSizeChange(+($any($event.target).value))\">\r\n <option value=\"10\">10</option>\r\n <option value=\"25\">25</option>\r\n <option value=\"50\">50</option>\r\n <option value=\"100\">100</option>\r\n </select>\r\n </div>\r\n <ngb-pagination\r\n [collectionSize]=\"table()!.sampleCount\"\r\n [page]=\"currentPage()\"\r\n [pageSize]=\"pageSize()\"\r\n [maxSize]=\"5\"\r\n [boundaryLinks]=\"true\"\r\n [rotate]=\"true\"\r\n (pageChange)=\"onPageChange($event)\"\r\n class=\"pagination-sm mb-0\">\r\n </ngb-pagination>\r\n <span class=\"text-muted\">\r\n Showing {{ (currentPage() - 1) * pageSize() + 1 }}-{{ Math.min(currentPage() * pageSize(), table()!.sampleCount) }}\r\n of {{ table()!.sampleCount }} samples\r\n </span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n } @else {\r\n <div class=\"card-body text-center py-5\">\r\n <div class=\"text-muted\">\r\n <i class=\"bi bi-inbox display-1 mb-3\"></i>\r\n <h5>No Columns Found</h5>\r\n <p>This table doesn't have any columns defined yet.</p>\r\n @if (effectiveCanEdit()) {\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"openAddColumnModal()\">\r\n <i class=\"bi bi-plus me-1\"></i>Add First Column\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n </div>\r\n }\r\n</div>\r\n", styles: ["@charset \"UTF-8\";.header-cell{position:relative}.header-cell .header-actions{opacity:0;transition:opacity .15s ease}thead th:hover .header-actions{opacity:1}.header-edit-btn{--bs-btn-padding-y: .125rem;--bs-btn-padding-x: .25rem;--bs-btn-font-size: .65rem;border:none!important}.header-edit-btn.btn-outline-primary:hover{background-color:var(--bs-primary);border-color:var(--bs-primary)}.header-edit-btn.btn-outline-danger:hover{background-color:var(--bs-danger);border-color:var(--bs-danger)}.sample-row.editable .sample-cell{cursor:pointer;transition:background-color .1s ease}.sample-row.editable .sample-cell:hover{background-color:var(--cupcake-table-hover);position:relative}.sample-row.editable .sample-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:var(--cupcake-text-muted);font-size:.75em;pointer-events:none}.pool-row.editable td.clickable{cursor:pointer;transition:background-color .1s ease}.pool-row.editable td.clickable:hover{background-color:var(--cupcake-table-hover);color:var(--cupcake-text)}.pool-row.editable td.clickable:hover .bi-pencil-square{opacity:.8}.pool-row.editable td.clickable .bi-pencil-square{opacity:.4;transition:opacity .1s ease}.pool-row.editable .pool-cell{cursor:pointer;transition:background-color .1s ease}.pool-row.editable .pool-cell:hover{background-color:var(--cupcake-table-hover);position:relative}.pool-row.editable .pool-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:var(--cupcake-text-muted);font-size:.75em;pointer-events:none}@media(max-width:768px){.sample-row.editable .sample-cell,.pool-row.editable .pool-cell{transition:none}}.sortable{cursor:pointer;user-select:none;transition:background-color .2s ease}.sortable:hover{background-color:var(--cupcake-table-hover)}.sortable.sorted{background-color:var(--cupcake-bg-secondary)}.sortable.sorted .bi{color:var(--cupcake-primary)}.sortable .bi{opacity:.5;transition:all .2s ease;font-size:.75em}.sortable:hover .bi{opacity:.8}.drag-handle{cursor:grab;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active{cursor:grabbing}.cdk-drag-preview{box-sizing:border-box;border-radius:4px;box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;background:var(--bs-light);border:1px solid var(--bs-border-color)}.cdk-drag-preview .header-cell{padding:.5rem;font-size:.875rem;font-weight:500}.cdk-drag-placeholder{opacity:.5;background:var(--cupcake-table-hover)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.cdk-drop-list-dragging th:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "ngmodule", type: NgbModule }, { kind: "directive", type: i6.NgbDropdown, selector: "[ngbDropdown]", inputs: ["autoClose", "dropdownClass", "open", "placement", "popperOptions", "container", "display"], outputs: ["openChange"], exportAs: ["ngbDropdown"] }, { kind: "directive", type: i6.NgbDropdownToggle, selector: "[ngbDropdownToggle]" }, { kind: "directive", type: i6.NgbDropdownMenu, selector: "[ngbDropdownMenu]" }, { kind: "component", type: i7$1.NgbPagination, selector: "ngb-pagination", inputs: ["disabled", "boundaryLinks", "directionLinks", "ellipses", "rotate", "collectionSize", "maxSize", "page", "pageSize", "size"], outputs: ["pageChange"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i6$2.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i6$2.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i6$2.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: DualScrollDirective, selector: "[appDualScroll]" }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11008
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: MetadataTableDetails, isStandalone: true, selector: "ccv-metadata-table-details", inputs: { readonlyMode: "readonlyMode", preloadedTable: "preloadedTable" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }, { propertyName: "excelInput", first: true, predicate: ["excelInput"], descendants: true }], ngImport: i0, template: "<div class=\"metadata-table-details-container\">\n <!-- Header Section with Breadcrumbs -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100 flex-wrap gap-2\">\n <div class=\"d-flex align-items-center flex-wrap gap-2\">\n <!-- Breadcrumb Navigation -->\n <nav aria-label=\"breadcrumb\" class=\"me-3\">\n <ol class=\"breadcrumb mb-0\">\n @if (!readonlyMode) {\n <li class=\"breadcrumb-item\">\n <a routerLink=\"/metadata-tables\" class=\"text-decoration-none\">\n <i class=\"bi bi-table me-1\"></i>Metadata Tables\n </a>\n </li>\n }\n <li class=\"breadcrumb-item active\" aria-current=\"page\">\n @if (table()) {\n {{ table()!.name }}\n } @else {\n Table Details\n }\n </li>\n </ol>\n </nav>\n @if (readonlyMode) {\n <span class=\"badge bg-secondary ms-2\">\n <i class=\"bi bi-eye me-1\"></i>Read-only\n </span>\n }\n\n <!-- Table Status Badge -->\n @if (table()) {\n <span class=\"badge ms-2\" [class]=\"getTableStatusClass()\">\n {{ getTableStatusBadge() }}\n </span>\n }\n </div>\n\n <!-- Action Buttons -->\n <div class=\"d-flex gap-2 flex-wrap\">\n @if (!readonlyMode) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary btn-sm\"\n (click)=\"navigateBack()\"\n title=\"Back to Tables\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back\n </button>\n }\n\n <!-- View Mode Toggle -->\n @if (hasColumns()) {\n <div class=\"btn-group\" role=\"group\">\n <button\n type=\"button\"\n class=\"btn btn-sm\"\n [class]=\"viewMode() === 'list' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"switchToListMode()\"\n title=\"Column List View\">\n <i class=\"bi bi-list-ul me-1\"></i>List\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm\"\n [class]=\"viewMode() === 'table' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"switchToTableMode()\"\n title=\"Table Data View\">\n <i class=\"bi bi-table me-1\"></i>Table\n </button>\n </div>\n }\n <!-- Import Dropdown -->\n @if (table() && effectiveCanEdit()) {\n <div class=\"btn-group\" ngbDropdown>\n <!-- Hidden file inputs -->\n <input\n #fileInput\n type=\"file\"\n accept=\".txt,.tsv\"\n style=\"display: none;\"\n (change)=\"importSdrf($event)\">\n <input\n #excelInput\n type=\"file\"\n accept=\".xlsx,.xls\"\n style=\"display: none;\"\n (change)=\"importExcel($event)\">\n\n <button\n type=\"button\"\n class=\"btn btn-outline-success btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\"\n title=\"Import Data\">\n <i class=\"bi bi-upload me-1\"></i>Import\n </button>\n <div class=\"dropdown-menu\" ngbDropdownMenu>\n <div class=\"dropdown-item\" (click)=\"$event.stopPropagation()\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"overrideSampleCountToggle\"\n [checked]=\"overrideSampleCount()\"\n (change)=\"overrideSampleCount.set(!overrideSampleCount())\">\n <label class=\"form-check-label small\" for=\"overrideSampleCountToggle\" title=\"Use the sample count from the imported file instead of the current table count\">Override count</label>\n </div>\n </div>\n <div class=\"dropdown-item\" (click)=\"$event.stopPropagation()\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"applySchemaTemplatesToggle\"\n [checked]=\"applySchemaTemplates()\"\n (change)=\"applySchemaTemplates.set(!applySchemaTemplates())\">\n <label class=\"form-check-label small\" for=\"applySchemaTemplatesToggle\" title=\"Pre-populate empty table columns from the official schema declared in the SDRF file\">Apply schema</label>\n </div>\n </div>\n <div class=\"dropdown-divider\"></div>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerFileInput(); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Import SDRF File\n </a>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerExcelInput(); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Import Excel Template\n </a>\n <div class=\"dropdown-divider\"></div>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"openColumnOverrideImportModal(); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-arrow-up me-2\"></i>Import / Override Columns\n </a>\n </div>\n </div>\n }\n\n <!-- Export Dropdown -->\n @if (table() && hasColumns() && !readonlyMode) {\n <div class=\"btn-group\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-outline-info btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\"\n title=\"Export Table\">\n <i class=\"bi bi-download me-1\"></i>Export\n </button>\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('sdrf'); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Export as SDRF\n </a>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('excel'); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Export as Excel\n </a>\n </div>\n </div>\n }\n\n <!-- Share Link Dropdown -->\n @if (table() && effectiveCanEdit()) {\n <div class=\"btn-group\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-sm dropdown-toggle\"\n [class]=\"shareUrl() ? 'btn-success' : 'btn-outline-secondary'\"\n ngbDropdownToggle\n title=\"Share Table\">\n <i class=\"bi bi-share me-1\"></i>Share\n </button>\n <div class=\"dropdown-menu dropdown-menu-end p-2\" ngbDropdownMenu style=\"min-width: 320px;\">\n @if (shareUrl()) {\n <div class=\"mb-2\">\n <label class=\"form-label small fw-semibold mb-1\">Share link</label>\n <div class=\"input-group input-group-sm\">\n <input type=\"text\" class=\"form-control form-control-sm font-monospace\"\n [value]=\"shareUrl()\" readonly>\n <button class=\"btn btn-outline-secondary btn-sm\" type=\"button\"\n (click)=\"copyShareLink()\"\n title=\"Copy link\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n </div>\n </div>\n <div class=\"d-flex gap-2\">\n <button class=\"btn btn-outline-primary btn-sm flex-fill\"\n (click)=\"generateShareLink()\"\n title=\"Regenerate link (invalidates current link)\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Regenerate\n </button>\n <button class=\"btn btn-outline-danger btn-sm flex-fill\"\n (click)=\"revokeShareLink()\"\n title=\"Revoke link\">\n <i class=\"bi bi-x-circle me-1\"></i>Revoke\n </button>\n </div>\n } @else {\n <p class=\"text-muted small mb-2\">No share link. Generate one to let anyone view this table without logging in.</p>\n <button class=\"btn btn-primary btn-sm w-100\"\n (click)=\"generateShareLink()\">\n <i class=\"bi bi-link-45deg me-1\"></i>Generate Share Link\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Edit Button -->\n @if (table() && effectiveCanEdit()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"editTable()\"\n [disabled]=\"isLoading()\"\n title=\"Edit Table\">\n <i class=\"bi bi-pencil me-1\"></i>Edit\n </button>\n }\n\n <!-- Reorder Columns Button -->\n @if (table() && effectiveCanEdit() && hasColumns()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-warning btn-sm\"\n (click)=\"reorderColumns()\"\n [disabled]=\"isLoading()\"\n title=\"Reorder Columns by Schema\">\n <i class=\"bi bi-arrows-move me-1\"></i>Reorder\n </button>\n }\n\n <!-- Create Pool Button -->\n @if (table() && effectiveCanEdit() && hasColumns()) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"createPool()\"\n [disabled]=\"isLoading()\"\n title=\"Create Sample Pool\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create Pool\n </button>\n }\n\n <!-- More Actions Dropdown -->\n @if (table() && effectiveCanEdit()) {\n <div class=\"btn-group\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-outline-danger btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\"\n aria-label=\"More actions\">\n <i class=\"bi bi-three-dots-vertical\" aria-hidden=\"true\"></i>\n </button>\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\n <a class=\"dropdown-item text-danger\" href=\"#\" (click)=\"deleteTable(); $event.preventDefault()\">\n <i class=\"bi bi-trash me-2\"></i>Delete Table\n </a>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n </nav>\n\n <!-- Loading Spinner -->\n @if (isLoading()) {\n <div class=\"d-flex justify-content-center align-items-center\" style=\"height: 400px;\">\n <div class=\"spinner-border text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n </div>\n }\n\n <!-- Table Details Content -->\n @if (table() && !isLoading()) {\n <div class=\"table-details-content p-4\">\n\n <!-- Table Information Card -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-info-circle me-2\"></i>Table Information\n </h5>\n <small class=\"text-muted\">\n Created {{ table()!.createdAt | date:'medium' }}\n </small>\n </div>\n </div>\n <div class=\"card-body\">\n <div class=\"row\">\n <div class=\"col-md-8\">\n <h3 class=\"fw-bold mb-3\">{{ table()!.name }}</h3>\n @if (table()!.description) {\n <p class=\"text-muted mb-3\">{{ table()!.description }}</p>\n }\n <div class=\"row\">\n <div class=\"col-md-6\">\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-hash text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Sample Count:</span>\n <span class=\"badge bg-primary\">{{ table()!.sampleCount }}</span>\n </div>\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-columns-gap text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Columns:</span>\n <span class=\"badge bg-info\">{{ table()!.columns?.length || 0 }}</span>\n </div>\n </div>\n <div class=\"col-md-6\">\n @if (table()!.ownerUsername) {\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-person text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Owner:</span>\n <span>{{ table()!.ownerUsername }}</span>\n </div>\n }\n @if (table()!.labGroupName) {\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-people text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Lab Group:</span>\n <span>{{ table()!.labGroupName }}</span>\n </div>\n }\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-tag text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Version:</span>\n <span class=\"badge bg-light text-dark\">{{ table()!.version || 'v1.0' }}</span>\n </div>\n </div>\n </div>\n </div>\n <div class=\"col-md-4 text-end\">\n <div class=\"d-flex flex-column gap-2\">\n @if (table()!.isLocked) {\n <span class=\"badge bg-warning\">\n <i class=\"bi bi-lock me-1\"></i>Locked\n </span>\n }\n @if (table()!.isPublished) {\n <span class=\"badge bg-success\">\n <i class=\"bi bi-cloud-check me-1\"></i>Published\n </span>\n }\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Sample Pool Table -->\n @if (viewMode() === 'table' && hasPools()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-layers me-2\"></i>Sample Pool Table\n <span class=\"badge bg-warning ms-2\">{{ sortedPools().length }} pools</span>\n </h5>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"table-responsive\" appDualScroll>\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">Pool</th>\n @for (column of sortedColumns(); track column.id) {\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\n <span>{{ getShortColumnName(column) }}</span>\n @if (effectiveCanEdit()) {\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\n (click)=\"editColumnValue(column)\"\n [title]=\"'Edit default value for ' + column.name\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n </div>\n }\n </div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (poolRow of poolTableRows(); track poolRow._poolId) {\n <tr class=\"pool-row\" [class.editable]=\"effectiveCanEdit()\">\n <td class=\"text-muted fw-semibold\" \n [class.clickable]=\"effectiveCanEdit()\"\n (click)=\"effectiveCanEdit() ? editPool(poolRow._poolData) : null\"\n [title]=\"effectiveCanEdit() ? 'Click to edit pool: ' + poolRow._poolName : poolRow._poolName\">\n <i class=\"bi bi-layers me-2\"></i>{{ poolRow._poolName }}\n @if (effectiveCanEdit()) {\n <i class=\"bi bi-pencil-square ms-1 text-primary\" style=\"font-size: 0.8em;\"></i>\n }\n </td>\n @for (column of sortedColumns(); track column.id) {\n <td\n class=\"pool-cell\"\n [class.opacity-50]=\"column.hidden\"\n [attr.data-pool-id]=\"poolRow._poolId\"\n [attr.data-column-id]=\"column.id\"\n (click)=\"effectiveCanEdit() ? editPoolColumnValue(poolRow._poolData, column) : null\"\n [title]=\"effectiveCanEdit() ? 'Click to edit ' + column.name + ' for pool ' + poolRow._poolName : ''\">\n {{ poolRow['col_' + column.id] || '-' }}\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n </div>\n }\n\n <!-- Columns/Table Section -->\n <div class=\"card\">\n <div class=\"card-header\">\n <div class=\"d-flex align-items-center justify-content-between flex-wrap gap-2\">\n <h5 class=\"mb-0\">\n @if (viewMode() === 'list') {\n <i class=\"bi bi-list-columns me-2\"></i>Table Columns\n } @else {\n <i class=\"bi bi-table me-2\"></i>Table Data\n }\n </h5>\n\n <!-- Column Filter -->\n @if (hasColumns()) {\n <div class=\"d-flex align-items-center flex-wrap gap-2\">\n <div class=\"input-group input-group-sm\" style=\"width: 250px; max-width: 100%;\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-funnel text-muted\"></i>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"Filter columns...\"\n [value]=\"columnFilter()\"\n (input)=\"onColumnFilterChange($any($event.target).value)\">\n @if (columnFilter()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearColumnFilter()\"\n title=\"Clear filter\"\n aria-label=\"Clear column filter\">\n <i class=\"bi bi-x\" aria-hidden=\"true\"></i>\n </button>\n }\n </div>\n @if (columnFilter()) {\n <small class=\"text-muted ms-2\">\n {{ filteredColumnCount }}/{{ totalColumnCount }}\n </small>\n }\n </div>\n }\n \n <div class=\"d-flex gap-2 align-items-center\">\n @if (viewMode() === 'list') {\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\n @if (table() && effectiveCanEdit()) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"openAddColumnModal()\"\n [disabled]=\"isLoading()\"\n title=\"Add new column with automatic reordering\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\n </button>\n }\n @if (table() && effectiveCanEdit() && sortedColumns().length === 0) {\n <small class=\"text-muted\">Import SDRF to add columns</small>\n }\n } @else {\n <span class=\"badge bg-info\">{{ table()!.sampleCount }} samples</span>\n @if (hasPools()) {\n <span class=\"badge bg-warning\">{{ sortedPools().length }} pools</span>\n }\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\n @if (table() && effectiveCanEdit()) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"openAddColumnModal()\"\n [disabled]=\"isLoading()\"\n title=\"Add new column with automatic reordering\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\n </button>\n }\n }\n </div>\n </div>\n </div>\n\n @if (hasColumns()) {\n <!-- List Mode: Column Definitions -->\n @if (viewMode() === 'list') {\n <div class=\"table-responsive\" appDualScroll>\n <table class=\"table table-hover mb-0\">\n <thead>\n <tr>\n <th scope=\"col\" style=\"width: 40px;\">\n @if (effectiveCanEdit()) {\n <i class=\"bi bi-arrows-move text-muted\" title=\"Drag to reorder\"></i>\n }\n </th>\n <th scope=\"col\" style=\"width: 60px;\" \n class=\"sortable\" \n (click)=\"sortBy('position')\"\n [class.sorted]=\"isSorted('position')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>#</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('position')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('name')\"\n [class.sorted]=\"isSorted('name')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Column Name</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('name')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('type')\"\n [class.sorted]=\"isSorted('type')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Type</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('type')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('value')\"\n [class.sorted]=\"isSorted('value')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Value</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('value')\"></i>\n </div>\n </th>\n <th scope=\"col\">Properties</th>\n <th scope=\"col\" style=\"width: 100px;\">Actions</th>\n </tr>\n </thead>\n <tbody cdkDropList (cdkDropListDropped)=\"onColumnDrop($event)\" [cdkDropListData]=\"dragColumnsForList()\">\n @for (column of dragColumnsForList(); track column.id) {\n <tr cdkDrag [cdkDragData]=\"column\" [cdkDragDisabled]=\"!effectiveCanEdit()\">\n <td class=\"text-center\">\n @if (effectiveCanEdit()) {\n <i class=\"bi bi-grip-vertical drag-handle\" cdkDragHandle></i>\n }\n </td>\n <td class=\"text-muted fw-semibold\">\n {{ (column.columnPosition || 0) + 1 }}\n </td>\n <td>\n <div class=\"d-flex align-items-center\">\n <i class=\"me-2\" [class]=\"getColumnTypeIcon(column) + ' ' + getColumnTypeClass(column)\"></i>\n <span class=\"fw-semibold\">{{ column.name }}</span>\n </div>\n </td>\n <td>\n <span class=\"badge bg-light text-dark\">{{ column.type }}</span>\n </td>\n <td>\n <div>\n <!-- Default Value -->\n <div class=\"fw-semibold\">{{ getColumnValueDisplay(column) }}</div>\n\n <!-- Sample-specific Modifiers -->\n @if (hasModifiers(column)) {\n <div class=\"mt-1\">\n @for (modifierDisplay of getModifiersDisplay(column); track $index) {\n <small class=\"badge bg-light text-dark me-1 mb-1 d-inline-block\"\n style=\"font-size: 0.7rem;\"\n [title]=\"modifierDisplay\">\n {{ modifierDisplay }}\n </small>\n }\n </div>\n }\n </div>\n </td>\n <td>\n <div class=\"d-flex gap-1 flex-wrap\">\n @if (column.mandatory) {\n <span class=\"badge bg-danger\" title=\"Required field\">Required</span>\n }\n @if (column.readonly) {\n <span class=\"badge bg-secondary\" title=\"Read-only field\">Read-only</span>\n }\n @if (column.hidden) {\n <span class=\"badge bg-warning\" title=\"Hidden field\">Hidden</span>\n }\n @if (column.autoGenerated) {\n <span class=\"badge bg-info\" title=\"Auto-generated field\">Auto</span>\n }\n @if (column.ontologyType) {\n <span class=\"badge bg-success\" title=\"Ontology: {{ column.ontologyType }}\">\n {{ column.ontologyType }}\n </span>\n }\n </div>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\">\n <button type=\"button\" class=\"btn btn-outline-primary\"\n (click)=\"viewColumnDetails(column)\"\n title=\"View column details\"\n [attr.aria-label]=\"'View details for ' + column.name\">\n <i class=\"bi bi-info-circle\" aria-hidden=\"true\"></i>\n </button>\n @if (effectiveCanEdit()) {\n <button type=\"button\"\n class=\"btn\"\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\n (click)=\"toggleColumnHidden(column)\"\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\"\n [attr.aria-label]=\"column.hidden ? 'Show ' + column.name : 'Hide ' + column.name\">\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\" aria-hidden=\"true\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-success\"\n (click)=\"editColumnValue(column)\"\n title=\"Edit default value\"\n [attr.aria-label]=\"'Edit default value for ' + column.name\">\n <i class=\"bi bi-pencil-square\" aria-hidden=\"true\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-info\"\n (click)=\"autofillColumnValues(column)\"\n title=\"Auto-fill values\"\n [attr.aria-label]=\"'Auto-fill values for ' + column.name\">\n <i class=\"bi bi-magic\" aria-hidden=\"true\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\"\n (click)=\"editColumnSettings(column)\"\n title=\"Edit column settings\"\n [attr.aria-label]=\"'Edit settings for ' + column.name\">\n <i class=\"bi bi-gear\" aria-hidden=\"true\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-info\"\n (click)=\"openColumnHistory(column)\"\n title=\"View column history\"\n [attr.aria-label]=\"'View history for ' + column.name\">\n <i class=\"bi bi-clock-history\" aria-hidden=\"true\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- Table Data -->\n @if (viewMode() === 'table') {\n <div class=\"card-body p-0\">\n <div class=\"table-responsive\" appDualScroll>\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">#</th>\n @for (column of sortedColumns(); track column.id) {\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\n <span>{{ getShortColumnName(column) }}</span>\n @if (effectiveCanEdit()) {\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"btn btn-sm header-edit-btn me-1\"\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\n (click)=\"toggleColumnHidden(column)\"\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\">\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\n (click)=\"editColumnValue(column)\"\n [title]=\"'Edit default value for ' + column.name\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-info header-edit-btn me-1\"\n (click)=\"autofillColumnValues(column)\"\n [title]=\"'Auto-fill values for ' + column.name\">\n <i class=\"bi bi-magic\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-danger header-edit-btn\"\n (click)=\"removeColumn(column)\"\n [title]=\"'Remove ' + column.name + ' column'\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n }\n </div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of paginatedRows(); track row._sampleIndex) {\n <tr class=\"sample-row\" [class.editable]=\"effectiveCanEdit()\">\n <td class=\"text-muted fw-semibold\">{{ row._sampleIndex }}</td>\n @for (column of sortedColumns(); track column.id) {\n <td\n class=\"sample-cell\"\n [class.opacity-50]=\"column.hidden\"\n [attr.data-sample-index]=\"row._sampleIndex\"\n [attr.data-column-id]=\"column.id\"\n (click)=\"effectiveCanEdit() ? editSampleColumnValue(column, row._sampleIndex) : null\"\n [title]=\"effectiveCanEdit() ? 'Click to edit ' + column.name + ' for sample ' + row._sampleIndex : ''\">\n {{ row['col_' + column.id] || '-' }}\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Table Mode Pagination -->\n @if (viewMode() === 'table' && totalPages() > 1) {\n <div class=\"d-flex justify-content-between align-items-center p-3 border-top\">\n <div class=\"d-flex align-items-center gap-2\">\n <span class=\"text-muted\">Rows per page:</span>\n <select class=\"form-select form-select-sm\" style=\"width: auto;\"\n [value]=\"pageSize()\"\n (change)=\"onPageSizeChange(+($any($event.target).value))\">\n <option value=\"10\">10</option>\n <option value=\"25\">25</option>\n <option value=\"50\">50</option>\n <option value=\"100\">100</option>\n </select>\n </div>\n <ngb-pagination\n [collectionSize]=\"table()!.sampleCount\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n <span class=\"text-muted\">\n Showing {{ (currentPage() - 1) * pageSize() + 1 }}-{{ Math.min(currentPage() * pageSize(), table()!.sampleCount) }}\n of {{ table()!.sampleCount }} samples\n </span>\n </div>\n }\n </div>\n }\n } @else {\n <div class=\"card-body text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-inbox display-1 mb-3\"></i>\n <h5>No Columns Found</h5>\n <p>This table doesn't have any columns defined yet.</p>\n @if (effectiveCanEdit()) {\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"openAddColumnModal()\">\n <i class=\"bi bi-plus me-1\"></i>Add First Column\n </button>\n }\n </div>\n </div>\n }\n </div>\n\n </div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.header-cell{position:relative}.header-cell .header-actions{opacity:0;transition:opacity .15s ease}thead th:hover .header-actions{opacity:1}.header-edit-btn{--bs-btn-padding-y: .125rem;--bs-btn-padding-x: .25rem;--bs-btn-font-size: .65rem;border:none!important}.header-edit-btn.btn-outline-primary:hover{background-color:var(--bs-primary);border-color:var(--bs-primary)}.header-edit-btn.btn-outline-danger:hover{background-color:var(--bs-danger);border-color:var(--bs-danger)}.sample-row.editable .sample-cell{cursor:pointer;transition:background-color .1s ease}.sample-row.editable .sample-cell:hover{background-color:var(--cupcake-table-hover);position:relative}.sample-row.editable .sample-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:var(--cupcake-text-muted);font-size:.75em;pointer-events:none}.pool-row.editable td.clickable{cursor:pointer;transition:background-color .1s ease}.pool-row.editable td.clickable:hover{background-color:var(--cupcake-table-hover);color:var(--cupcake-text)}.pool-row.editable td.clickable:hover .bi-pencil-square{opacity:.8}.pool-row.editable td.clickable .bi-pencil-square{opacity:.4;transition:opacity .1s ease}.pool-row.editable .pool-cell{cursor:pointer;transition:background-color .1s ease}.pool-row.editable .pool-cell:hover{background-color:var(--cupcake-table-hover);position:relative}.pool-row.editable .pool-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:var(--cupcake-text-muted);font-size:.75em;pointer-events:none}@media(max-width:768px){.sample-row.editable .sample-cell,.pool-row.editable .pool-cell{transition:none}}.sortable{cursor:pointer;user-select:none;transition:background-color .2s ease}.sortable:hover{background-color:var(--cupcake-table-hover)}.sortable.sorted{background-color:var(--cupcake-bg-secondary)}.sortable.sorted .bi{color:var(--cupcake-primary)}.sortable .bi{opacity:.5;transition:all .2s ease;font-size:.75em}.sortable:hover .bi{opacity:.8}.drag-handle{cursor:grab;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active{cursor:grabbing}.cdk-drag-preview{box-sizing:border-box;border-radius:4px;box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;background:var(--bs-light);border:1px solid var(--bs-border-color)}.cdk-drag-preview .header-cell{padding:.5rem;font-size:.875rem;font-weight:500}.cdk-drag-placeholder{opacity:.5;background:var(--cupcake-table-hover)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.cdk-drop-list-dragging th:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "ngmodule", type: NgbModule }, { kind: "directive", type: i6.NgbDropdown, selector: "[ngbDropdown]", inputs: ["autoClose", "dropdownClass", "open", "placement", "popperOptions", "container", "display"], outputs: ["openChange"], exportAs: ["ngbDropdown"] }, { kind: "directive", type: i6.NgbDropdownToggle, selector: "[ngbDropdownToggle]" }, { kind: "directive", type: i6.NgbDropdownMenu, selector: "[ngbDropdownMenu]" }, { kind: "component", type: i7$1.NgbPagination, selector: "ngb-pagination", inputs: ["disabled", "boundaryLinks", "directionLinks", "ellipses", "rotate", "collectionSize", "maxSize", "page", "pageSize", "size"], outputs: ["pageChange"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i6$2.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i6$2.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i6$2.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: DualScrollDirective, selector: "[appDualScroll]" }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10980
11009
|
}
|
|
10981
11010
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: MetadataTableDetails, decorators: [{
|
|
10982
11011
|
type: Component,
|
|
10983
|
-
args: [{ selector: 'ccv-metadata-table-details', standalone: true, imports: [CommonModule, RouterModule, NgbModule, DragDropModule, DualScrollDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"metadata-table-details-container\">\r\n <!-- Header Section with Breadcrumbs -->\r\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\r\n <div class=\"container-fluid\">\r\n <div class=\"d-flex align-items-center justify-content-between w-100 flex-wrap gap-2\">\r\n <div class=\"d-flex align-items-center flex-wrap gap-2\">\r\n <!-- Breadcrumb Navigation -->\r\n <nav aria-label=\"breadcrumb\" class=\"me-3\">\r\n <ol class=\"breadcrumb mb-0\">\r\n @if (!readonlyMode) {\r\n <li class=\"breadcrumb-item\">\r\n <a routerLink=\"/metadata-tables\" class=\"text-decoration-none\">\r\n <i class=\"bi bi-table me-1\"></i>Metadata Tables\r\n </a>\r\n </li>\r\n }\r\n <li class=\"breadcrumb-item active\" aria-current=\"page\">\r\n @if (table()) {\r\n {{ table()!.name }}\r\n } @else {\r\n Table Details\r\n }\r\n </li>\r\n </ol>\r\n </nav>\r\n @if (readonlyMode) {\r\n <span class=\"badge bg-secondary ms-2\">\r\n <i class=\"bi bi-eye me-1\"></i>Read-only\r\n </span>\r\n }\r\n\r\n <!-- Table Status Badge -->\r\n @if (table()) {\r\n <span class=\"badge ms-2\" [class]=\"getTableStatusClass()\">\r\n {{ getTableStatusBadge() }}\r\n </span>\r\n }\r\n </div>\r\n\r\n <!-- Action Buttons -->\r\n <div class=\"d-flex gap-2 flex-wrap\">\r\n @if (!readonlyMode) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-secondary btn-sm\"\r\n (click)=\"navigateBack()\"\r\n title=\"Back to Tables\">\r\n <i class=\"bi bi-arrow-left me-1\"></i>Back\r\n </button>\r\n }\r\n\r\n <!-- View Mode Toggle -->\r\n @if (hasColumns()) {\r\n <div class=\"btn-group\" role=\"group\">\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm\"\r\n [class]=\"viewMode() === 'list' ? 'btn-primary' : 'btn-outline-primary'\"\r\n (click)=\"switchToListMode()\"\r\n title=\"Column List View\">\r\n <i class=\"bi bi-list-ul me-1\"></i>List\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm\"\r\n [class]=\"viewMode() === 'table' ? 'btn-primary' : 'btn-outline-primary'\"\r\n (click)=\"switchToTableMode()\"\r\n title=\"Table Data View\">\r\n <i class=\"bi bi-table me-1\"></i>Table\r\n </button>\r\n </div>\r\n }\r\n <!-- Import Dropdown -->\r\n @if (table() && effectiveCanEdit()) {\r\n <div class=\"btn-group\" ngbDropdown>\r\n <!-- Hidden file inputs -->\r\n <input\r\n #fileInput\r\n type=\"file\"\r\n accept=\".txt,.tsv\"\r\n style=\"display: none;\"\r\n (change)=\"importSdrf($event)\">\r\n <input\r\n #excelInput\r\n type=\"file\"\r\n accept=\".xlsx,.xls\"\r\n style=\"display: none;\"\r\n (change)=\"importExcel($event)\">\r\n\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-success btn-sm dropdown-toggle\"\r\n ngbDropdownToggle\r\n [disabled]=\"isLoading()\"\r\n title=\"Import Data\">\r\n <i class=\"bi bi-upload me-1\"></i>Import\r\n </button>\r\n <div class=\"dropdown-menu\" ngbDropdownMenu>\r\n <div class=\"dropdown-item\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"form-check form-switch\">\r\n <input\r\n class=\"form-check-input\"\r\n type=\"checkbox\"\r\n id=\"overrideSampleCountToggle\"\r\n [checked]=\"overrideSampleCount()\"\r\n (change)=\"overrideSampleCount.set(!overrideSampleCount())\">\r\n <label class=\"form-check-label small\" for=\"overrideSampleCountToggle\" title=\"Use the sample count from the imported file instead of the current table count\">Override count</label>\r\n </div>\r\n </div>\r\n <div class=\"dropdown-item\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"form-check form-switch\">\r\n <input\r\n class=\"form-check-input\"\r\n type=\"checkbox\"\r\n id=\"applySchemaTemplatesToggle\"\r\n [checked]=\"applySchemaTemplates()\"\r\n (change)=\"applySchemaTemplates.set(!applySchemaTemplates())\">\r\n <label class=\"form-check-label small\" for=\"applySchemaTemplatesToggle\" title=\"Pre-populate empty table columns from the official schema declared in the SDRF file\">Apply schema</label>\r\n </div>\r\n </div>\r\n <div class=\"dropdown-divider\"></div>\r\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerFileInput(); $event.preventDefault()\">\r\n <i class=\"bi bi-file-earmark-text me-2\"></i>Import SDRF File\r\n </a>\r\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerExcelInput(); $event.preventDefault()\">\r\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Import Excel Template\r\n </a>\r\n <div class=\"dropdown-divider\"></div>\r\n <a class=\"dropdown-item\" href=\"#\" (click)=\"openColumnOverrideImportModal(); $event.preventDefault()\">\r\n <i class=\"bi bi-file-earmark-arrow-up me-2\"></i>Import / Override Columns\r\n </a>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Export Dropdown -->\r\n @if (table() && hasColumns() && !readonlyMode) {\r\n <div class=\"btn-group\" ngbDropdown>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-info btn-sm dropdown-toggle\"\r\n ngbDropdownToggle\r\n [disabled]=\"isLoading()\"\r\n title=\"Export Table\">\r\n <i class=\"bi bi-download me-1\"></i>Export\r\n </button>\r\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\r\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('sdrf'); $event.preventDefault()\">\r\n <i class=\"bi bi-file-earmark-text me-2\"></i>Export as SDRF\r\n </a>\r\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('excel'); $event.preventDefault()\">\r\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Export as Excel\r\n </a>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Share Link Dropdown -->\r\n @if (table() && effectiveCanEdit()) {\r\n <div class=\"btn-group\" ngbDropdown>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm dropdown-toggle\"\r\n [class]=\"shareUrl() ? 'btn-success' : 'btn-outline-secondary'\"\r\n ngbDropdownToggle\r\n title=\"Share Table\">\r\n <i class=\"bi bi-share me-1\"></i>Share\r\n </button>\r\n <div class=\"dropdown-menu dropdown-menu-end p-2\" ngbDropdownMenu style=\"min-width: 320px;\">\r\n @if (shareUrl()) {\r\n <div class=\"mb-2\">\r\n <label class=\"form-label small fw-semibold mb-1\">Share link</label>\r\n <div class=\"input-group input-group-sm\">\r\n <input type=\"text\" class=\"form-control form-control-sm font-monospace\"\r\n [value]=\"shareUrl()\" readonly>\r\n <button class=\"btn btn-outline-secondary btn-sm\" type=\"button\"\r\n (click)=\"copyShareLink()\"\r\n title=\"Copy link\">\r\n <i class=\"bi bi-clipboard\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n <div class=\"d-flex gap-2\">\r\n <button class=\"btn btn-outline-primary btn-sm flex-fill\"\r\n (click)=\"generateShareLink()\"\r\n title=\"Regenerate link (invalidates current link)\">\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Regenerate\r\n </button>\r\n <button class=\"btn btn-outline-danger btn-sm flex-fill\"\r\n (click)=\"revokeShareLink()\"\r\n title=\"Revoke link\">\r\n <i class=\"bi bi-x-circle me-1\"></i>Revoke\r\n </button>\r\n </div>\r\n } @else {\r\n <p class=\"text-muted small mb-2\">No share link. Generate one to let anyone view this table without logging in.</p>\r\n <button class=\"btn btn-primary btn-sm w-100\"\r\n (click)=\"generateShareLink()\">\r\n <i class=\"bi bi-link-45deg me-1\"></i>Generate Share Link\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Edit Button -->\r\n @if (table() && effectiveCanEdit()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-primary btn-sm\"\r\n (click)=\"editTable()\"\r\n [disabled]=\"isLoading()\"\r\n title=\"Edit Table\">\r\n <i class=\"bi bi-pencil me-1\"></i>Edit\r\n </button>\r\n }\r\n\r\n <!-- Reorder Columns Button -->\r\n @if (table() && effectiveCanEdit() && hasColumns()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-warning btn-sm\"\r\n (click)=\"reorderColumns()\"\r\n [disabled]=\"isLoading()\"\r\n title=\"Reorder Columns by Schema\">\r\n <i class=\"bi bi-arrows-move me-1\"></i>Reorder\r\n </button>\r\n }\r\n\r\n <!-- Create Pool Button -->\r\n @if (table() && effectiveCanEdit() && hasColumns()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-success btn-sm\"\r\n (click)=\"createPool()\"\r\n [disabled]=\"isLoading()\"\r\n title=\"Create Sample Pool\">\r\n <i class=\"bi bi-plus-circle me-1\"></i>Create Pool\r\n </button>\r\n }\r\n\r\n <!-- More Actions Dropdown -->\r\n @if (table() && effectiveCanEdit()) {\r\n <div class=\"btn-group\" ngbDropdown>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-danger btn-sm dropdown-toggle\"\r\n ngbDropdownToggle\r\n [disabled]=\"isLoading()\"\r\n aria-label=\"More actions\">\r\n <i class=\"bi bi-three-dots-vertical\" aria-hidden=\"true\"></i>\r\n </button>\r\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\r\n <a class=\"dropdown-item text-danger\" href=\"#\" (click)=\"deleteTable(); $event.preventDefault()\">\r\n <i class=\"bi bi-trash me-2\"></i>Delete Table\r\n </a>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </nav>\r\n\r\n <!-- Loading Spinner -->\r\n @if (isLoading()) {\r\n <div class=\"d-flex justify-content-center align-items-center\" style=\"height: 400px;\">\r\n <div class=\"spinner-border text-primary\" role=\"status\">\r\n <span class=\"visually-hidden\">Loading...</span>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Table Details Content -->\r\n @if (table() && !isLoading()) {\r\n <div class=\"table-details-content p-4\">\r\n\r\n <!-- Table Information Card -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <div class=\"d-flex align-items-center justify-content-between\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-info-circle me-2\"></i>Table Information\r\n </h5>\r\n <small class=\"text-muted\">\r\n Created {{ table()!.createdAt | date:'medium' }}\r\n </small>\r\n </div>\r\n </div>\r\n <div class=\"card-body\">\r\n <div class=\"row\">\r\n <div class=\"col-md-8\">\r\n <h3 class=\"fw-bold mb-3\">{{ table()!.name }}</h3>\r\n @if (table()!.description) {\r\n <p class=\"text-muted mb-3\">{{ table()!.description }}</p>\r\n }\r\n <div class=\"row\">\r\n <div class=\"col-md-6\">\r\n <div class=\"d-flex align-items-center mb-2\">\r\n <i class=\"bi bi-hash text-muted me-2\"></i>\r\n <span class=\"fw-semibold me-2\">Sample Count:</span>\r\n <span class=\"badge bg-primary\">{{ table()!.sampleCount }}</span>\r\n </div>\r\n <div class=\"d-flex align-items-center mb-2\">\r\n <i class=\"bi bi-columns-gap text-muted me-2\"></i>\r\n <span class=\"fw-semibold me-2\">Columns:</span>\r\n <span class=\"badge bg-info\">{{ table()!.columns?.length || 0 }}</span>\r\n </div>\r\n </div>\r\n <div class=\"col-md-6\">\r\n @if (table()!.ownerUsername) {\r\n <div class=\"d-flex align-items-center mb-2\">\r\n <i class=\"bi bi-person text-muted me-2\"></i>\r\n <span class=\"fw-semibold me-2\">Owner:</span>\r\n <span>{{ table()!.ownerUsername }}</span>\r\n </div>\r\n }\r\n @if (table()!.labGroupName) {\r\n <div class=\"d-flex align-items-center mb-2\">\r\n <i class=\"bi bi-people text-muted me-2\"></i>\r\n <span class=\"fw-semibold me-2\">Lab Group:</span>\r\n <span>{{ table()!.labGroupName }}</span>\r\n </div>\r\n }\r\n <div class=\"d-flex align-items-center mb-2\">\r\n <i class=\"bi bi-tag text-muted me-2\"></i>\r\n <span class=\"fw-semibold me-2\">Version:</span>\r\n <span class=\"badge bg-light text-dark\">{{ table()!.version || 'v1.0' }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"col-md-4 text-end\">\r\n <div class=\"d-flex flex-column gap-2\">\r\n @if (table()!.isLocked) {\r\n <span class=\"badge bg-warning\">\r\n <i class=\"bi bi-lock me-1\"></i>Locked\r\n </span>\r\n }\r\n @if (table()!.isPublished) {\r\n <span class=\"badge bg-success\">\r\n <i class=\"bi bi-cloud-check me-1\"></i>Published\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Sample Pool Table -->\r\n @if (viewMode() === 'table' && hasPools()) {\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-layers me-2\"></i>Sample Pool Table\r\n <span class=\"badge bg-warning ms-2\">{{ sortedPools().length }} pools</span>\r\n </h5>\r\n </div>\r\n <div class=\"card-body p-0\">\r\n <div class=\"table-responsive\" appDualScroll>\r\n <table class=\"table table-hover mb-0\">\r\n <thead class=\"table-light\">\r\n <tr>\r\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">Pool</th>\r\n @for (column of sortedColumns(); track column.id) {\r\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\r\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\r\n <span>{{ getShortColumnName(column) }}</span>\r\n @if (effectiveCanEdit()) {\r\n <div class=\"header-actions\">\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\r\n (click)=\"editColumnValue(column)\"\r\n [title]=\"'Edit default value for ' + column.name\">\r\n <i class=\"bi bi-pencil\"></i>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n </th>\r\n }\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (poolRow of poolTableRows(); track poolRow._poolId) {\r\n <tr class=\"pool-row\" [class.editable]=\"effectiveCanEdit()\">\r\n <td class=\"text-muted fw-semibold\" \r\n [class.clickable]=\"effectiveCanEdit()\"\r\n (click)=\"effectiveCanEdit() ? editPool(poolRow._poolData) : null\"\r\n [title]=\"effectiveCanEdit() ? 'Click to edit pool: ' + poolRow._poolName : poolRow._poolName\">\r\n <i class=\"bi bi-layers me-2\"></i>{{ poolRow._poolName }}\r\n @if (effectiveCanEdit()) {\r\n <i class=\"bi bi-pencil-square ms-1 text-primary\" style=\"font-size: 0.8em;\"></i>\r\n }\r\n </td>\r\n @for (column of sortedColumns(); track column.id) {\r\n <td\r\n class=\"pool-cell\"\r\n [class.opacity-50]=\"column.hidden\"\r\n [attr.data-pool-id]=\"poolRow._poolId\"\r\n [attr.data-column-id]=\"column.id\"\r\n (click)=\"effectiveCanEdit() ? editPoolColumnValue(poolRow._poolData, column) : null\"\r\n [title]=\"effectiveCanEdit() ? 'Click to edit ' + column.name + ' for pool ' + poolRow._poolName : ''\">\r\n {{ poolRow['col_' + column.id] || '-' }}\r\n </td>\r\n }\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Columns/Table Section -->\r\n <div class=\"card\">\r\n <div class=\"card-header\">\r\n <div class=\"d-flex align-items-center justify-content-between flex-wrap gap-2\">\r\n <h5 class=\"mb-0\">\r\n @if (viewMode() === 'list') {\r\n <i class=\"bi bi-list-columns me-2\"></i>Table Columns\r\n } @else {\r\n <i class=\"bi bi-table me-2\"></i>Table Data\r\n }\r\n </h5>\r\n\r\n <!-- Column Filter -->\r\n @if (hasColumns()) {\r\n <div class=\"d-flex align-items-center flex-wrap gap-2\">\r\n <div class=\"input-group input-group-sm\" style=\"width: 250px; max-width: 100%;\">\r\n <span class=\"input-group-text\">\r\n <i class=\"bi bi-funnel text-muted\"></i>\r\n </span>\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n placeholder=\"Filter columns...\"\r\n [value]=\"columnFilter()\"\r\n (input)=\"onColumnFilterChange($any($event.target).value)\">\r\n @if (columnFilter()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-secondary\"\r\n (click)=\"clearColumnFilter()\"\r\n title=\"Clear filter\"\r\n aria-label=\"Clear column filter\">\r\n <i class=\"bi bi-x\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </div>\r\n @if (columnFilter()) {\r\n <small class=\"text-muted ms-2\">\r\n {{ filteredColumnCount }}/{{ totalColumnCount }}\r\n </small>\r\n }\r\n </div>\r\n }\r\n \r\n <div class=\"d-flex gap-2 align-items-center\">\r\n @if (viewMode() === 'list') {\r\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\r\n @if (table() && effectiveCanEdit()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-success btn-sm\"\r\n (click)=\"openAddColumnModal()\"\r\n [disabled]=\"isLoading()\"\r\n title=\"Add new column with automatic reordering\">\r\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\r\n </button>\r\n }\r\n @if (table() && effectiveCanEdit() && sortedColumns().length === 0) {\r\n <small class=\"text-muted\">Import SDRF to add columns</small>\r\n }\r\n } @else {\r\n <span class=\"badge bg-info\">{{ table()!.sampleCount }} samples</span>\r\n @if (hasPools()) {\r\n <span class=\"badge bg-warning\">{{ sortedPools().length }} pools</span>\r\n }\r\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\r\n @if (table() && effectiveCanEdit()) {\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-success btn-sm\"\r\n (click)=\"openAddColumnModal()\"\r\n [disabled]=\"isLoading()\"\r\n title=\"Add new column with automatic reordering\">\r\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n @if (hasColumns()) {\r\n <!-- List Mode: Column Definitions -->\r\n @if (viewMode() === 'list') {\r\n <div class=\"table-responsive\" appDualScroll>\r\n <table class=\"table table-hover mb-0\">\r\n <thead>\r\n <tr>\r\n <th scope=\"col\" style=\"width: 40px;\">\r\n @if (effectiveCanEdit()) {\r\n <i class=\"bi bi-arrows-move text-muted\" title=\"Drag to reorder\"></i>\r\n }\r\n </th>\r\n <th scope=\"col\" style=\"width: 60px;\" \r\n class=\"sortable\" \r\n (click)=\"sortBy('position')\"\r\n [class.sorted]=\"isSorted('position')\">\r\n <div class=\"d-flex align-items-center justify-content-between\">\r\n <span>#</span>\r\n <i class=\"bi ms-1\" [class]=\"getSortIcon('position')\"></i>\r\n </div>\r\n </th>\r\n <th scope=\"col\" \r\n class=\"sortable\" \r\n (click)=\"sortBy('name')\"\r\n [class.sorted]=\"isSorted('name')\">\r\n <div class=\"d-flex align-items-center justify-content-between\">\r\n <span>Column Name</span>\r\n <i class=\"bi ms-1\" [class]=\"getSortIcon('name')\"></i>\r\n </div>\r\n </th>\r\n <th scope=\"col\" \r\n class=\"sortable\" \r\n (click)=\"sortBy('type')\"\r\n [class.sorted]=\"isSorted('type')\">\r\n <div class=\"d-flex align-items-center justify-content-between\">\r\n <span>Type</span>\r\n <i class=\"bi ms-1\" [class]=\"getSortIcon('type')\"></i>\r\n </div>\r\n </th>\r\n <th scope=\"col\" \r\n class=\"sortable\" \r\n (click)=\"sortBy('value')\"\r\n [class.sorted]=\"isSorted('value')\">\r\n <div class=\"d-flex align-items-center justify-content-between\">\r\n <span>Value</span>\r\n <i class=\"bi ms-1\" [class]=\"getSortIcon('value')\"></i>\r\n </div>\r\n </th>\r\n <th scope=\"col\">Properties</th>\r\n <th scope=\"col\" style=\"width: 100px;\">Actions</th>\r\n </tr>\r\n </thead>\r\n <tbody cdkDropList (cdkDropListDropped)=\"onColumnDrop($event)\" [cdkDropListData]=\"dragColumnsForList()\">\r\n @for (column of dragColumnsForList(); track column.id) {\r\n <tr cdkDrag [cdkDragData]=\"column\" [cdkDragDisabled]=\"!effectiveCanEdit()\">\r\n <td class=\"text-center\">\r\n @if (effectiveCanEdit()) {\r\n <i class=\"bi bi-grip-vertical drag-handle\" cdkDragHandle></i>\r\n }\r\n </td>\r\n <td class=\"text-muted fw-semibold\">\r\n {{ (column.columnPosition || 0) + 1 }}\r\n </td>\r\n <td>\r\n <div class=\"d-flex align-items-center\">\r\n <i class=\"me-2\" [class]=\"getColumnTypeIcon(column) + ' ' + getColumnTypeClass(column)\"></i>\r\n <span class=\"fw-semibold\">{{ column.name }}</span>\r\n </div>\r\n </td>\r\n <td>\r\n <span class=\"badge bg-light text-dark\">{{ column.type }}</span>\r\n </td>\r\n <td>\r\n <div>\r\n <!-- Default Value -->\r\n <div class=\"fw-semibold\">{{ getColumnValueDisplay(column) }}</div>\r\n\r\n <!-- Sample-specific Modifiers -->\r\n @if (hasModifiers(column)) {\r\n <div class=\"mt-1\">\r\n @for (modifierDisplay of getModifiersDisplay(column); track $index) {\r\n <small class=\"badge bg-light text-dark me-1 mb-1 d-inline-block\"\r\n style=\"font-size: 0.7rem;\"\r\n [title]=\"modifierDisplay\">\r\n {{ modifierDisplay }}\r\n </small>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </td>\r\n <td>\r\n <div class=\"d-flex gap-1 flex-wrap\">\r\n @if (column.mandatory) {\r\n <span class=\"badge bg-danger\" title=\"Required field\">Required</span>\r\n }\r\n @if (column.readonly) {\r\n <span class=\"badge bg-secondary\" title=\"Read-only field\">Read-only</span>\r\n }\r\n @if (column.hidden) {\r\n <span class=\"badge bg-warning\" title=\"Hidden field\">Hidden</span>\r\n }\r\n @if (column.autoGenerated) {\r\n <span class=\"badge bg-info\" title=\"Auto-generated field\">Auto</span>\r\n }\r\n @if (column.ontologyType) {\r\n <span class=\"badge bg-success\" title=\"Ontology: {{ column.ontologyType }}\">\r\n {{ column.ontologyType }}\r\n </span>\r\n }\r\n </div>\r\n </td>\r\n <td>\r\n <div class=\"btn-group btn-group-sm\">\r\n <button type=\"button\" class=\"btn btn-outline-primary\"\r\n (click)=\"viewColumnDetails(column)\"\r\n title=\"View column details\"\r\n [attr.aria-label]=\"'View details for ' + column.name\">\r\n <i class=\"bi bi-info-circle\" aria-hidden=\"true\"></i>\r\n </button>\r\n @if (effectiveCanEdit()) {\r\n <button type=\"button\"\r\n class=\"btn\"\r\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\r\n (click)=\"toggleColumnHidden(column)\"\r\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\"\r\n [attr.aria-label]=\"column.hidden ? 'Show ' + column.name : 'Hide ' + column.name\">\r\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\" aria-hidden=\"true\"></i>\r\n </button>\r\n <button type=\"button\" class=\"btn btn-outline-success\"\r\n (click)=\"editColumnValue(column)\"\r\n title=\"Edit default value\"\r\n [attr.aria-label]=\"'Edit default value for ' + column.name\">\r\n <i class=\"bi bi-pencil-square\" aria-hidden=\"true\"></i>\r\n </button>\r\n <button type=\"button\" class=\"btn btn-outline-info\"\r\n (click)=\"autofillColumnValues(column)\"\r\n title=\"Auto-fill values\"\r\n [attr.aria-label]=\"'Auto-fill values for ' + column.name\">\r\n <i class=\"bi bi-magic\" aria-hidden=\"true\"></i>\r\n </button>\r\n <button type=\"button\" class=\"btn btn-outline-secondary\"\r\n (click)=\"editColumnSettings(column)\"\r\n title=\"Edit column settings\"\r\n [attr.aria-label]=\"'Edit settings for ' + column.name\">\r\n <i class=\"bi bi-gear\" aria-hidden=\"true\"></i>\r\n </button>\r\n <button type=\"button\" class=\"btn btn-outline-info\"\r\n (click)=\"openColumnHistory(column)\"\r\n title=\"View column history\"\r\n [attr.aria-label]=\"'View history for ' + column.name\">\r\n <i class=\"bi bi-clock-history\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </div>\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n }\r\n\r\n <!-- Table Data -->\r\n @if (viewMode() === 'table') {\r\n <div class=\"card-body p-0\">\r\n <div class=\"table-responsive\" appDualScroll>\r\n <table class=\"table table-hover mb-0\">\r\n <thead class=\"table-light\">\r\n <tr>\r\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">#</th>\r\n @for (column of sortedColumns(); track column.id) {\r\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\r\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\r\n <span>{{ getShortColumnName(column) }}</span>\r\n @if (effectiveCanEdit()) {\r\n <div class=\"header-actions\">\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm header-edit-btn me-1\"\r\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\r\n (click)=\"toggleColumnHidden(column)\"\r\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\">\r\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\"></i>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\r\n (click)=\"editColumnValue(column)\"\r\n [title]=\"'Edit default value for ' + column.name\">\r\n <i class=\"bi bi-pencil\"></i>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm btn-outline-info header-edit-btn me-1\"\r\n (click)=\"autofillColumnValues(column)\"\r\n [title]=\"'Auto-fill values for ' + column.name\">\r\n <i class=\"bi bi-magic\"></i>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-sm btn-outline-danger header-edit-btn\"\r\n (click)=\"removeColumn(column)\"\r\n [title]=\"'Remove ' + column.name + ' column'\">\r\n <i class=\"bi bi-trash\"></i>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n </th>\r\n }\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (row of paginatedRows(); track row._sampleIndex) {\r\n <tr class=\"sample-row\" [class.editable]=\"effectiveCanEdit()\">\r\n <td class=\"text-muted fw-semibold\">{{ row._sampleIndex }}</td>\r\n @for (column of sortedColumns(); track column.id) {\r\n <td\r\n class=\"sample-cell\"\r\n [class.opacity-50]=\"column.hidden\"\r\n [attr.data-sample-index]=\"row._sampleIndex\"\r\n [attr.data-column-id]=\"column.id\"\r\n (click)=\"effectiveCanEdit() ? editSampleColumnValue(column, row._sampleIndex) : null\"\r\n [title]=\"effectiveCanEdit() ? 'Click to edit ' + column.name + ' for sample ' + row._sampleIndex : ''\">\r\n {{ row['col_' + column.id] || '-' }}\r\n </td>\r\n }\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n\r\n <!-- Table Mode Pagination -->\r\n @if (viewMode() === 'table' && totalPages() > 1) {\r\n <div class=\"d-flex justify-content-between align-items-center p-3 border-top\">\r\n <div class=\"d-flex align-items-center gap-2\">\r\n <span class=\"text-muted\">Rows per page:</span>\r\n <select class=\"form-select form-select-sm\" style=\"width: auto;\"\r\n [value]=\"pageSize()\"\r\n (change)=\"onPageSizeChange(+($any($event.target).value))\">\r\n <option value=\"10\">10</option>\r\n <option value=\"25\">25</option>\r\n <option value=\"50\">50</option>\r\n <option value=\"100\">100</option>\r\n </select>\r\n </div>\r\n <ngb-pagination\r\n [collectionSize]=\"table()!.sampleCount\"\r\n [page]=\"currentPage()\"\r\n [pageSize]=\"pageSize()\"\r\n [maxSize]=\"5\"\r\n [boundaryLinks]=\"true\"\r\n [rotate]=\"true\"\r\n (pageChange)=\"onPageChange($event)\"\r\n class=\"pagination-sm mb-0\">\r\n </ngb-pagination>\r\n <span class=\"text-muted\">\r\n Showing {{ (currentPage() - 1) * pageSize() + 1 }}-{{ Math.min(currentPage() * pageSize(), table()!.sampleCount) }}\r\n of {{ table()!.sampleCount }} samples\r\n </span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n } @else {\r\n <div class=\"card-body text-center py-5\">\r\n <div class=\"text-muted\">\r\n <i class=\"bi bi-inbox display-1 mb-3\"></i>\r\n <h5>No Columns Found</h5>\r\n <p>This table doesn't have any columns defined yet.</p>\r\n @if (effectiveCanEdit()) {\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"openAddColumnModal()\">\r\n <i class=\"bi bi-plus me-1\"></i>Add First Column\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n </div>\r\n }\r\n</div>\r\n", styles: ["@charset \"UTF-8\";.header-cell{position:relative}.header-cell .header-actions{opacity:0;transition:opacity .15s ease}thead th:hover .header-actions{opacity:1}.header-edit-btn{--bs-btn-padding-y: .125rem;--bs-btn-padding-x: .25rem;--bs-btn-font-size: .65rem;border:none!important}.header-edit-btn.btn-outline-primary:hover{background-color:var(--bs-primary);border-color:var(--bs-primary)}.header-edit-btn.btn-outline-danger:hover{background-color:var(--bs-danger);border-color:var(--bs-danger)}.sample-row.editable .sample-cell{cursor:pointer;transition:background-color .1s ease}.sample-row.editable .sample-cell:hover{background-color:var(--cupcake-table-hover);position:relative}.sample-row.editable .sample-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:var(--cupcake-text-muted);font-size:.75em;pointer-events:none}.pool-row.editable td.clickable{cursor:pointer;transition:background-color .1s ease}.pool-row.editable td.clickable:hover{background-color:var(--cupcake-table-hover);color:var(--cupcake-text)}.pool-row.editable td.clickable:hover .bi-pencil-square{opacity:.8}.pool-row.editable td.clickable .bi-pencil-square{opacity:.4;transition:opacity .1s ease}.pool-row.editable .pool-cell{cursor:pointer;transition:background-color .1s ease}.pool-row.editable .pool-cell:hover{background-color:var(--cupcake-table-hover);position:relative}.pool-row.editable .pool-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:var(--cupcake-text-muted);font-size:.75em;pointer-events:none}@media(max-width:768px){.sample-row.editable .sample-cell,.pool-row.editable .pool-cell{transition:none}}.sortable{cursor:pointer;user-select:none;transition:background-color .2s ease}.sortable:hover{background-color:var(--cupcake-table-hover)}.sortable.sorted{background-color:var(--cupcake-bg-secondary)}.sortable.sorted .bi{color:var(--cupcake-primary)}.sortable .bi{opacity:.5;transition:all .2s ease;font-size:.75em}.sortable:hover .bi{opacity:.8}.drag-handle{cursor:grab;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active{cursor:grabbing}.cdk-drag-preview{box-sizing:border-box;border-radius:4px;box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;background:var(--bs-light);border:1px solid var(--bs-border-color)}.cdk-drag-preview .header-cell{padding:.5rem;font-size:.875rem;font-weight:500}.cdk-drag-placeholder{opacity:.5;background:var(--cupcake-table-hover)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.cdk-drop-list-dragging th:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"] }]
|
|
11012
|
+
args: [{ selector: 'ccv-metadata-table-details', standalone: true, imports: [CommonModule, RouterModule, NgbModule, DragDropModule, DualScrollDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"metadata-table-details-container\">\n <!-- Header Section with Breadcrumbs -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100 flex-wrap gap-2\">\n <div class=\"d-flex align-items-center flex-wrap gap-2\">\n <!-- Breadcrumb Navigation -->\n <nav aria-label=\"breadcrumb\" class=\"me-3\">\n <ol class=\"breadcrumb mb-0\">\n @if (!readonlyMode) {\n <li class=\"breadcrumb-item\">\n <a routerLink=\"/metadata-tables\" class=\"text-decoration-none\">\n <i class=\"bi bi-table me-1\"></i>Metadata Tables\n </a>\n </li>\n }\n <li class=\"breadcrumb-item active\" aria-current=\"page\">\n @if (table()) {\n {{ table()!.name }}\n } @else {\n Table Details\n }\n </li>\n </ol>\n </nav>\n @if (readonlyMode) {\n <span class=\"badge bg-secondary ms-2\">\n <i class=\"bi bi-eye me-1\"></i>Read-only\n </span>\n }\n\n <!-- Table Status Badge -->\n @if (table()) {\n <span class=\"badge ms-2\" [class]=\"getTableStatusClass()\">\n {{ getTableStatusBadge() }}\n </span>\n }\n </div>\n\n <!-- Action Buttons -->\n <div class=\"d-flex gap-2 flex-wrap\">\n @if (!readonlyMode) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary btn-sm\"\n (click)=\"navigateBack()\"\n title=\"Back to Tables\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back\n </button>\n }\n\n <!-- View Mode Toggle -->\n @if (hasColumns()) {\n <div class=\"btn-group\" role=\"group\">\n <button\n type=\"button\"\n class=\"btn btn-sm\"\n [class]=\"viewMode() === 'list' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"switchToListMode()\"\n title=\"Column List View\">\n <i class=\"bi bi-list-ul me-1\"></i>List\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm\"\n [class]=\"viewMode() === 'table' ? 'btn-primary' : 'btn-outline-primary'\"\n (click)=\"switchToTableMode()\"\n title=\"Table Data View\">\n <i class=\"bi bi-table me-1\"></i>Table\n </button>\n </div>\n }\n <!-- Import Dropdown -->\n @if (table() && effectiveCanEdit()) {\n <div class=\"btn-group\" ngbDropdown>\n <!-- Hidden file inputs -->\n <input\n #fileInput\n type=\"file\"\n accept=\".txt,.tsv\"\n style=\"display: none;\"\n (change)=\"importSdrf($event)\">\n <input\n #excelInput\n type=\"file\"\n accept=\".xlsx,.xls\"\n style=\"display: none;\"\n (change)=\"importExcel($event)\">\n\n <button\n type=\"button\"\n class=\"btn btn-outline-success btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\"\n title=\"Import Data\">\n <i class=\"bi bi-upload me-1\"></i>Import\n </button>\n <div class=\"dropdown-menu\" ngbDropdownMenu>\n <div class=\"dropdown-item\" (click)=\"$event.stopPropagation()\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"overrideSampleCountToggle\"\n [checked]=\"overrideSampleCount()\"\n (change)=\"overrideSampleCount.set(!overrideSampleCount())\">\n <label class=\"form-check-label small\" for=\"overrideSampleCountToggle\" title=\"Use the sample count from the imported file instead of the current table count\">Override count</label>\n </div>\n </div>\n <div class=\"dropdown-item\" (click)=\"$event.stopPropagation()\">\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"applySchemaTemplatesToggle\"\n [checked]=\"applySchemaTemplates()\"\n (change)=\"applySchemaTemplates.set(!applySchemaTemplates())\">\n <label class=\"form-check-label small\" for=\"applySchemaTemplatesToggle\" title=\"Pre-populate empty table columns from the official schema declared in the SDRF file\">Apply schema</label>\n </div>\n </div>\n <div class=\"dropdown-divider\"></div>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerFileInput(); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Import SDRF File\n </a>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"triggerExcelInput(); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Import Excel Template\n </a>\n <div class=\"dropdown-divider\"></div>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"openColumnOverrideImportModal(); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-arrow-up me-2\"></i>Import / Override Columns\n </a>\n </div>\n </div>\n }\n\n <!-- Export Dropdown -->\n @if (table() && hasColumns() && !readonlyMode) {\n <div class=\"btn-group\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-outline-info btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\"\n title=\"Export Table\">\n <i class=\"bi bi-download me-1\"></i>Export\n </button>\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('sdrf'); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-text me-2\"></i>Export as SDRF\n </a>\n <a class=\"dropdown-item\" href=\"#\" (click)=\"exportToSdrf('excel'); $event.preventDefault()\">\n <i class=\"bi bi-file-earmark-spreadsheet me-2\"></i>Export as Excel\n </a>\n </div>\n </div>\n }\n\n <!-- Share Link Dropdown -->\n @if (table() && effectiveCanEdit()) {\n <div class=\"btn-group\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-sm dropdown-toggle\"\n [class]=\"shareUrl() ? 'btn-success' : 'btn-outline-secondary'\"\n ngbDropdownToggle\n title=\"Share Table\">\n <i class=\"bi bi-share me-1\"></i>Share\n </button>\n <div class=\"dropdown-menu dropdown-menu-end p-2\" ngbDropdownMenu style=\"min-width: 320px;\">\n @if (shareUrl()) {\n <div class=\"mb-2\">\n <label class=\"form-label small fw-semibold mb-1\">Share link</label>\n <div class=\"input-group input-group-sm\">\n <input type=\"text\" class=\"form-control form-control-sm font-monospace\"\n [value]=\"shareUrl()\" readonly>\n <button class=\"btn btn-outline-secondary btn-sm\" type=\"button\"\n (click)=\"copyShareLink()\"\n title=\"Copy link\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n </div>\n </div>\n <div class=\"d-flex gap-2\">\n <button class=\"btn btn-outline-primary btn-sm flex-fill\"\n (click)=\"generateShareLink()\"\n title=\"Regenerate link (invalidates current link)\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Regenerate\n </button>\n <button class=\"btn btn-outline-danger btn-sm flex-fill\"\n (click)=\"revokeShareLink()\"\n title=\"Revoke link\">\n <i class=\"bi bi-x-circle me-1\"></i>Revoke\n </button>\n </div>\n } @else {\n <p class=\"text-muted small mb-2\">No share link. Generate one to let anyone view this table without logging in.</p>\n <button class=\"btn btn-primary btn-sm w-100\"\n (click)=\"generateShareLink()\">\n <i class=\"bi bi-link-45deg me-1\"></i>Generate Share Link\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Edit Button -->\n @if (table() && effectiveCanEdit()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"editTable()\"\n [disabled]=\"isLoading()\"\n title=\"Edit Table\">\n <i class=\"bi bi-pencil me-1\"></i>Edit\n </button>\n }\n\n <!-- Reorder Columns Button -->\n @if (table() && effectiveCanEdit() && hasColumns()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-warning btn-sm\"\n (click)=\"reorderColumns()\"\n [disabled]=\"isLoading()\"\n title=\"Reorder Columns by Schema\">\n <i class=\"bi bi-arrows-move me-1\"></i>Reorder\n </button>\n }\n\n <!-- Create Pool Button -->\n @if (table() && effectiveCanEdit() && hasColumns()) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"createPool()\"\n [disabled]=\"isLoading()\"\n title=\"Create Sample Pool\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create Pool\n </button>\n }\n\n <!-- More Actions Dropdown -->\n @if (table() && effectiveCanEdit()) {\n <div class=\"btn-group\" ngbDropdown>\n <button\n type=\"button\"\n class=\"btn btn-outline-danger btn-sm dropdown-toggle\"\n ngbDropdownToggle\n [disabled]=\"isLoading()\"\n aria-label=\"More actions\">\n <i class=\"bi bi-three-dots-vertical\" aria-hidden=\"true\"></i>\n </button>\n <div class=\"dropdown-menu dropdown-menu-end\" ngbDropdownMenu>\n <a class=\"dropdown-item text-danger\" href=\"#\" (click)=\"deleteTable(); $event.preventDefault()\">\n <i class=\"bi bi-trash me-2\"></i>Delete Table\n </a>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n </nav>\n\n <!-- Loading Spinner -->\n @if (isLoading()) {\n <div class=\"d-flex justify-content-center align-items-center\" style=\"height: 400px;\">\n <div class=\"spinner-border text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n </div>\n }\n\n <!-- Table Details Content -->\n @if (table() && !isLoading()) {\n <div class=\"table-details-content p-4\">\n\n <!-- Table Information Card -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-info-circle me-2\"></i>Table Information\n </h5>\n <small class=\"text-muted\">\n Created {{ table()!.createdAt | date:'medium' }}\n </small>\n </div>\n </div>\n <div class=\"card-body\">\n <div class=\"row\">\n <div class=\"col-md-8\">\n <h3 class=\"fw-bold mb-3\">{{ table()!.name }}</h3>\n @if (table()!.description) {\n <p class=\"text-muted mb-3\">{{ table()!.description }}</p>\n }\n <div class=\"row\">\n <div class=\"col-md-6\">\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-hash text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Sample Count:</span>\n <span class=\"badge bg-primary\">{{ table()!.sampleCount }}</span>\n </div>\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-columns-gap text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Columns:</span>\n <span class=\"badge bg-info\">{{ table()!.columns?.length || 0 }}</span>\n </div>\n </div>\n <div class=\"col-md-6\">\n @if (table()!.ownerUsername) {\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-person text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Owner:</span>\n <span>{{ table()!.ownerUsername }}</span>\n </div>\n }\n @if (table()!.labGroupName) {\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-people text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Lab Group:</span>\n <span>{{ table()!.labGroupName }}</span>\n </div>\n }\n <div class=\"d-flex align-items-center mb-2\">\n <i class=\"bi bi-tag text-muted me-2\"></i>\n <span class=\"fw-semibold me-2\">Version:</span>\n <span class=\"badge bg-light text-dark\">{{ table()!.version || 'v1.0' }}</span>\n </div>\n </div>\n </div>\n </div>\n <div class=\"col-md-4 text-end\">\n <div class=\"d-flex flex-column gap-2\">\n @if (table()!.isLocked) {\n <span class=\"badge bg-warning\">\n <i class=\"bi bi-lock me-1\"></i>Locked\n </span>\n }\n @if (table()!.isPublished) {\n <span class=\"badge bg-success\">\n <i class=\"bi bi-cloud-check me-1\"></i>Published\n </span>\n }\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Sample Pool Table -->\n @if (viewMode() === 'table' && hasPools()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-layers me-2\"></i>Sample Pool Table\n <span class=\"badge bg-warning ms-2\">{{ sortedPools().length }} pools</span>\n </h5>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"table-responsive\" appDualScroll>\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">Pool</th>\n @for (column of sortedColumns(); track column.id) {\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\n <span>{{ getShortColumnName(column) }}</span>\n @if (effectiveCanEdit()) {\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\n (click)=\"editColumnValue(column)\"\n [title]=\"'Edit default value for ' + column.name\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n </div>\n }\n </div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (poolRow of poolTableRows(); track poolRow._poolId) {\n <tr class=\"pool-row\" [class.editable]=\"effectiveCanEdit()\">\n <td class=\"text-muted fw-semibold\" \n [class.clickable]=\"effectiveCanEdit()\"\n (click)=\"effectiveCanEdit() ? editPool(poolRow._poolData) : null\"\n [title]=\"effectiveCanEdit() ? 'Click to edit pool: ' + poolRow._poolName : poolRow._poolName\">\n <i class=\"bi bi-layers me-2\"></i>{{ poolRow._poolName }}\n @if (effectiveCanEdit()) {\n <i class=\"bi bi-pencil-square ms-1 text-primary\" style=\"font-size: 0.8em;\"></i>\n }\n </td>\n @for (column of sortedColumns(); track column.id) {\n <td\n class=\"pool-cell\"\n [class.opacity-50]=\"column.hidden\"\n [attr.data-pool-id]=\"poolRow._poolId\"\n [attr.data-column-id]=\"column.id\"\n (click)=\"effectiveCanEdit() ? editPoolColumnValue(poolRow._poolData, column) : null\"\n [title]=\"effectiveCanEdit() ? 'Click to edit ' + column.name + ' for pool ' + poolRow._poolName : ''\">\n {{ poolRow['col_' + column.id] || '-' }}\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n </div>\n }\n\n <!-- Columns/Table Section -->\n <div class=\"card\">\n <div class=\"card-header\">\n <div class=\"d-flex align-items-center justify-content-between flex-wrap gap-2\">\n <h5 class=\"mb-0\">\n @if (viewMode() === 'list') {\n <i class=\"bi bi-list-columns me-2\"></i>Table Columns\n } @else {\n <i class=\"bi bi-table me-2\"></i>Table Data\n }\n </h5>\n\n <!-- Column Filter -->\n @if (hasColumns()) {\n <div class=\"d-flex align-items-center flex-wrap gap-2\">\n <div class=\"input-group input-group-sm\" style=\"width: 250px; max-width: 100%;\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-funnel text-muted\"></i>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"Filter columns...\"\n [value]=\"columnFilter()\"\n (input)=\"onColumnFilterChange($any($event.target).value)\">\n @if (columnFilter()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearColumnFilter()\"\n title=\"Clear filter\"\n aria-label=\"Clear column filter\">\n <i class=\"bi bi-x\" aria-hidden=\"true\"></i>\n </button>\n }\n </div>\n @if (columnFilter()) {\n <small class=\"text-muted ms-2\">\n {{ filteredColumnCount }}/{{ totalColumnCount }}\n </small>\n }\n </div>\n }\n \n <div class=\"d-flex gap-2 align-items-center\">\n @if (viewMode() === 'list') {\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\n @if (table() && effectiveCanEdit()) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"openAddColumnModal()\"\n [disabled]=\"isLoading()\"\n title=\"Add new column with automatic reordering\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\n </button>\n }\n @if (table() && effectiveCanEdit() && sortedColumns().length === 0) {\n <small class=\"text-muted\">Import SDRF to add columns</small>\n }\n } @else {\n <span class=\"badge bg-info\">{{ table()!.sampleCount }} samples</span>\n @if (hasPools()) {\n <span class=\"badge bg-warning\">{{ sortedPools().length }} pools</span>\n }\n <span class=\"badge bg-primary\">{{ sortedColumns().length }} columns</span>\n @if (table() && effectiveCanEdit()) {\n <button\n type=\"button\"\n class=\"btn btn-success btn-sm\"\n (click)=\"openAddColumnModal()\"\n [disabled]=\"isLoading()\"\n title=\"Add new column with automatic reordering\">\n <i class=\"bi bi-plus-circle me-1\"></i>Add Column\n </button>\n }\n }\n </div>\n </div>\n </div>\n\n @if (hasColumns()) {\n <!-- List Mode: Column Definitions -->\n @if (viewMode() === 'list') {\n <div class=\"table-responsive\" appDualScroll>\n <table class=\"table table-hover mb-0\">\n <thead>\n <tr>\n <th scope=\"col\" style=\"width: 40px;\">\n @if (effectiveCanEdit()) {\n <i class=\"bi bi-arrows-move text-muted\" title=\"Drag to reorder\"></i>\n }\n </th>\n <th scope=\"col\" style=\"width: 60px;\" \n class=\"sortable\" \n (click)=\"sortBy('position')\"\n [class.sorted]=\"isSorted('position')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>#</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('position')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('name')\"\n [class.sorted]=\"isSorted('name')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Column Name</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('name')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('type')\"\n [class.sorted]=\"isSorted('type')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Type</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('type')\"></i>\n </div>\n </th>\n <th scope=\"col\" \n class=\"sortable\" \n (click)=\"sortBy('value')\"\n [class.sorted]=\"isSorted('value')\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <span>Value</span>\n <i class=\"bi ms-1\" [class]=\"getSortIcon('value')\"></i>\n </div>\n </th>\n <th scope=\"col\">Properties</th>\n <th scope=\"col\" style=\"width: 100px;\">Actions</th>\n </tr>\n </thead>\n <tbody cdkDropList (cdkDropListDropped)=\"onColumnDrop($event)\" [cdkDropListData]=\"dragColumnsForList()\">\n @for (column of dragColumnsForList(); track column.id) {\n <tr cdkDrag [cdkDragData]=\"column\" [cdkDragDisabled]=\"!effectiveCanEdit()\">\n <td class=\"text-center\">\n @if (effectiveCanEdit()) {\n <i class=\"bi bi-grip-vertical drag-handle\" cdkDragHandle></i>\n }\n </td>\n <td class=\"text-muted fw-semibold\">\n {{ (column.columnPosition || 0) + 1 }}\n </td>\n <td>\n <div class=\"d-flex align-items-center\">\n <i class=\"me-2\" [class]=\"getColumnTypeIcon(column) + ' ' + getColumnTypeClass(column)\"></i>\n <span class=\"fw-semibold\">{{ column.name }}</span>\n </div>\n </td>\n <td>\n <span class=\"badge bg-light text-dark\">{{ column.type }}</span>\n </td>\n <td>\n <div>\n <!-- Default Value -->\n <div class=\"fw-semibold\">{{ getColumnValueDisplay(column) }}</div>\n\n <!-- Sample-specific Modifiers -->\n @if (hasModifiers(column)) {\n <div class=\"mt-1\">\n @for (modifierDisplay of getModifiersDisplay(column); track $index) {\n <small class=\"badge bg-light text-dark me-1 mb-1 d-inline-block\"\n style=\"font-size: 0.7rem;\"\n [title]=\"modifierDisplay\">\n {{ modifierDisplay }}\n </small>\n }\n </div>\n }\n </div>\n </td>\n <td>\n <div class=\"d-flex gap-1 flex-wrap\">\n @if (column.mandatory) {\n <span class=\"badge bg-danger\" title=\"Required field\">Required</span>\n }\n @if (column.readonly) {\n <span class=\"badge bg-secondary\" title=\"Read-only field\">Read-only</span>\n }\n @if (column.hidden) {\n <span class=\"badge bg-warning\" title=\"Hidden field\">Hidden</span>\n }\n @if (column.autoGenerated) {\n <span class=\"badge bg-info\" title=\"Auto-generated field\">Auto</span>\n }\n @if (column.ontologyType) {\n <span class=\"badge bg-success\" title=\"Ontology: {{ column.ontologyType }}\">\n {{ column.ontologyType }}\n </span>\n }\n </div>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\">\n <button type=\"button\" class=\"btn btn-outline-primary\"\n (click)=\"viewColumnDetails(column)\"\n title=\"View column details\"\n [attr.aria-label]=\"'View details for ' + column.name\">\n <i class=\"bi bi-info-circle\" aria-hidden=\"true\"></i>\n </button>\n @if (effectiveCanEdit()) {\n <button type=\"button\"\n class=\"btn\"\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\n (click)=\"toggleColumnHidden(column)\"\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\"\n [attr.aria-label]=\"column.hidden ? 'Show ' + column.name : 'Hide ' + column.name\">\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\" aria-hidden=\"true\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-success\"\n (click)=\"editColumnValue(column)\"\n title=\"Edit default value\"\n [attr.aria-label]=\"'Edit default value for ' + column.name\">\n <i class=\"bi bi-pencil-square\" aria-hidden=\"true\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-info\"\n (click)=\"autofillColumnValues(column)\"\n title=\"Auto-fill values\"\n [attr.aria-label]=\"'Auto-fill values for ' + column.name\">\n <i class=\"bi bi-magic\" aria-hidden=\"true\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\"\n (click)=\"editColumnSettings(column)\"\n title=\"Edit column settings\"\n [attr.aria-label]=\"'Edit settings for ' + column.name\">\n <i class=\"bi bi-gear\" aria-hidden=\"true\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-info\"\n (click)=\"openColumnHistory(column)\"\n title=\"View column history\"\n [attr.aria-label]=\"'View history for ' + column.name\">\n <i class=\"bi bi-clock-history\" aria-hidden=\"true\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- Table Data -->\n @if (viewMode() === 'table') {\n <div class=\"card-body p-0\">\n <div class=\"table-responsive\" appDualScroll>\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 80px;\" class=\"bg-secondary bg-opacity-10 text-secondary\">#</th>\n @for (column of sortedColumns(); track column.id) {\n <th scope=\"col\" [class]=\"getColumnHeaderClass(column)\" [title]=\"column.name\">\n <div class=\"header-cell d-flex justify-content-between align-items-center\">\n <span>{{ getShortColumnName(column) }}</span>\n @if (effectiveCanEdit()) {\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"btn btn-sm header-edit-btn me-1\"\n [class]=\"column.hidden ? 'btn-outline-warning' : 'btn-outline-success'\"\n (click)=\"toggleColumnHidden(column)\"\n [title]=\"column.hidden ? 'Show column' : 'Hide column'\">\n <i class=\"bi\" [class]=\"column.hidden ? 'bi-eye-slash' : 'bi-eye'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-primary header-edit-btn me-1\"\n (click)=\"editColumnValue(column)\"\n [title]=\"'Edit default value for ' + column.name\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-info header-edit-btn me-1\"\n (click)=\"autofillColumnValues(column)\"\n [title]=\"'Auto-fill values for ' + column.name\">\n <i class=\"bi bi-magic\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-danger header-edit-btn\"\n (click)=\"removeColumn(column)\"\n [title]=\"'Remove ' + column.name + ' column'\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n }\n </div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of paginatedRows(); track row._sampleIndex) {\n <tr class=\"sample-row\" [class.editable]=\"effectiveCanEdit()\">\n <td class=\"text-muted fw-semibold\">{{ row._sampleIndex }}</td>\n @for (column of sortedColumns(); track column.id) {\n <td\n class=\"sample-cell\"\n [class.opacity-50]=\"column.hidden\"\n [attr.data-sample-index]=\"row._sampleIndex\"\n [attr.data-column-id]=\"column.id\"\n (click)=\"effectiveCanEdit() ? editSampleColumnValue(column, row._sampleIndex) : null\"\n [title]=\"effectiveCanEdit() ? 'Click to edit ' + column.name + ' for sample ' + row._sampleIndex : ''\">\n {{ row['col_' + column.id] || '-' }}\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Table Mode Pagination -->\n @if (viewMode() === 'table' && totalPages() > 1) {\n <div class=\"d-flex justify-content-between align-items-center p-3 border-top\">\n <div class=\"d-flex align-items-center gap-2\">\n <span class=\"text-muted\">Rows per page:</span>\n <select class=\"form-select form-select-sm\" style=\"width: auto;\"\n [value]=\"pageSize()\"\n (change)=\"onPageSizeChange(+($any($event.target).value))\">\n <option value=\"10\">10</option>\n <option value=\"25\">25</option>\n <option value=\"50\">50</option>\n <option value=\"100\">100</option>\n </select>\n </div>\n <ngb-pagination\n [collectionSize]=\"table()!.sampleCount\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n <span class=\"text-muted\">\n Showing {{ (currentPage() - 1) * pageSize() + 1 }}-{{ Math.min(currentPage() * pageSize(), table()!.sampleCount) }}\n of {{ table()!.sampleCount }} samples\n </span>\n </div>\n }\n </div>\n }\n } @else {\n <div class=\"card-body text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-inbox display-1 mb-3\"></i>\n <h5>No Columns Found</h5>\n <p>This table doesn't have any columns defined yet.</p>\n @if (effectiveCanEdit()) {\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"openAddColumnModal()\">\n <i class=\"bi bi-plus me-1\"></i>Add First Column\n </button>\n }\n </div>\n </div>\n }\n </div>\n\n </div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.header-cell{position:relative}.header-cell .header-actions{opacity:0;transition:opacity .15s ease}thead th:hover .header-actions{opacity:1}.header-edit-btn{--bs-btn-padding-y: .125rem;--bs-btn-padding-x: .25rem;--bs-btn-font-size: .65rem;border:none!important}.header-edit-btn.btn-outline-primary:hover{background-color:var(--bs-primary);border-color:var(--bs-primary)}.header-edit-btn.btn-outline-danger:hover{background-color:var(--bs-danger);border-color:var(--bs-danger)}.sample-row.editable .sample-cell{cursor:pointer;transition:background-color .1s ease}.sample-row.editable .sample-cell:hover{background-color:var(--cupcake-table-hover);position:relative}.sample-row.editable .sample-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:var(--cupcake-text-muted);font-size:.75em;pointer-events:none}.pool-row.editable td.clickable{cursor:pointer;transition:background-color .1s ease}.pool-row.editable td.clickable:hover{background-color:var(--cupcake-table-hover);color:var(--cupcake-text)}.pool-row.editable td.clickable:hover .bi-pencil-square{opacity:.8}.pool-row.editable td.clickable .bi-pencil-square{opacity:.4;transition:opacity .1s ease}.pool-row.editable .pool-cell{cursor:pointer;transition:background-color .1s ease}.pool-row.editable .pool-cell:hover{background-color:var(--cupcake-table-hover);position:relative}.pool-row.editable .pool-cell:hover:after{content:\"\\270e\";position:absolute;right:4px;top:50%;transform:translateY(-50%);color:var(--cupcake-text-muted);font-size:.75em;pointer-events:none}@media(max-width:768px){.sample-row.editable .sample-cell,.pool-row.editable .pool-cell{transition:none}}.sortable{cursor:pointer;user-select:none;transition:background-color .2s ease}.sortable:hover{background-color:var(--cupcake-table-hover)}.sortable.sorted{background-color:var(--cupcake-bg-secondary)}.sortable.sorted .bi{color:var(--cupcake-primary)}.sortable .bi{opacity:.5;transition:all .2s ease;font-size:.75em}.sortable:hover .bi{opacity:.8}.drag-handle{cursor:grab;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active{cursor:grabbing}.cdk-drag-preview{box-sizing:border-box;border-radius:4px;box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;background:var(--bs-light);border:1px solid var(--bs-border-color)}.cdk-drag-preview .header-cell{padding:.5rem;font-size:.875rem;font-weight:500}.cdk-drag-placeholder{opacity:.5;background:var(--cupcake-table-hover)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.cdk-drop-list-dragging th:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"] }]
|
|
10984
11013
|
}], ctorParameters: () => [{ type: i1$2.ActivatedRoute }, { type: i1$2.Router }, { type: MetadataTableService }, { type: AsyncExportService }, { type: ChunkedUploadService }, { type: SamplePoolService }, { type: MetadataColumnService }, { type: i3.ToastService }, { type: AsyncTaskUIService }, { type: i2.NgbModal }, { type: OfficialColumnCacheService }, { type: i3.ConfirmDialogService }], propDecorators: { fileInput: [{
|
|
10985
11014
|
type: ViewChild,
|
|
10986
11015
|
args: ['fileInput']
|