@noatgnu/cupcake-vanilla 1.4.3 → 1.4.5

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,
@@ -723,6 +729,24 @@ class MetadataTableService extends BaseApiService {
723
729
  formData.append('normalize_ontology', String(options.normalizeOntology ?? true));
724
730
  return this.post(`${this.apiUrl}/metadata-tables/${tableId}/commit_column_override/`, formData);
725
731
  }
732
+ /**
733
+ * Generate a new share token for a metadata table (owner only)
734
+ */
735
+ generateShareToken(tableId) {
736
+ return this.post(`${this.apiUrl}/metadata-tables/${tableId}/generate_share_token/`, {});
737
+ }
738
+ /**
739
+ * Revoke the share token for a metadata table (owner only)
740
+ */
741
+ revokeShareToken(tableId) {
742
+ return this.delete(`${this.apiUrl}/metadata-tables/${tableId}/revoke_share_token/`);
743
+ }
744
+ /**
745
+ * Retrieve a shared table by its public token (no authentication required)
746
+ */
747
+ getSharedTable(token) {
748
+ return this.get(`${this.apiUrl}/shared-tables/${token}/`);
749
+ }
726
750
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: MetadataTableService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
727
751
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: MetadataTableService, providedIn: 'root' });
728
752
  }
@@ -1595,6 +1619,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
1595
1619
  }], ctorParameters: () => [] });
1596
1620
 
1597
1621
  class OntologySearchService extends BaseApiService {
1622
+ websocket = inject(WebSocketService);
1598
1623
  /**
1599
1624
  * Get ontology suggestions across all sources for SDRF validation
1600
1625
  *
@@ -1655,6 +1680,13 @@ class OntologySearchService extends BaseApiService {
1655
1680
  limit
1656
1681
  });
1657
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
+ }
1658
1690
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: OntologySearchService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1659
1691
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: OntologySearchService, providedIn: 'root' });
1660
1692
  }
@@ -4177,6 +4209,7 @@ class MetadataValueEditModal {
4177
4209
  ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
4178
4210
  isLoadingSuggestions = signal(false, /* @ts-ignore */
4179
4211
  ...(ngDevMode ? [{ debugName: "isLoadingSuggestions" }] : /* istanbul ignore next */ []));
4212
+ lastSearchTerm = '';
4180
4213
  searchType = signal('icontains', /* @ts-ignore */
4181
4214
  ...(ngDevMode ? [{ debugName: "searchType" }] : /* istanbul ignore next */ []));
4182
4215
  isLoadingFavorites = signal(false, /* @ts-ignore */
@@ -4317,7 +4350,13 @@ class MetadataValueEditModal {
4317
4350
  }
4318
4351
  // Ontology autocomplete methods
4319
4352
  searchOntology = (text$) => {
4320
- return text$.pipe(debounceTime(200), distinctUntilChanged(), tap(() => this.isLoadingSuggestions.set(true)), switchMap$1(term => {
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 => {
4321
4360
  if (term.length < 2 || !this.hasOntologyType) {
4322
4361
  this.isLoadingSuggestions.set(false);
4323
4362
  return of([]);
@@ -5049,6 +5088,7 @@ class AsyncTaskUIService {
5049
5088
  [TaskType.TRANSCRIBE_AUDIO]: 'Audio transcription',
5050
5089
  [TaskType.TRANSCRIBE_VIDEO]: 'Video transcription',
5051
5090
  [TaskType.VALIDATE_SDRF_FILE]: 'SDRF file validation',
5091
+ [TaskType.EXPORT_PROTOCOLS_IO]: 'protocols.io export',
5052
5092
  };
5053
5093
  return typeMap[taskType] || taskType;
5054
5094
  }
@@ -9784,6 +9824,15 @@ class MetadataTableDetails {
9784
9824
  isSorted(field) {
9785
9825
  return this.sortField() === field;
9786
9826
  }
9827
+ readonlyMode = false;
9828
+ set preloadedTable(value) {
9829
+ if (value) {
9830
+ this.table.set(value);
9831
+ this.isLoading.set(false);
9832
+ const naturalOrder = [...(value.columns || [])].sort((a, b) => (a.columnPosition || 0) - (b.columnPosition || 0));
9833
+ this.dragColumnsForList.set(naturalOrder);
9834
+ }
9835
+ }
9787
9836
  isLoading = signal(false, /* @ts-ignore */
9788
9837
  ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
9789
9838
  overrideSampleCount = signal(false, /* @ts-ignore */
@@ -9806,6 +9855,15 @@ class MetadataTableDetails {
9806
9855
  ...(ngDevMode ? [{ debugName: "sortField" }] : /* istanbul ignore next */ []));
9807
9856
  sortDirection = signal('asc', /* @ts-ignore */
9808
9857
  ...(ngDevMode ? [{ debugName: "sortDirection" }] : /* istanbul ignore next */ []));
9858
+ effectiveCanEdit = computed(() => !this.readonlyMode && (this.table()?.canEdit ?? false), /* @ts-ignore */
9859
+ ...(ngDevMode ? [{ debugName: "effectiveCanEdit" }] : /* istanbul ignore next */ []));
9860
+ shareUrl = computed(() => {
9861
+ const token = this.table()?.shareToken;
9862
+ if (!token)
9863
+ return null;
9864
+ return `${window.location.origin}${window.location.pathname}#/shared/${token}`;
9865
+ }, /* @ts-ignore */
9866
+ ...(ngDevMode ? [{ debugName: "shareUrl" }] : /* istanbul ignore next */ []));
9809
9867
  hasColumns = computed(() => {
9810
9868
  const table = this.table();
9811
9869
  return table?.columns && table.columns.length > 0;
@@ -9899,6 +9957,9 @@ class MetadataTableDetails {
9899
9957
  this.confirmDialog = confirmDialog;
9900
9958
  }
9901
9959
  ngOnInit() {
9960
+ if (this.table()) {
9961
+ return;
9962
+ }
9902
9963
  this.route.params.subscribe(params => {
9903
9964
  const id = Number(params['id']);
9904
9965
  const mode = params['mode'] || 'table';
@@ -9953,12 +10014,18 @@ class MetadataTableDetails {
9953
10014
  if (tableId) {
9954
10015
  this.router.navigate(['/metadata-tables', tableId, 'list']);
9955
10016
  }
10017
+ else {
10018
+ this.viewMode.set('list');
10019
+ }
9956
10020
  }
9957
10021
  switchToTableMode() {
9958
10022
  const tableId = this.tableId();
9959
10023
  if (tableId) {
9960
10024
  this.router.navigate(['/metadata-tables', tableId, 'table']);
9961
10025
  }
10026
+ else {
10027
+ this.viewMode.set('table');
10028
+ }
9962
10029
  }
9963
10030
  onPageChange(page) {
9964
10031
  this.currentPage.set(page);
@@ -9967,6 +10034,38 @@ class MetadataTableDetails {
9967
10034
  this.pageSize.set(size);
9968
10035
  this.currentPage.set(1); // Reset to first page when changing page size
9969
10036
  }
10037
+ generateShareLink() {
10038
+ const table = this.table();
10039
+ if (!table)
10040
+ return;
10041
+ this.metadataTableService.generateShareToken(table.id).subscribe({
10042
+ next: (response) => {
10043
+ this.table.set({ ...table, shareToken: response.shareToken });
10044
+ this.toastService.success('Share link generated.');
10045
+ },
10046
+ error: () => this.toastService.error('Failed to generate share link.')
10047
+ });
10048
+ }
10049
+ async revokeShareLink() {
10050
+ const table = this.table();
10051
+ if (!table)
10052
+ return;
10053
+ if (await this.confirmDialog.confirm({ message: 'Revoke the share link? Anyone with the current link will lose access.' })) {
10054
+ this.metadataTableService.revokeShareToken(table.id).subscribe({
10055
+ next: () => {
10056
+ this.table.set({ ...table, shareToken: null });
10057
+ this.toastService.success('Share link revoked.');
10058
+ },
10059
+ error: () => this.toastService.error('Failed to revoke share link.')
10060
+ });
10061
+ }
10062
+ }
10063
+ copyShareLink() {
10064
+ const url = this.shareUrl();
10065
+ if (!url)
10066
+ return;
10067
+ navigator.clipboard.writeText(url).then(() => this.toastService.success('Link copied to clipboard.'), () => this.toastService.error('Could not copy to clipboard.'));
10068
+ }
9970
10069
  editTable() {
9971
10070
  const table = this.table();
9972
10071
  if (!table || !table.canEdit)
@@ -10905,17 +11004,21 @@ class MetadataTableDetails {
10905
11004
  });
10906
11005
  }
10907
11006
  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 });
10908
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: MetadataTableDetails, isStandalone: true, selector: "ccv-metadata-table-details", viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }, { propertyName: "excelInput", first: true, predicate: ["excelInput"], descendants: true }], ngImport: i0, template: "<div class=\"metadata-table-details-container\">\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 <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 <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\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 <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 <!-- 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() && table()!.canEdit) {\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()) {\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 <!-- Edit Button -->\r\n @if (table() && table()!.canEdit) {\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() && table()!.canEdit && 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() && table()!.canEdit && 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() && table()!.canEdit) {\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 (table()!.canEdit) {\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]=\"table()!.canEdit\">\r\n <td class=\"text-muted fw-semibold\" \r\n [class.clickable]=\"table()!.canEdit\"\r\n (click)=\"table()!.canEdit ? editPool(poolRow._poolData) : null\"\r\n [title]=\"table()!.canEdit ? 'Click to edit pool: ' + poolRow._poolName : poolRow._poolName\">\r\n <i class=\"bi bi-layers me-2\"></i>{{ poolRow._poolName }}\r\n @if (table()!.canEdit) {\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)=\"table()!.canEdit ? editPoolColumnValue(poolRow._poolData, column) : null\"\r\n [title]=\"table()!.canEdit ? '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() && table()!.canEdit) {\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() && table()!.canEdit && 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() && table()!.canEdit) {\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 (table()!.canEdit) {\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]=\"!table()!.canEdit\">\r\n <td class=\"text-center\">\r\n @if (table()!.canEdit) {\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 (table()!.canEdit) {\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 (table()!.canEdit) {\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]=\"table()!.canEdit\">\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)=\"table()!.canEdit ? editSampleColumnValue(column, row._sampleIndex) : null\"\r\n [title]=\"table()!.canEdit ? '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 (table()!.canEdit) {\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 });
11007
+ 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 });
10909
11008
  }
10910
11009
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: MetadataTableDetails, decorators: [{
10911
11010
  type: Component,
10912
- 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 <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 <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\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 <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 <!-- 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() && table()!.canEdit) {\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()) {\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 <!-- Edit Button -->\r\n @if (table() && table()!.canEdit) {\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() && table()!.canEdit && 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() && table()!.canEdit && 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() && table()!.canEdit) {\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 (table()!.canEdit) {\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]=\"table()!.canEdit\">\r\n <td class=\"text-muted fw-semibold\" \r\n [class.clickable]=\"table()!.canEdit\"\r\n (click)=\"table()!.canEdit ? editPool(poolRow._poolData) : null\"\r\n [title]=\"table()!.canEdit ? 'Click to edit pool: ' + poolRow._poolName : poolRow._poolName\">\r\n <i class=\"bi bi-layers me-2\"></i>{{ poolRow._poolName }}\r\n @if (table()!.canEdit) {\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)=\"table()!.canEdit ? editPoolColumnValue(poolRow._poolData, column) : null\"\r\n [title]=\"table()!.canEdit ? '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() && table()!.canEdit) {\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() && table()!.canEdit && 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() && table()!.canEdit) {\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 (table()!.canEdit) {\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]=\"!table()!.canEdit\">\r\n <td class=\"text-center\">\r\n @if (table()!.canEdit) {\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 (table()!.canEdit) {\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 (table()!.canEdit) {\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]=\"table()!.canEdit\">\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)=\"table()!.canEdit ? editSampleColumnValue(column, row._sampleIndex) : null\"\r\n [title]=\"table()!.canEdit ? '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 (table()!.canEdit) {\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"] }]
11011
+ 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"] }]
10913
11012
  }], 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: [{
10914
11013
  type: ViewChild,
10915
11014
  args: ['fileInput']
10916
11015
  }], excelInput: [{
10917
11016
  type: ViewChild,
10918
11017
  args: ['excelInput']
11018
+ }], readonlyMode: [{
11019
+ type: Input
11020
+ }], preloadedTable: [{
11021
+ type: Input
10919
11022
  }] } });
10920
11023
 
10921
11024
  class BasicAutofillComponent {