@truenas/ui-components 0.3.25 → 0.4.0
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.
|
@@ -9,7 +9,7 @@ import * as i1 from '@angular/cdk/a11y';
|
|
|
9
9
|
import { A11yModule, FocusMonitor } from '@angular/cdk/a11y';
|
|
10
10
|
import * as i1$2 from '@angular/common';
|
|
11
11
|
import { DOCUMENT, NgTemplateOutlet, CommonModule, AsyncPipe, isPlatformBrowser } from '@angular/common';
|
|
12
|
-
import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiOpenInNew, mdiHelpCircle, mdiDotsVertical, mdiClose
|
|
12
|
+
import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiOpenInNew, mdiHelpCircle, mdiDotsVertical, mdiClose } from '@mdi/js';
|
|
13
13
|
import * as i1$1 from '@angular/platform-browser';
|
|
14
14
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
15
15
|
import { HttpClient } from '@angular/common/http';
|
|
@@ -6586,7 +6586,7 @@ class TnCheckboxComponent {
|
|
|
6586
6586
|
useExisting: forwardRef(() => TnCheckboxComponent),
|
|
6587
6587
|
multi: true
|
|
6588
6588
|
}
|
|
6589
|
-
], queries: [{ propertyName: "labelContent", predicate: TnCheckboxLabelDirective, isSignal: true }], viewQueries: [{ propertyName: "checkboxEl", first: true, predicate: ["checkboxEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-checkbox__label\" tnTestIdType=\"checkbox\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n <input\n #checkboxEl\n type=\"checkbox\"\n class=\"tn-checkbox__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [indeterminate]=\"indeterminate()\"\n [attr.aria-label]=\"hasProjectedLabel() ? (label() | tnLabelText) : null\"\n [attr.aria-describedby]=\"error() ? id + '-error' : null\"\n [attr.aria-invalid]=\"error() ? 'true' : null\"\n (change)=\"onCheckboxChange($event)\"\n />\n <span class=\"tn-checkbox__checkmark\"></span>\n @if (!hideLabel()) {\n <span class=\"tn-checkbox__text\">\n @if (hasProjectedLabel()) {\n <ng-content select=\"[tnCheckboxLabel]\" />\n } @else {\n <span [innerHTML]=\"label() | tnLabelMarkup\"></span>\n }\n </span>\n }\n </label>\n\n @if (error()) {\n <div\n class=\"tn-checkbox__error\"\n role=\"alert\"\n aria-live=\"polite\"\n [id]=\"id + '-error'\"\n >\n {{ error() }}\n </div>\n }\n</div>", styles: [".tn-checkbox{display:flex;flex-direction:column;gap:4px}.tn-checkbox__label{display:flex;align-items:center;gap:8px;cursor:pointer;-webkit-user-select:none;user-select:none;font-family:var(--tn-font-family-body, \"Inter\", sans-serif);font-size:14px;line-height:1.5;color:var(--tn-fg1, #333)}.tn-checkbox__label:hover:not(.tn-checkbox--disabled) .tn-checkbox__checkmark{border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input{position:absolute;opacity:0;cursor:pointer;height:0;width:0}.tn-checkbox__input:checked~.tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:checked~.tn-checkbox__checkmark:after{display:block}.tn-checkbox__input:indeterminate~.tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:indeterminate~.tn-checkbox__checkmark:after{display:block;content:\"\";left:4px;top:8px;width:8px;height:2px;background:var(--tn-primary-txt, #fff);border-radius:1px;transform:none}.tn-checkbox__input:focus~.tn-checkbox__checkmark{outline:2px solid var(--tn-primary, #0095d5);outline-offset:2px}.tn-checkbox__input:disabled~.tn-checkbox__checkmark{background-color:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #e0e0e0);cursor:not-allowed}.tn-checkbox__input:disabled:checked~.tn-checkbox__checkmark{background-color:var(--tn-lines, #e0e0e0);border-color:var(--tn-lines, #e0e0e0)}.tn-checkbox__checkmark{position:relative;height:16px;width:16px;background-color:var(--tn-bg1, #fff);border:1px solid var(--tn-lines, #ccc);border-radius:2px;transition:all .2s ease;flex-shrink:0}.tn-checkbox__checkmark:after{content:\"\";position:absolute;display:none;left:5px;top:2px;width:4px;height:8px;border:solid var(--tn-primary-txt, #fff);border-width:0 2px 2px 0;transform:rotate(45deg)}.tn-checkbox__text{flex:1;min-width:0}.tn-checkbox__text ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.tn-checkbox__error{color:var(--tn-red, #dc2626);font-size:12px;font-family:var(--tn-font-family-body, \"Inter\", sans-serif);margin-top:4px;display:flex;align-items:center;gap:4px}.tn-checkbox--disabled .tn-checkbox__label{cursor:not-allowed;color:var(--tn-fg2, #666);opacity:.6}.tn-checkbox--disabled .tn-checkbox__text{color:var(--tn-fg2, #666)}.tn-checkbox--error .tn-checkbox__checkmark{border-color:var(--tn-red, #dc2626)}.tn-checkbox--indeterminate .tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:focus-visible~.tn-checkbox__checkmark{outline:2px solid var(--tn-primary, #0095d5);outline-offset:2px}@media(prefers-contrast:high){.tn-checkbox__checkmark{border-width:2px}}@media(prefers-reduced-motion:reduce){.tn-checkbox__checkmark{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }, { kind: "pipe", type: LabelTextPipe, name: "tnLabelText" }] });
|
|
6589
|
+
], queries: [{ propertyName: "labelContent", predicate: TnCheckboxLabelDirective, isSignal: true }], viewQueries: [{ propertyName: "checkboxEl", first: true, predicate: ["checkboxEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-checkbox__label\" tnTestIdType=\"checkbox\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n <input\n #checkboxEl\n type=\"checkbox\"\n class=\"tn-checkbox__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [indeterminate]=\"indeterminate()\"\n [attr.aria-label]=\"hasProjectedLabel() || hideLabel() ? (label() | tnLabelText) : null\"\n [attr.aria-describedby]=\"error() ? id + '-error' : null\"\n [attr.aria-invalid]=\"error() ? 'true' : null\"\n (change)=\"onCheckboxChange($event)\"\n />\n <span class=\"tn-checkbox__checkmark\"></span>\n @if (!hideLabel()) {\n <span class=\"tn-checkbox__text\">\n @if (hasProjectedLabel()) {\n <ng-content select=\"[tnCheckboxLabel]\" />\n } @else {\n <span [innerHTML]=\"label() | tnLabelMarkup\"></span>\n }\n </span>\n }\n </label>\n\n @if (error()) {\n <div\n class=\"tn-checkbox__error\"\n role=\"alert\"\n aria-live=\"polite\"\n [id]=\"id + '-error'\"\n >\n {{ error() }}\n </div>\n }\n</div>", styles: [".tn-checkbox{display:flex;flex-direction:column;gap:4px}.tn-checkbox__label{display:flex;align-items:center;gap:8px;cursor:pointer;-webkit-user-select:none;user-select:none;font-family:var(--tn-font-family-body, \"Inter\", sans-serif);font-size:14px;line-height:1.5;color:var(--tn-fg1, #333)}.tn-checkbox__label:hover:not(.tn-checkbox--disabled) .tn-checkbox__checkmark{border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input{position:absolute;opacity:0;cursor:pointer;height:0;width:0}.tn-checkbox__input:checked~.tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:checked~.tn-checkbox__checkmark:after{display:block}.tn-checkbox__input:indeterminate~.tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:indeterminate~.tn-checkbox__checkmark:after{display:block;content:\"\";left:4px;top:8px;width:8px;height:2px;background:var(--tn-primary-txt, #fff);border-radius:1px;transform:none}.tn-checkbox__input:focus~.tn-checkbox__checkmark{outline:2px solid var(--tn-primary, #0095d5);outline-offset:2px}.tn-checkbox__input:disabled~.tn-checkbox__checkmark{background-color:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #e0e0e0);cursor:not-allowed}.tn-checkbox__input:disabled:checked~.tn-checkbox__checkmark{background-color:var(--tn-lines, #e0e0e0);border-color:var(--tn-lines, #e0e0e0)}.tn-checkbox__checkmark{position:relative;height:16px;width:16px;background-color:var(--tn-bg1, #fff);border:1px solid var(--tn-lines, #ccc);border-radius:2px;transition:all .2s ease;flex-shrink:0}.tn-checkbox__checkmark:after{content:\"\";position:absolute;display:none;left:5px;top:2px;width:4px;height:8px;border:solid var(--tn-primary-txt, #fff);border-width:0 2px 2px 0;transform:rotate(45deg)}.tn-checkbox__text{flex:1;min-width:0}.tn-checkbox__text ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.tn-checkbox__error{color:var(--tn-red, #dc2626);font-size:12px;font-family:var(--tn-font-family-body, \"Inter\", sans-serif);margin-top:4px;display:flex;align-items:center;gap:4px}.tn-checkbox--disabled .tn-checkbox__label{cursor:not-allowed;color:var(--tn-fg2, #666);opacity:.6}.tn-checkbox--disabled .tn-checkbox__text{color:var(--tn-fg2, #666)}.tn-checkbox--error .tn-checkbox__checkmark{border-color:var(--tn-red, #dc2626)}.tn-checkbox--indeterminate .tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:focus-visible~.tn-checkbox__checkmark{outline:2px solid var(--tn-primary, #0095d5);outline-offset:2px}@media(prefers-contrast:high){.tn-checkbox__checkmark{border-width:2px}}@media(prefers-reduced-motion:reduce){.tn-checkbox__checkmark{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }, { kind: "pipe", type: LabelTextPipe, name: "tnLabelText" }] });
|
|
6590
6590
|
}
|
|
6591
6591
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCheckboxComponent, decorators: [{
|
|
6592
6592
|
type: Component,
|
|
@@ -6596,7 +6596,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
6596
6596
|
useExisting: forwardRef(() => TnCheckboxComponent),
|
|
6597
6597
|
multi: true
|
|
6598
6598
|
}
|
|
6599
|
-
], template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-checkbox__label\" tnTestIdType=\"checkbox\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n <input\n #checkboxEl\n type=\"checkbox\"\n class=\"tn-checkbox__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [indeterminate]=\"indeterminate()\"\n [attr.aria-label]=\"hasProjectedLabel() ? (label() | tnLabelText) : null\"\n [attr.aria-describedby]=\"error() ? id + '-error' : null\"\n [attr.aria-invalid]=\"error() ? 'true' : null\"\n (change)=\"onCheckboxChange($event)\"\n />\n <span class=\"tn-checkbox__checkmark\"></span>\n @if (!hideLabel()) {\n <span class=\"tn-checkbox__text\">\n @if (hasProjectedLabel()) {\n <ng-content select=\"[tnCheckboxLabel]\" />\n } @else {\n <span [innerHTML]=\"label() | tnLabelMarkup\"></span>\n }\n </span>\n }\n </label>\n\n @if (error()) {\n <div\n class=\"tn-checkbox__error\"\n role=\"alert\"\n aria-live=\"polite\"\n [id]=\"id + '-error'\"\n >\n {{ error() }}\n </div>\n }\n</div>", styles: [".tn-checkbox{display:flex;flex-direction:column;gap:4px}.tn-checkbox__label{display:flex;align-items:center;gap:8px;cursor:pointer;-webkit-user-select:none;user-select:none;font-family:var(--tn-font-family-body, \"Inter\", sans-serif);font-size:14px;line-height:1.5;color:var(--tn-fg1, #333)}.tn-checkbox__label:hover:not(.tn-checkbox--disabled) .tn-checkbox__checkmark{border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input{position:absolute;opacity:0;cursor:pointer;height:0;width:0}.tn-checkbox__input:checked~.tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:checked~.tn-checkbox__checkmark:after{display:block}.tn-checkbox__input:indeterminate~.tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:indeterminate~.tn-checkbox__checkmark:after{display:block;content:\"\";left:4px;top:8px;width:8px;height:2px;background:var(--tn-primary-txt, #fff);border-radius:1px;transform:none}.tn-checkbox__input:focus~.tn-checkbox__checkmark{outline:2px solid var(--tn-primary, #0095d5);outline-offset:2px}.tn-checkbox__input:disabled~.tn-checkbox__checkmark{background-color:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #e0e0e0);cursor:not-allowed}.tn-checkbox__input:disabled:checked~.tn-checkbox__checkmark{background-color:var(--tn-lines, #e0e0e0);border-color:var(--tn-lines, #e0e0e0)}.tn-checkbox__checkmark{position:relative;height:16px;width:16px;background-color:var(--tn-bg1, #fff);border:1px solid var(--tn-lines, #ccc);border-radius:2px;transition:all .2s ease;flex-shrink:0}.tn-checkbox__checkmark:after{content:\"\";position:absolute;display:none;left:5px;top:2px;width:4px;height:8px;border:solid var(--tn-primary-txt, #fff);border-width:0 2px 2px 0;transform:rotate(45deg)}.tn-checkbox__text{flex:1;min-width:0}.tn-checkbox__text ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.tn-checkbox__error{color:var(--tn-red, #dc2626);font-size:12px;font-family:var(--tn-font-family-body, \"Inter\", sans-serif);margin-top:4px;display:flex;align-items:center;gap:4px}.tn-checkbox--disabled .tn-checkbox__label{cursor:not-allowed;color:var(--tn-fg2, #666);opacity:.6}.tn-checkbox--disabled .tn-checkbox__text{color:var(--tn-fg2, #666)}.tn-checkbox--error .tn-checkbox__checkmark{border-color:var(--tn-red, #dc2626)}.tn-checkbox--indeterminate .tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:focus-visible~.tn-checkbox__checkmark{outline:2px solid var(--tn-primary, #0095d5);outline-offset:2px}@media(prefers-contrast:high){.tn-checkbox__checkmark{border-width:2px}}@media(prefers-reduced-motion:reduce){.tn-checkbox__checkmark{transition:none}}\n"] }]
|
|
6599
|
+
], template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-checkbox__label\" tnTestIdType=\"checkbox\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n <input\n #checkboxEl\n type=\"checkbox\"\n class=\"tn-checkbox__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [indeterminate]=\"indeterminate()\"\n [attr.aria-label]=\"hasProjectedLabel() || hideLabel() ? (label() | tnLabelText) : null\"\n [attr.aria-describedby]=\"error() ? id + '-error' : null\"\n [attr.aria-invalid]=\"error() ? 'true' : null\"\n (change)=\"onCheckboxChange($event)\"\n />\n <span class=\"tn-checkbox__checkmark\"></span>\n @if (!hideLabel()) {\n <span class=\"tn-checkbox__text\">\n @if (hasProjectedLabel()) {\n <ng-content select=\"[tnCheckboxLabel]\" />\n } @else {\n <span [innerHTML]=\"label() | tnLabelMarkup\"></span>\n }\n </span>\n }\n </label>\n\n @if (error()) {\n <div\n class=\"tn-checkbox__error\"\n role=\"alert\"\n aria-live=\"polite\"\n [id]=\"id + '-error'\"\n >\n {{ error() }}\n </div>\n }\n</div>", styles: [".tn-checkbox{display:flex;flex-direction:column;gap:4px}.tn-checkbox__label{display:flex;align-items:center;gap:8px;cursor:pointer;-webkit-user-select:none;user-select:none;font-family:var(--tn-font-family-body, \"Inter\", sans-serif);font-size:14px;line-height:1.5;color:var(--tn-fg1, #333)}.tn-checkbox__label:hover:not(.tn-checkbox--disabled) .tn-checkbox__checkmark{border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input{position:absolute;opacity:0;cursor:pointer;height:0;width:0}.tn-checkbox__input:checked~.tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:checked~.tn-checkbox__checkmark:after{display:block}.tn-checkbox__input:indeterminate~.tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:indeterminate~.tn-checkbox__checkmark:after{display:block;content:\"\";left:4px;top:8px;width:8px;height:2px;background:var(--tn-primary-txt, #fff);border-radius:1px;transform:none}.tn-checkbox__input:focus~.tn-checkbox__checkmark{outline:2px solid var(--tn-primary, #0095d5);outline-offset:2px}.tn-checkbox__input:disabled~.tn-checkbox__checkmark{background-color:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #e0e0e0);cursor:not-allowed}.tn-checkbox__input:disabled:checked~.tn-checkbox__checkmark{background-color:var(--tn-lines, #e0e0e0);border-color:var(--tn-lines, #e0e0e0)}.tn-checkbox__checkmark{position:relative;height:16px;width:16px;background-color:var(--tn-bg1, #fff);border:1px solid var(--tn-lines, #ccc);border-radius:2px;transition:all .2s ease;flex-shrink:0}.tn-checkbox__checkmark:after{content:\"\";position:absolute;display:none;left:5px;top:2px;width:4px;height:8px;border:solid var(--tn-primary-txt, #fff);border-width:0 2px 2px 0;transform:rotate(45deg)}.tn-checkbox__text{flex:1;min-width:0}.tn-checkbox__text ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.tn-checkbox__error{color:var(--tn-red, #dc2626);font-size:12px;font-family:var(--tn-font-family-body, \"Inter\", sans-serif);margin-top:4px;display:flex;align-items:center;gap:4px}.tn-checkbox--disabled .tn-checkbox__label{cursor:not-allowed;color:var(--tn-fg2, #666);opacity:.6}.tn-checkbox--disabled .tn-checkbox__text{color:var(--tn-fg2, #666)}.tn-checkbox--error .tn-checkbox__checkmark{border-color:var(--tn-red, #dc2626)}.tn-checkbox--indeterminate .tn-checkbox__checkmark{background-color:var(--tn-primary, #0095d5);border-color:var(--tn-primary, #0095d5)}.tn-checkbox__input:focus-visible~.tn-checkbox__checkmark{outline:2px solid var(--tn-primary, #0095d5);outline-offset:2px}@media(prefers-contrast:high){.tn-checkbox__checkmark{border-width:2px}}@media(prefers-reduced-motion:reduce){.tn-checkbox__checkmark{transition:none}}\n"] }]
|
|
6600
6600
|
}], propDecorators: { checkboxEl: [{ type: i0.ViewChild, args: ['checkboxEl', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hideLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], labelContent: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnCheckboxLabelDirective), { isSignal: true }] }] } });
|
|
6601
6601
|
|
|
6602
6602
|
/**
|
|
@@ -10681,6 +10681,15 @@ class TnTableComponent {
|
|
|
10681
10681
|
* the row by id in your data source before assigning here.
|
|
10682
10682
|
*/
|
|
10683
10683
|
activeRow = input(null, ...(ngDevMode ? [{ debugName: "activeRow" }] : []));
|
|
10684
|
+
/**
|
|
10685
|
+
* Optional predicate marking rows as active, in addition to `activeRow`.
|
|
10686
|
+
* Use when several rows can be active at once (e.g. multi-selection
|
|
10687
|
+
* highlighting) or when matching by key rather than by reference. Active
|
|
10688
|
+
* rows get the same `tn-table__row--active` styling and, when `clickable`,
|
|
10689
|
+
* `aria-selected`. Re-evaluated on each change detection, so it may depend
|
|
10690
|
+
* on signals — keep it cheap and pure.
|
|
10691
|
+
*/
|
|
10692
|
+
activeWhen = input(undefined, ...(ngDevMode ? [{ debugName: "activeWhen" }] : []));
|
|
10684
10693
|
/**
|
|
10685
10694
|
* Overrides the active-row background color. Accepts any CSS color value
|
|
10686
10695
|
* (`#hex`, `rgb()`, `var(--token)`). Defaults to `--tn-bg3` when null.
|
|
@@ -10710,6 +10719,13 @@ class TnTableComponent {
|
|
|
10710
10719
|
selectionChange = output();
|
|
10711
10720
|
/** Emits the row when a clickable row is activated (click or Enter/Space). */
|
|
10712
10721
|
rowClick = output();
|
|
10722
|
+
/**
|
|
10723
|
+
* Emits the row when a clickable row is double-clicked. Double-click has no
|
|
10724
|
+
* keyboard equivalent (Enter/Space emit `rowClick`), so consumers must
|
|
10725
|
+
* provide an accessible alternative for the same action — e.g. a dedicated
|
|
10726
|
+
* button inside the row, as the file picker does for entering directories.
|
|
10727
|
+
*/
|
|
10728
|
+
rowDoubleClick = output();
|
|
10713
10729
|
// --- Content queries ---
|
|
10714
10730
|
columnDefs = contentChildren(TnTableColumnDirective, ...(ngDevMode ? [{ debugName: "columnDefs" }] : []));
|
|
10715
10731
|
detailRowDef = contentChild(TnDetailRowDefDirective, ...(ngDevMode ? [{ debugName: "detailRowDef" }] : []));
|
|
@@ -10886,6 +10902,9 @@ class TnTableComponent {
|
|
|
10886
10902
|
}
|
|
10887
10903
|
// --- Active row ---
|
|
10888
10904
|
isRowActive(row) {
|
|
10905
|
+
if (this.activeWhen()?.(row)) {
|
|
10906
|
+
return true;
|
|
10907
|
+
}
|
|
10889
10908
|
const active = this.activeRow();
|
|
10890
10909
|
return active !== null && active === row;
|
|
10891
10910
|
}
|
|
@@ -10896,6 +10915,12 @@ class TnTableComponent {
|
|
|
10896
10915
|
}
|
|
10897
10916
|
this.rowClick.emit(row);
|
|
10898
10917
|
}
|
|
10918
|
+
onRowDoubleClick(row) {
|
|
10919
|
+
if (!this.clickable()) {
|
|
10920
|
+
return;
|
|
10921
|
+
}
|
|
10922
|
+
this.rowDoubleClick.emit(row);
|
|
10923
|
+
}
|
|
10899
10924
|
onRowKeydown(event, row) {
|
|
10900
10925
|
if (!this.clickable()) {
|
|
10901
10926
|
return;
|
|
@@ -10932,7 +10957,7 @@ class TnTableComponent {
|
|
|
10932
10957
|
return row[column];
|
|
10933
10958
|
}
|
|
10934
10959
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10935
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTableComponent, isStandalone: true, selector: "tn-table", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayedColumns: { classPropertyName: "displayedColumns", publicName: "displayedColumns", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, emptyIcon: { classPropertyName: "emptyIcon", publicName: "emptyIcon", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, isRowExpandable: { classPropertyName: "isRowExpandable", publicName: "isRowExpandable", isSignal: true, isRequired: false, transformFunction: null }, activeRow: { classPropertyName: "activeRow", publicName: "activeRow", isSignal: true, isRequired: false, transformFunction: null }, activeBg: { classPropertyName: "activeBg", publicName: "activeBg", isSignal: true, isRequired: false, transformFunction: null }, activeIndicator: { classPropertyName: "activeIndicator", publicName: "activeIndicator", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMessage: { classPropertyName: "loadingMessage", publicName: "loadingMessage", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick" }, host: { properties: { "class.tn-table--bordered": "bordered()", "class.tn-table--loading": "loading()", "style.--tn-table-active-bg": "activeBg()", "style.--tn-table-active-indicator": "activeIndicator()" }, classAttribute: "tn-table" }, queries: [{ propertyName: "columnDefs", predicate: TnTableColumnDirective, isSignal: true }, { propertyName: "detailRowDef", first: true, predicate: TnDetailRowDefDirective, descendants: true, isSignal: true }], hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<table class=\"tn-table__table\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? sortDirection() === 'asc' ? 'ascending' : 'descending'\n : null\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"getColumnDef(column)?.sortable() && onSortClick(column)\"\n (keydown.enter)=\"onSortClick(column)\"\n (keydown.space)=\"onSortClick(column); $event.preventDefault()\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n (click)=\"onRowClick(row)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"effectiveDisplayedColumns().length\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n</table>\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}.tn-table{display:block;width:100%;overflow-x:auto}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2);border-radius:4px;overflow:hidden}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{width:48px;padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{width:48px;padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;z-index:1;border-radius:4px}.tn-table--loading .tn-table__table{pointer-events:none;-webkit-user-select:none;user-select:none}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnEmptyComponent, selector: "tn-empty", inputs: ["title", "description", "icon", "iconLibrary", "iconSize", "actionText", "bordered", "size"], outputs: ["onAction"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnSpinnerComponent, selector: "tn-spinner", inputs: ["mode", "value", "diameter", "strokeWidth", "ariaLabel", "ariaLabelledby"] }], animations: [
|
|
10960
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTableComponent, isStandalone: true, selector: "tn-table", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayedColumns: { classPropertyName: "displayedColumns", publicName: "displayedColumns", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, emptyIcon: { classPropertyName: "emptyIcon", publicName: "emptyIcon", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, isRowExpandable: { classPropertyName: "isRowExpandable", publicName: "isRowExpandable", isSignal: true, isRequired: false, transformFunction: null }, activeRow: { classPropertyName: "activeRow", publicName: "activeRow", isSignal: true, isRequired: false, transformFunction: null }, activeWhen: { classPropertyName: "activeWhen", publicName: "activeWhen", isSignal: true, isRequired: false, transformFunction: null }, activeBg: { classPropertyName: "activeBg", publicName: "activeBg", isSignal: true, isRequired: false, transformFunction: null }, activeIndicator: { classPropertyName: "activeIndicator", publicName: "activeIndicator", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMessage: { classPropertyName: "loadingMessage", publicName: "loadingMessage", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick", rowDoubleClick: "rowDoubleClick" }, host: { properties: { "class.tn-table--bordered": "bordered()", "class.tn-table--loading": "loading()", "style.--tn-table-active-bg": "activeBg()", "style.--tn-table-active-indicator": "activeIndicator()" }, classAttribute: "tn-table" }, queries: [{ propertyName: "columnDefs", predicate: TnTableColumnDirective, isSignal: true }, { propertyName: "detailRowDef", first: true, predicate: TnDetailRowDefDirective, descendants: true, isSignal: true }], hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<table class=\"tn-table__table\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? sortDirection() === 'asc' ? 'ascending' : 'descending'\n : null\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"getColumnDef(column)?.sortable() && onSortClick(column)\"\n (keydown.enter)=\"onSortClick(column)\"\n (keydown.space)=\"onSortClick(column); $event.preventDefault()\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n (click)=\"onRowClick(row)\"\n (dblclick)=\"onRowDoubleClick(row)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"effectiveDisplayedColumns().length\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n</table>\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}.tn-table{display:block;width:100%;overflow-x:auto}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2);border-radius:4px;overflow:hidden}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{width:48px;padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{width:48px;padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;z-index:1;border-radius:4px}.tn-table--loading .tn-table__table{pointer-events:none;-webkit-user-select:none;user-select:none}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnEmptyComponent, selector: "tn-empty", inputs: ["title", "description", "icon", "iconLibrary", "iconSize", "actionText", "bordered", "size"], outputs: ["onAction"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnSpinnerComponent, selector: "tn-spinner", inputs: ["mode", "value", "diameter", "strokeWidth", "ariaLabel", "ariaLabelledby"] }], animations: [
|
|
10936
10961
|
trigger('detailExpand', [
|
|
10937
10962
|
state('collapsed,void', style({ height: '0px', minHeight: '0', overflow: 'hidden' })),
|
|
10938
10963
|
state('expanded', style({ height: '*' })),
|
|
@@ -10954,8 +10979,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
10954
10979
|
'[class.tn-table--loading]': 'loading()',
|
|
10955
10980
|
'[style.--tn-table-active-bg]': 'activeBg()',
|
|
10956
10981
|
'[style.--tn-table-active-indicator]': 'activeIndicator()',
|
|
10957
|
-
}, template: "<table class=\"tn-table__table\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? sortDirection() === 'asc' ? 'ascending' : 'descending'\n : null\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"getColumnDef(column)?.sortable() && onSortClick(column)\"\n (keydown.enter)=\"onSortClick(column)\"\n (keydown.space)=\"onSortClick(column); $event.preventDefault()\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n (click)=\"onRowClick(row)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"effectiveDisplayedColumns().length\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n</table>\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}.tn-table{display:block;width:100%;overflow-x:auto}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2);border-radius:4px;overflow:hidden}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{width:48px;padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{width:48px;padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;z-index:1;border-radius:4px}.tn-table--loading .tn-table__table{pointer-events:none;-webkit-user-select:none;user-select:none}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"] }]
|
|
10958
|
-
}], ctorParameters: () => [], propDecorators: { dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayedColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayedColumns", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], emptyIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIcon", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], isRowExpandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isRowExpandable", required: false }] }], activeRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeRow", required: false }] }], activeBg: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeBg", required: false }] }], activeIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndicator", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMessage", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], columnDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnTableColumnDirective), { isSignal: true }] }], detailRowDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnDetailRowDefDirective), { isSignal: true }] }] } });
|
|
10982
|
+
}, template: "<table class=\"tn-table__table\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? sortDirection() === 'asc' ? 'ascending' : 'descending'\n : null\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"getColumnDef(column)?.sortable() && onSortClick(column)\"\n (keydown.enter)=\"onSortClick(column)\"\n (keydown.space)=\"onSortClick(column); $event.preventDefault()\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n (click)=\"onRowClick(row)\"\n (dblclick)=\"onRowDoubleClick(row)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"effectiveDisplayedColumns().length\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n</table>\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}.tn-table{display:block;width:100%;overflow-x:auto}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2);border-radius:4px;overflow:hidden}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{width:48px;padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{width:48px;padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;z-index:1;border-radius:4px}.tn-table--loading .tn-table__table{pointer-events:none;-webkit-user-select:none;user-select:none}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"] }]
|
|
10983
|
+
}], ctorParameters: () => [], propDecorators: { dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayedColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayedColumns", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], emptyIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIcon", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], isRowExpandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isRowExpandable", required: false }] }], activeRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeRow", required: false }] }], activeWhen: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeWhen", required: false }] }], activeBg: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeBg", required: false }] }], activeIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndicator", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMessage", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDoubleClick: [{ type: i0.Output, args: ["rowDoubleClick"] }], columnDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnTableColumnDirective), { isSignal: true }] }], detailRowDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnDetailRowDefDirective), { isSignal: true }] }] } });
|
|
10959
10984
|
|
|
10960
10985
|
/**
|
|
10961
10986
|
* Harness for interacting with `tn-table` in tests.
|
|
@@ -11169,6 +11194,18 @@ class TnTableHarness extends ComponentHarness {
|
|
|
11169
11194
|
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11170
11195
|
await row.click();
|
|
11171
11196
|
}
|
|
11197
|
+
/**
|
|
11198
|
+
* Double-clicks a row (for tables with `clickable` enabled), triggering
|
|
11199
|
+
* `rowDoubleClick`. Note that a real double-click also fires two single
|
|
11200
|
+
* clicks first; this helper dispatches only the `dblclick` event.
|
|
11201
|
+
*
|
|
11202
|
+
* @param rowIndex Zero-based index of the data row.
|
|
11203
|
+
*/
|
|
11204
|
+
async doubleClickRow(rowIndex) {
|
|
11205
|
+
await this.assertRowExists(rowIndex);
|
|
11206
|
+
const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
|
|
11207
|
+
await row.dispatchEvent('dblclick');
|
|
11208
|
+
}
|
|
11172
11209
|
/**
|
|
11173
11210
|
* Sends a keyboard event to a row (Enter/Space activate clickable rows).
|
|
11174
11211
|
*
|
|
@@ -11770,7 +11807,7 @@ class TnTreeComponent extends CdkTree {
|
|
|
11770
11807
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11771
11808
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: TnTreeComponent, isStandalone: true, selector: "tn-tree", host: { attributes: { "role": "tree" }, classAttribute: "tn-tree" }, providers: [
|
|
11772
11809
|
{ provide: CdkTree, useExisting: TnTreeComponent }
|
|
11773
|
-
], exportAs: ["tnTree"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<ng-container cdkTreeNodeOutlet />", styles: ["
|
|
11810
|
+
], exportAs: ["tnTree"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<ng-container cdkTreeNodeOutlet />", styles: [".tn-tree{display:block;width:100%;background-color:var(--tn-bg1);border:1px solid var(--tn-lines);border-radius:6px;overflow:hidden}tn-tree .tn-tree-node-wrapper>.tn-tree-node,tn-tree .tn-nested-tree-node-wrapper>.tn-nested-tree-node__content{border-bottom:none;border-top:1px solid var(--tn-lines)}tn-tree>.tn-tree-node-wrapper:first-child>.tn-tree-node,tn-tree>.tn-nested-tree-node-wrapper:first-child>.tn-nested-tree-node__content{border-top:none}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2.CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
11774
11811
|
}
|
|
11775
11812
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeComponent, decorators: [{
|
|
11776
11813
|
type: Component,
|
|
@@ -11779,7 +11816,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11779
11816
|
], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], host: {
|
|
11780
11817
|
'class': 'tn-tree',
|
|
11781
11818
|
'role': 'tree'
|
|
11782
|
-
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container cdkTreeNodeOutlet />", styles: ["
|
|
11819
|
+
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container cdkTreeNodeOutlet />", styles: [".tn-tree{display:block;width:100%;background-color:var(--tn-bg1);border:1px solid var(--tn-lines);border-radius:6px;overflow:hidden}tn-tree .tn-tree-node-wrapper>.tn-tree-node,tn-tree .tn-nested-tree-node-wrapper>.tn-nested-tree-node__content{border-bottom:none;border-top:1px solid var(--tn-lines)}tn-tree>.tn-tree-node-wrapper:first-child>.tn-tree-node,tn-tree>.tn-nested-tree-node-wrapper:first-child>.tn-nested-tree-node__content{border-top:none}\n"] }]
|
|
11783
11820
|
}], ctorParameters: () => [] });
|
|
11784
11821
|
|
|
11785
11822
|
class TnTreeNodeComponent extends CdkTreeNode {
|
|
@@ -11821,7 +11858,12 @@ class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
|
|
|
11821
11858
|
* defaults to the previous hardcoded value.
|
|
11822
11859
|
*/
|
|
11823
11860
|
toggleAriaLabel = input('Toggle node', ...(ngDevMode ? [{ debugName: "toggleAriaLabel" }] : []));
|
|
11824
|
-
/**
|
|
11861
|
+
/**
|
|
11862
|
+
* Semantic test-id base applied to the built-in toggle button (`button-`
|
|
11863
|
+
* prefixed). When unset, derives from the node's own `testId` — a row with
|
|
11864
|
+
* `[testId]="['vdev', guid]"` gets `button-toggle-vdev-<guid>` — so every
|
|
11865
|
+
* identified row has an addressable expand control without repeating the id.
|
|
11866
|
+
*/
|
|
11825
11867
|
toggleTestId = input(...(ngDevMode ? [undefined, { debugName: "toggleTestId" }] : []));
|
|
11826
11868
|
/**
|
|
11827
11869
|
* Suppresses the built-in toggle button (and its alignment spacer) so the
|
|
@@ -11833,7 +11875,22 @@ class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
|
|
|
11833
11875
|
*/
|
|
11834
11876
|
hideToggle = input(false, { ...(ngDevMode ? { debugName: "hideToggle" } : {}), transform: booleanAttribute });
|
|
11835
11877
|
changeDetectorRef = inject(ChangeDetectorRef);
|
|
11878
|
+
hostTestId = inject(TnTestIdDirective, { self: true });
|
|
11836
11879
|
destroyed = false;
|
|
11880
|
+
/** Explicit `toggleTestId` wins; otherwise `toggle`-scoped from the node's `testId`. */
|
|
11881
|
+
resolvedToggleTestId = computed(() => {
|
|
11882
|
+
const explicit = this.toggleTestId();
|
|
11883
|
+
if (explicit !== undefined && explicit !== null) {
|
|
11884
|
+
return explicit;
|
|
11885
|
+
}
|
|
11886
|
+
const nodeTestId = this.hostTestId.testId();
|
|
11887
|
+
if (nodeTestId === undefined || nodeTestId === null || nodeTestId === '') {
|
|
11888
|
+
return undefined;
|
|
11889
|
+
}
|
|
11890
|
+
// Role leads (button-toggle-<node id>), mirroring tn-dialog-shell's
|
|
11891
|
+
// button-close-<base> convention for fixed chrome.
|
|
11892
|
+
return scopeTestId('toggle', ...(Array.isArray(nodeTestId) ? nodeTestId : [nodeTestId]));
|
|
11893
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedToggleTestId" }] : []));
|
|
11837
11894
|
constructor() {
|
|
11838
11895
|
super(inject((ElementRef)), inject(CdkTree, { optional: true }), inject(CDK_TREE_NODE_OUTLET_NODE, { optional: true }), inject(ChangeDetectorRef, { optional: true }));
|
|
11839
11896
|
inject(DestroyRef).onDestroy(() => this.destroyed = true);
|
|
@@ -11927,7 +11984,7 @@ class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
|
|
|
11927
11984
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnNestedTreeNodeComponent, isStandalone: true, selector: "tn-nested-tree-node", inputs: { toggleAriaLabel: { classPropertyName: "toggleAriaLabel", publicName: "toggleAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, toggleTestId: { classPropertyName: "toggleTestId", publicName: "toggleTestId", isSignal: true, isRequired: false, transformFunction: null }, hideToggle: { classPropertyName: "hideToggle", publicName: "hideToggle", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "treeitem" }, properties: { "attr.aria-level": "level + 1", "attr.aria-expanded": "isExpandable ? isExpanded : null" }, classAttribute: "tn-nested-tree-node-wrapper" }, providers: [
|
|
11928
11985
|
{ provide: CdkNestedTreeNode, useExisting: TnNestedTreeNodeComponent },
|
|
11929
11986
|
{ provide: CdkTreeNode, useExisting: TnNestedTreeNodeComponent }
|
|
11930
|
-
], exportAs: ["tnNestedTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-nested-tree-node__content\">\n @if (!hideToggle()) {\n <!-- Toggle button for expandable nodes (provided by component).\n Alt+click / Alt+Enter toggles the node together with all descendants.\n tabindex=\"-1\" keeps it out of the tab order \u2014 the tree's key manager\n drives focus at the treeitem level, matching cdkTreeNodeToggle. -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n type=\"button\"\n tabindex=\"-1\"\n tnTestIdType=\"button\"\n [tnTestId]=\"
|
|
11987
|
+
], exportAs: ["tnNestedTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-nested-tree-node__content\">\n @if (!hideToggle()) {\n <!-- Toggle button for expandable nodes (provided by component).\n Alt+click / Alt+Enter toggles the node together with all descendants.\n tabindex=\"-1\" keeps it out of the tab order \u2014 the tree's key manager\n drives focus at the treeitem level, matching cdkTreeNodeToggle. -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n type=\"button\"\n tabindex=\"-1\"\n tnTestIdType=\"button\"\n [tnTestId]=\"resolvedToggleTestId()\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n (click)=\"toggleNode($event); $event.stopPropagation()\"\n (keydown.enter)=\"toggleNode($event); $event.preventDefault()\"\n (keydown.space)=\"toggleNode($event); $event.preventDefault()\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}\n", styles: [":host{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:1rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:0 8px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11931
11988
|
}
|
|
11932
11989
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnNestedTreeNodeComponent, decorators: [{
|
|
11933
11990
|
type: Component,
|
|
@@ -11939,7 +11996,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
11939
11996
|
'[attr.aria-level]': 'level + 1',
|
|
11940
11997
|
'[attr.aria-expanded]': 'isExpandable ? isExpanded : null',
|
|
11941
11998
|
'role': 'treeitem'
|
|
11942
|
-
}, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-nested-tree-node__content\">\n @if (!hideToggle()) {\n <!-- Toggle button for expandable nodes (provided by component).\n Alt+click / Alt+Enter toggles the node together with all descendants.\n tabindex=\"-1\" keeps it out of the tab order \u2014 the tree's key manager\n drives focus at the treeitem level, matching cdkTreeNodeToggle. -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n type=\"button\"\n tabindex=\"-1\"\n tnTestIdType=\"button\"\n [tnTestId]=\"
|
|
11999
|
+
}, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-nested-tree-node__content\">\n @if (!hideToggle()) {\n <!-- Toggle button for expandable nodes (provided by component).\n Alt+click / Alt+Enter toggles the node together with all descendants.\n tabindex=\"-1\" keeps it out of the tab order \u2014 the tree's key manager\n drives focus at the treeitem level, matching cdkTreeNodeToggle. -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n type=\"button\"\n tabindex=\"-1\"\n tnTestIdType=\"button\"\n [tnTestId]=\"resolvedToggleTestId()\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n (click)=\"toggleNode($event); $event.stopPropagation()\"\n (keydown.enter)=\"toggleNode($event); $event.preventDefault()\"\n (keydown.space)=\"toggleNode($event); $event.preventDefault()\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}\n", styles: [":host{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:1rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:0 8px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"] }]
|
|
11943
12000
|
}], ctorParameters: () => [], propDecorators: { toggleAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleAriaLabel", required: false }] }], toggleTestId: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleTestId", required: false }] }], hideToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideToggle", required: false }] }] } });
|
|
11944
12001
|
|
|
11945
12002
|
class TnTreeNodeOutletDirective {
|
|
@@ -12181,6 +12238,7 @@ class TnTreeNodeHarness extends ComponentHarness {
|
|
|
12181
12238
|
return toggle;
|
|
12182
12239
|
}
|
|
12183
12240
|
}
|
|
12241
|
+
|
|
12184
12242
|
/**
|
|
12185
12243
|
* Harness for interacting with `tn-tree` in tests.
|
|
12186
12244
|
*
|
|
@@ -13399,59 +13457,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
13399
13457
|
}]
|
|
13400
13458
|
}] });
|
|
13401
13459
|
|
|
13402
|
-
class StripMntPrefixPipe {
|
|
13403
|
-
transform(path) {
|
|
13404
|
-
if (!path) {
|
|
13405
|
-
return '';
|
|
13406
|
-
}
|
|
13407
|
-
if (path.startsWith('/mnt/')) {
|
|
13408
|
-
return path.substring(4); // Remove "/mnt" prefix -> "/mnt/foo" becomes "/foo"
|
|
13409
|
-
}
|
|
13410
|
-
else if (path === '/mnt') {
|
|
13411
|
-
return '/'; // Show root as just "/"
|
|
13412
|
-
}
|
|
13413
|
-
return path;
|
|
13414
|
-
}
|
|
13415
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: StripMntPrefixPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
13416
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: StripMntPrefixPipe, isStandalone: true, name: "tnStripMntPrefix" });
|
|
13417
|
-
}
|
|
13418
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: StripMntPrefixPipe, decorators: [{
|
|
13419
|
-
type: Pipe,
|
|
13420
|
-
args: [{
|
|
13421
|
-
name: 'tnStripMntPrefix',
|
|
13422
|
-
standalone: true,
|
|
13423
|
-
}]
|
|
13424
|
-
}] });
|
|
13425
|
-
|
|
13426
|
-
class TruncatePathPipe {
|
|
13427
|
-
transform(path) {
|
|
13428
|
-
// At root /mnt, show just "/"
|
|
13429
|
-
if (!path || path === '/mnt') {
|
|
13430
|
-
return [{ name: '/', path: '/mnt' }];
|
|
13431
|
-
}
|
|
13432
|
-
// For subdirectories, show ".." (parent) and current directory
|
|
13433
|
-
const segments = [];
|
|
13434
|
-
// Calculate parent path
|
|
13435
|
-
const lastSlashIndex = path.lastIndexOf('/');
|
|
13436
|
-
const parentPath = lastSlashIndex > 0 ? path.substring(0, lastSlashIndex) : '/mnt';
|
|
13437
|
-
// Get current directory name
|
|
13438
|
-
const currentDirName = path.substring(lastSlashIndex + 1);
|
|
13439
|
-
// Add parent navigation (..) and current directory
|
|
13440
|
-
segments.push({ name: '..', path: parentPath });
|
|
13441
|
-
segments.push({ name: currentDirName, path: path });
|
|
13442
|
-
return segments;
|
|
13443
|
-
}
|
|
13444
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TruncatePathPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
13445
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: TruncatePathPipe, isStandalone: true, name: "tnTruncatePath" });
|
|
13446
|
-
}
|
|
13447
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TruncatePathPipe, decorators: [{
|
|
13448
|
-
type: Pipe,
|
|
13449
|
-
args: [{
|
|
13450
|
-
name: 'tnTruncatePath',
|
|
13451
|
-
standalone: true,
|
|
13452
|
-
}]
|
|
13453
|
-
}] });
|
|
13454
|
-
|
|
13455
13460
|
class TnBrandedSpinnerComponent {
|
|
13456
13461
|
ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
13457
13462
|
paths = [];
|
|
@@ -17415,6 +17420,102 @@ class TnStepperHarness extends ComponentHarness {
|
|
|
17415
17420
|
}
|
|
17416
17421
|
}
|
|
17417
17422
|
|
|
17423
|
+
/** Item types selectable under `mode`; null means every type is selectable. */
|
|
17424
|
+
function getSelectableTypes(mode) {
|
|
17425
|
+
if (Array.isArray(mode)) {
|
|
17426
|
+
return mode;
|
|
17427
|
+
}
|
|
17428
|
+
return mode === 'any' ? null : [mode];
|
|
17429
|
+
}
|
|
17430
|
+
const directoryLikeTypes = ['folder', 'dataset', 'mountpoint'];
|
|
17431
|
+
/**
|
|
17432
|
+
* Whether the browsed directory itself is a valid selection under `mode`,
|
|
17433
|
+
* i.e. whether any selectable type is directory-like. An empty selection
|
|
17434
|
+
* then stands for the current directory, which keeps empty directories
|
|
17435
|
+
* selectable.
|
|
17436
|
+
*/
|
|
17437
|
+
function allowsCurrentDirectorySelection(mode) {
|
|
17438
|
+
const selectableTypes = getSelectableTypes(mode);
|
|
17439
|
+
return !selectableTypes || selectableTypes.some((type) => directoryLikeTypes.includes(type));
|
|
17440
|
+
}
|
|
17441
|
+
|
|
17442
|
+
/**
|
|
17443
|
+
* Whether `path` is `root` itself or nested anywhere beneath it. A plain
|
|
17444
|
+
* prefix check is not enough: '/mntx' is not within '/mnt', and a path
|
|
17445
|
+
* containing relative segments ('/mnt/../etc') could resolve outside the
|
|
17446
|
+
* root even though it starts with it — such paths are never within the root.
|
|
17447
|
+
*/
|
|
17448
|
+
function isPathWithinRoot(path, root) {
|
|
17449
|
+
if (path.split('/').some((segment) => segment === '.' || segment === '..')) {
|
|
17450
|
+
return false;
|
|
17451
|
+
}
|
|
17452
|
+
if (root === '/') {
|
|
17453
|
+
return path.startsWith('/');
|
|
17454
|
+
}
|
|
17455
|
+
return path === root || path.startsWith(`${root}/`);
|
|
17456
|
+
}
|
|
17457
|
+
/**
|
|
17458
|
+
* Collapses trailing slashes so root comparisons are exact: '/mnt/backups/'
|
|
17459
|
+
* would otherwise never equal the '/mnt/backups' produced by navigation.
|
|
17460
|
+
* '/' stays '/'.
|
|
17461
|
+
*/
|
|
17462
|
+
function normalizeRootPath(root) {
|
|
17463
|
+
return root.replace(/\/+$/, '') || '/';
|
|
17464
|
+
}
|
|
17465
|
+
|
|
17466
|
+
/**
|
|
17467
|
+
* Maximum directory buttons shown after the root segment before the middle of
|
|
17468
|
+
* the path is collapsed into an "…" segment.
|
|
17469
|
+
*/
|
|
17470
|
+
const MAX_VISIBLE_DIRS = 4;
|
|
17471
|
+
class TruncatePathPipe {
|
|
17472
|
+
transform(path, rootPath = '/mnt') {
|
|
17473
|
+
const root = normalizeRootPath(rootPath);
|
|
17474
|
+
// The root is always the first segment. Its leading slash is rendered by
|
|
17475
|
+
// the breadcrumb as a separator, so the name omits it (e.g. "mnt"),
|
|
17476
|
+
// reading as "/ mnt / showcase"
|
|
17477
|
+
const segments = [{ name: root.replace(/^\//, ''), path: root }];
|
|
17478
|
+
if (!path || path === root) {
|
|
17479
|
+
return segments;
|
|
17480
|
+
}
|
|
17481
|
+
if (!isPathWithinRoot(path, root)) {
|
|
17482
|
+
// Path escaped the root — show just its leaf after the root segment
|
|
17483
|
+
segments.push({ name: path.substring(path.lastIndexOf('/') + 1), path });
|
|
17484
|
+
return segments;
|
|
17485
|
+
}
|
|
17486
|
+
// One clickable segment per directory between the root and the current path
|
|
17487
|
+
const relativePath = root === '/' ? path : path.substring(root.length);
|
|
17488
|
+
let segmentPath = root;
|
|
17489
|
+
for (const name of relativePath.split('/').filter(Boolean)) {
|
|
17490
|
+
segmentPath = segmentPath === '/' ? `/${name}` : `${segmentPath}/${name}`;
|
|
17491
|
+
segments.push({ name, path: segmentPath });
|
|
17492
|
+
}
|
|
17493
|
+
return this.truncateMiddle(segments);
|
|
17494
|
+
}
|
|
17495
|
+
/**
|
|
17496
|
+
* Collapses the middle of deep paths into an "…" segment that navigates to the
|
|
17497
|
+
* parent of the first visible directory, e.g. /mnt/a/b/c/d/e/f → /mnt … d e f.
|
|
17498
|
+
*/
|
|
17499
|
+
truncateMiddle(segments) {
|
|
17500
|
+
const [root, ...dirs] = segments;
|
|
17501
|
+
if (dirs.length <= MAX_VISIBLE_DIRS) {
|
|
17502
|
+
return segments;
|
|
17503
|
+
}
|
|
17504
|
+
const visibleDirs = dirs.slice(-(MAX_VISIBLE_DIRS - 1));
|
|
17505
|
+
const collapsedParent = dirs[dirs.length - MAX_VISIBLE_DIRS];
|
|
17506
|
+
return [root, { name: '…', path: collapsedParent.path }, ...visibleDirs];
|
|
17507
|
+
}
|
|
17508
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TruncatePathPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
17509
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: TruncatePathPipe, isStandalone: true, name: "tnTruncatePath" });
|
|
17510
|
+
}
|
|
17511
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TruncatePathPipe, decorators: [{
|
|
17512
|
+
type: Pipe,
|
|
17513
|
+
args: [{
|
|
17514
|
+
name: 'tnTruncatePath',
|
|
17515
|
+
standalone: true,
|
|
17516
|
+
}]
|
|
17517
|
+
}] });
|
|
17518
|
+
|
|
17418
17519
|
/**
|
|
17419
17520
|
* Auto-generated from SVG files - DO NOT EDIT MANUALLY
|
|
17420
17521
|
*
|
|
@@ -17440,254 +17541,289 @@ function registerTruenasIcons(iconRegistry) {
|
|
|
17440
17541
|
});
|
|
17441
17542
|
}
|
|
17442
17543
|
|
|
17544
|
+
/** Uniquifies the inline-creation error id across popup instances. */
|
|
17545
|
+
let nextInlineErrorId = 0;
|
|
17443
17546
|
class TnFilePickerPopupComponent {
|
|
17547
|
+
/**
|
|
17548
|
+
* What can be selected: a single mode, or an explicit list of selectable
|
|
17549
|
+
* item types, e.g. ['folder', 'dataset'].
|
|
17550
|
+
*/
|
|
17444
17551
|
mode = input('any', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
17445
17552
|
multiSelect = input(false, ...(ngDevMode ? [{ debugName: "multiSelect" }] : []));
|
|
17446
|
-
|
|
17447
|
-
|
|
17448
|
-
|
|
17553
|
+
/**
|
|
17554
|
+
* Consumer-defined creation flows rendered as buttons in the footer, next to the
|
|
17555
|
+
* Select button. Pressing one emits `createAction` with the action id and the
|
|
17556
|
+
* currently browsed path.
|
|
17557
|
+
*/
|
|
17558
|
+
createActions = input([], ...(ngDevMode ? [{ debugName: "createActions" }] : []));
|
|
17449
17559
|
currentPath = input('/mnt', ...(ngDevMode ? [{ debugName: "currentPath" }] : []));
|
|
17560
|
+
/**
|
|
17561
|
+
* Topmost path the breadcrumb can navigate to. It is always the first
|
|
17562
|
+
* breadcrumb segment, rendered with its leading slash drawn as a separator
|
|
17563
|
+
* so the path reads "/ mnt / showcase".
|
|
17564
|
+
*/
|
|
17565
|
+
rootPath = input('/mnt', ...(ngDevMode ? [{ debugName: "rootPath" }] : []));
|
|
17450
17566
|
fileItems = input([], ...(ngDevMode ? [{ debugName: "fileItems" }] : []));
|
|
17451
17567
|
selectedItems = input([], ...(ngDevMode ? [{ debugName: "selectedItems" }] : []));
|
|
17452
17568
|
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
17453
|
-
creationLoading = input(false, ...(ngDevMode ? [{ debugName: "creationLoading" }] : []));
|
|
17454
17569
|
fileExtensions = input(undefined, ...(ngDevMode ? [{ debugName: "fileExtensions" }] : []));
|
|
17455
17570
|
iconRegistry = inject(TnIconRegistryService);
|
|
17571
|
+
elementRef = inject(ElementRef);
|
|
17456
17572
|
constructor() {
|
|
17457
17573
|
// Register TrueNAS custom icons
|
|
17458
17574
|
registerTruenasIcons(this.iconRegistry);
|
|
17459
|
-
// Register MDI icons used by this component
|
|
17460
|
-
this.registerMdiIcons();
|
|
17461
17575
|
}
|
|
17462
17576
|
/**
|
|
17463
|
-
*
|
|
17464
|
-
*
|
|
17577
|
+
* Requests a focus of the inline creation input on the next view check.
|
|
17578
|
+
* Set when the row opens and when a failed attempt re-enables the input —
|
|
17579
|
+
* the two moments the input either just entered the DOM or just lost focus
|
|
17580
|
+
* to the `disabled` attribute. Cleared once the focus is scheduled, so
|
|
17581
|
+
* focus is NOT permanently forced back: after an error the user can still
|
|
17582
|
+
* click elsewhere without being trapped in the input.
|
|
17583
|
+
*/
|
|
17584
|
+
pendingCreationFocus = false;
|
|
17585
|
+
/**
|
|
17586
|
+
* Focus runs here rather than at the call sites because the input may not
|
|
17587
|
+
* be in the DOM (or may still be disabled) until a later view check, and a
|
|
17588
|
+
* re-render can replace the element after focus() was scheduled — render
|
|
17589
|
+
* scheduling differs between hosts. So the request keeps retrying on each
|
|
17590
|
+
* check and is cleared only once the input actually holds focus.
|
|
17465
17591
|
*/
|
|
17466
|
-
registerMdiIcons() {
|
|
17467
|
-
const mdiIcons = {
|
|
17468
|
-
'folder': mdiFolder,
|
|
17469
|
-
'file': mdiFile,
|
|
17470
|
-
'database': mdiDatabase,
|
|
17471
|
-
'harddisk': mdiHarddisk,
|
|
17472
|
-
'folder-network': mdiFolderNetwork,
|
|
17473
|
-
'folder-plus': mdiFolderPlus,
|
|
17474
|
-
'loading': mdiLoading,
|
|
17475
|
-
'lock': mdiLock,
|
|
17476
|
-
'folder-open': mdiFolderOpen,
|
|
17477
|
-
'alert-circle': mdiAlertCircle
|
|
17478
|
-
};
|
|
17479
|
-
// Register MDI library with resolver for file picker icons
|
|
17480
|
-
this.iconRegistry.registerLibrary({
|
|
17481
|
-
name: 'mdi',
|
|
17482
|
-
resolver: (iconName) => {
|
|
17483
|
-
const pathData = mdiIcons[iconName];
|
|
17484
|
-
if (!pathData) {
|
|
17485
|
-
return null;
|
|
17486
|
-
}
|
|
17487
|
-
return `<svg viewBox="0 0 24 24"><path fill="currentColor" d="${pathData}"/></svg>`;
|
|
17488
|
-
}
|
|
17489
|
-
});
|
|
17490
|
-
}
|
|
17491
|
-
ngOnInit() {
|
|
17492
|
-
}
|
|
17493
|
-
ngAfterViewInit() {
|
|
17494
|
-
}
|
|
17495
17592
|
ngAfterViewChecked() {
|
|
17496
|
-
|
|
17497
|
-
|
|
17498
|
-
|
|
17499
|
-
|
|
17500
|
-
|
|
17501
|
-
|
|
17502
|
-
|
|
17593
|
+
if (!this.pendingCreationFocus) {
|
|
17594
|
+
return;
|
|
17595
|
+
}
|
|
17596
|
+
const creation = this.inlineCreation();
|
|
17597
|
+
if (!creation) {
|
|
17598
|
+
this.pendingCreationFocus = false;
|
|
17599
|
+
return;
|
|
17600
|
+
}
|
|
17601
|
+
if (creation.loading) {
|
|
17602
|
+
return;
|
|
17503
17603
|
}
|
|
17604
|
+
const nameInput = this.elementRef.nativeElement
|
|
17605
|
+
.querySelector('.inline-create-input');
|
|
17606
|
+
if (!nameInput) {
|
|
17607
|
+
return;
|
|
17608
|
+
}
|
|
17609
|
+
if (document.activeElement === nameInput) {
|
|
17610
|
+
this.pendingCreationFocus = false;
|
|
17611
|
+
return;
|
|
17612
|
+
}
|
|
17613
|
+
setTimeout(() => nameInput.focus());
|
|
17504
17614
|
}
|
|
17505
17615
|
itemClick = output();
|
|
17506
17616
|
itemDoubleClick = output();
|
|
17507
17617
|
pathNavigate = output();
|
|
17508
|
-
|
|
17618
|
+
/** Emits when a `createActions` button without a `create` callback is pressed. */
|
|
17619
|
+
createAction = output();
|
|
17620
|
+
/** Emits the created path when an inline `create` flow succeeds. */
|
|
17621
|
+
created = output();
|
|
17509
17622
|
clearSelection = output();
|
|
17510
17623
|
close = output();
|
|
17511
17624
|
submit = output();
|
|
17512
17625
|
cancel = output();
|
|
17513
|
-
submitFolderName = output();
|
|
17514
|
-
cancelFolderCreation = output();
|
|
17515
17626
|
// Table configuration
|
|
17516
17627
|
displayedColumns = ['select', 'name', 'size', 'modified'];
|
|
17517
17628
|
// Computed values
|
|
17629
|
+
/**
|
|
17630
|
+
* Whether an empty selection stands for the directory being browsed: true
|
|
17631
|
+
* whenever the selectable types include a directory-like type. The footer
|
|
17632
|
+
* then names the implicit selection and the Select button stays enabled; on
|
|
17633
|
+
* `submit` with no `selectedItems`, consumers treat `currentPath` as the
|
|
17634
|
+
* selection.
|
|
17635
|
+
*/
|
|
17636
|
+
allowCurrentDirectorySelection = computed(() => allowsCurrentDirectorySelection(this.mode()), ...(ngDevMode ? [{ debugName: "allowCurrentDirectorySelection" }] : []));
|
|
17518
17637
|
filteredFileItems = computed(() => {
|
|
17519
17638
|
const items = this.fileItems();
|
|
17520
17639
|
const extensions = this.fileExtensions();
|
|
17521
|
-
const
|
|
17522
|
-
|
|
17523
|
-
let
|
|
17524
|
-
// Check if item
|
|
17525
|
-
if (
|
|
17526
|
-
|
|
17527
|
-
(mode === 'folder' && item.type === 'folder') ||
|
|
17528
|
-
(mode === 'dataset' && item.type === 'dataset') ||
|
|
17529
|
-
(mode === 'zvol' && item.type === 'zvol');
|
|
17530
|
-
shouldDisable = !matchesMode;
|
|
17640
|
+
const selectableTypes = getSelectableTypes(this.mode());
|
|
17641
|
+
const displayed = items.map(item => {
|
|
17642
|
+
let selectable = true;
|
|
17643
|
+
// Check if the item's type is selectable
|
|
17644
|
+
if (selectableTypes) {
|
|
17645
|
+
selectable = selectableTypes.includes(item.type);
|
|
17531
17646
|
}
|
|
17532
17647
|
// Check file extension filter (only applies to files)
|
|
17533
17648
|
if (extensions && extensions.length > 0 && item.type === 'file') {
|
|
17534
|
-
|
|
17535
|
-
shouldDisable = shouldDisable || !matchesExtension;
|
|
17649
|
+
selectable = selectable && extensions.some(ext => item.name.toLowerCase().endsWith(ext.toLowerCase()));
|
|
17536
17650
|
}
|
|
17537
|
-
|
|
17538
|
-
|
|
17651
|
+
return {
|
|
17652
|
+
...item,
|
|
17653
|
+
// Don't override existing disabled state from backend
|
|
17654
|
+
disabled: item.disabled || !selectable,
|
|
17655
|
+
// Dim only items with no interaction left: navigatable items can still
|
|
17656
|
+
// be entered by double-click even when they are not selectable
|
|
17657
|
+
dimmed: item.disabled || (!selectable && !this.isNavigatable(item))
|
|
17658
|
+
};
|
|
17539
17659
|
});
|
|
17660
|
+
// The inline creation row renders as the first item of the listing
|
|
17661
|
+
if (this.inlineCreation()) {
|
|
17662
|
+
displayed.unshift({
|
|
17663
|
+
path: '',
|
|
17664
|
+
name: '',
|
|
17665
|
+
type: 'folder',
|
|
17666
|
+
disabled: true,
|
|
17667
|
+
dimmed: false,
|
|
17668
|
+
inlineCreate: true
|
|
17669
|
+
});
|
|
17670
|
+
}
|
|
17671
|
+
return displayed;
|
|
17540
17672
|
}, ...(ngDevMode ? [{ debugName: "filteredFileItems" }] : []));
|
|
17541
17673
|
onItemClick(item) {
|
|
17542
|
-
if (item.
|
|
17674
|
+
if (item.inlineCreate) {
|
|
17543
17675
|
return;
|
|
17544
|
-
}
|
|
17676
|
+
}
|
|
17545
17677
|
this.itemClick.emit(item);
|
|
17546
17678
|
}
|
|
17547
17679
|
onItemDoubleClick(item) {
|
|
17548
|
-
if (item.
|
|
17680
|
+
if (item.inlineCreate) {
|
|
17549
17681
|
return;
|
|
17550
|
-
}
|
|
17682
|
+
}
|
|
17683
|
+
this.itemDoubleClick.emit(item);
|
|
17684
|
+
}
|
|
17685
|
+
onNavigateClick(event, item) {
|
|
17686
|
+
// Don't also toggle selection via the cell's click handler
|
|
17687
|
+
event.stopPropagation();
|
|
17551
17688
|
this.itemDoubleClick.emit(item);
|
|
17552
17689
|
}
|
|
17553
17690
|
navigateToPath(path) {
|
|
17554
|
-
//
|
|
17555
|
-
|
|
17556
|
-
if (hasCreatingItem) {
|
|
17557
|
-
console.warn('Cannot navigate while creating a folder');
|
|
17558
|
-
return;
|
|
17559
|
-
}
|
|
17691
|
+
// Leaving the directory abandons a pending inline creation row
|
|
17692
|
+
this.inlineCreation.set(null);
|
|
17560
17693
|
this.pathNavigate.emit(path);
|
|
17561
17694
|
}
|
|
17562
|
-
|
|
17563
|
-
|
|
17564
|
-
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
|
|
17571
|
-
onSubmit() {
|
|
17572
|
-
this.submit.emit();
|
|
17573
|
-
}
|
|
17574
|
-
onCancel() {
|
|
17575
|
-
this.cancel.emit();
|
|
17576
|
-
}
|
|
17577
|
-
onFolderNameSubmit(event, item) {
|
|
17578
|
-
const inp = event.target;
|
|
17579
|
-
const name = inp.value.trim();
|
|
17580
|
-
if (item.tempId) {
|
|
17581
|
-
// Even if empty, let parent component handle validation
|
|
17582
|
-
this.submitFolderName.emit({ name, tempId: item.tempId });
|
|
17695
|
+
onCreateAction(action) {
|
|
17696
|
+
if (action.create) {
|
|
17697
|
+
// The clicked footer button is about to become disabled while focused;
|
|
17698
|
+
// blur it now so the browser's focus fixup doesn't fight the input
|
|
17699
|
+
// focus from ngAfterViewChecked
|
|
17700
|
+
document.activeElement?.blur?.();
|
|
17701
|
+
this.inlineCreation.set({ action, name: '', loading: false });
|
|
17702
|
+
this.pendingCreationFocus = true;
|
|
17703
|
+
return;
|
|
17583
17704
|
}
|
|
17705
|
+
this.createAction.emit({ actionId: action.id, parentPath: this.currentPath() });
|
|
17584
17706
|
}
|
|
17585
|
-
|
|
17586
|
-
|
|
17587
|
-
|
|
17707
|
+
/**
|
|
17708
|
+
* Inline creation state while a `create`-capable action's row is open.
|
|
17709
|
+
* The consumer's `create` callback does the real work; the row shows its
|
|
17710
|
+
* rejection message as the inline error. The entered name lives here (not
|
|
17711
|
+
* only in the DOM) so a listing re-render can't wipe it.
|
|
17712
|
+
*/
|
|
17713
|
+
inlineCreation = signal(null, ...(ngDevMode ? [{ debugName: "inlineCreation" }] : []));
|
|
17714
|
+
/** Links the inline creation input to its error message for screen readers. */
|
|
17715
|
+
inlineErrorId = `tn-inline-create-error-${nextInlineErrorId++}`;
|
|
17716
|
+
async submitInlineCreation() {
|
|
17717
|
+
const state = this.inlineCreation();
|
|
17718
|
+
if (!state || state.loading) {
|
|
17719
|
+
return;
|
|
17588
17720
|
}
|
|
17589
|
-
|
|
17590
|
-
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
|
|
17596
|
-
|
|
17597
|
-
|
|
17721
|
+
const trimmed = state.name.trim();
|
|
17722
|
+
if (!trimmed) {
|
|
17723
|
+
// Submitting an empty name abandons the row
|
|
17724
|
+
this.inlineCreation.set(null);
|
|
17725
|
+
return;
|
|
17726
|
+
}
|
|
17727
|
+
const pending = { ...state, loading: true, error: undefined };
|
|
17728
|
+
this.inlineCreation.set(pending);
|
|
17729
|
+
try {
|
|
17730
|
+
const createdPath = await state.action.create(this.currentPath(), trimmed);
|
|
17731
|
+
// Navigating away abandoned the row while the call was in flight —
|
|
17732
|
+
// don't select a result the user already walked away from
|
|
17733
|
+
if (this.inlineCreation() !== pending) {
|
|
17734
|
+
return;
|
|
17735
|
+
}
|
|
17736
|
+
this.inlineCreation.set(null);
|
|
17737
|
+
this.created.emit(createdPath);
|
|
17738
|
+
}
|
|
17739
|
+
catch (err) {
|
|
17740
|
+
// Same abandonment check: a late rejection must not resurrect the row
|
|
17741
|
+
// (in a different directory, no less)
|
|
17742
|
+
if (this.inlineCreation() !== pending) {
|
|
17743
|
+
return;
|
|
17744
|
+
}
|
|
17745
|
+
const message = err instanceof Error ? err.message : 'Creation failed';
|
|
17746
|
+
// The row stays editable for retry; ngAfterViewChecked refocuses it
|
|
17747
|
+
// once (the disabled attribute dropped focus during the attempt)
|
|
17748
|
+
this.inlineCreation.set({ ...pending, loading: false, error: message });
|
|
17749
|
+
this.pendingCreationFocus = true;
|
|
17598
17750
|
}
|
|
17599
17751
|
}
|
|
17600
|
-
|
|
17752
|
+
onInlineCreationKeyDown(event) {
|
|
17753
|
+
// Keep keystrokes away from the table row, whose Enter/Space activation
|
|
17754
|
+
// would swallow typed spaces
|
|
17755
|
+
event.stopPropagation();
|
|
17601
17756
|
if (event.key === 'Enter') {
|
|
17602
17757
|
event.preventDefault();
|
|
17603
|
-
this.
|
|
17758
|
+
void this.submitInlineCreation();
|
|
17604
17759
|
}
|
|
17605
17760
|
else if (event.key === 'Escape') {
|
|
17606
17761
|
event.preventDefault();
|
|
17607
|
-
this.
|
|
17762
|
+
this.inlineCreation.set(null);
|
|
17763
|
+
}
|
|
17764
|
+
}
|
|
17765
|
+
/**
|
|
17766
|
+
* Mirrors the typed name into the creation state and clears a shown error
|
|
17767
|
+
* as soon as the name is edited, re-arming blur submit.
|
|
17768
|
+
*/
|
|
17769
|
+
onInlineCreationInput(event) {
|
|
17770
|
+
const state = this.inlineCreation();
|
|
17771
|
+
if (!state) {
|
|
17772
|
+
return;
|
|
17608
17773
|
}
|
|
17774
|
+
const name = event.target.value;
|
|
17775
|
+
this.inlineCreation.set({ ...state, name, error: undefined });
|
|
17609
17776
|
}
|
|
17610
|
-
|
|
17611
|
-
|
|
17777
|
+
onInlineCreationBlur() {
|
|
17778
|
+
// A failed attempt already showed its error — repeating the create call
|
|
17779
|
+
// on every focus loss would loop (submit → error → refocus → blur → …).
|
|
17780
|
+
// The user retries with Enter or by editing the name, or abandons with
|
|
17781
|
+
// Escape / navigation.
|
|
17782
|
+
if (this.inlineCreation()?.error) {
|
|
17783
|
+
return;
|
|
17784
|
+
}
|
|
17785
|
+
// Auto-submit on blur, matching inline-rename conventions
|
|
17786
|
+
void this.submitInlineCreation();
|
|
17787
|
+
}
|
|
17788
|
+
onClearSelection() {
|
|
17789
|
+
this.clearSelection.emit();
|
|
17790
|
+
}
|
|
17791
|
+
onSubmit() {
|
|
17792
|
+
this.submit.emit();
|
|
17793
|
+
}
|
|
17794
|
+
onCancel() {
|
|
17795
|
+
this.cancel.emit();
|
|
17612
17796
|
}
|
|
17613
17797
|
// Utility methods
|
|
17614
17798
|
isNavigatable(item) {
|
|
17615
17799
|
return ['folder', 'dataset', 'mountpoint'].includes(item.type);
|
|
17616
17800
|
}
|
|
17801
|
+
/**
|
|
17802
|
+
* Fully-qualified sprite icon name for an item. `tnIconMarker`/`libIconMarker`
|
|
17803
|
+
* calls double as build-time markers that pull these icons into the sprite.
|
|
17804
|
+
*/
|
|
17617
17805
|
getItemIcon(item) {
|
|
17618
17806
|
if (item.icon) {
|
|
17619
17807
|
return item.icon;
|
|
17620
17808
|
}
|
|
17621
17809
|
switch (item.type) {
|
|
17622
|
-
case 'folder': return 'folder';
|
|
17623
|
-
case 'dataset': return 'tn-dataset';
|
|
17624
|
-
case 'zvol': return 'database';
|
|
17625
|
-
case 'mountpoint': return '
|
|
17810
|
+
case 'folder': return tnIconMarker('folder', 'mdi');
|
|
17811
|
+
case 'dataset': return libIconMarker('tn-dataset');
|
|
17812
|
+
case 'zvol': return tnIconMarker('database', 'mdi');
|
|
17813
|
+
case 'mountpoint': return tnIconMarker('server-network', 'mdi');
|
|
17626
17814
|
case 'file': return this.getFileIcon(item.name);
|
|
17627
|
-
default: return 'file';
|
|
17815
|
+
default: return tnIconMarker('file', 'mdi');
|
|
17628
17816
|
}
|
|
17629
17817
|
}
|
|
17630
17818
|
getFileIcon(filename) {
|
|
17631
17819
|
const ext = filename.split('.').pop()?.toLowerCase();
|
|
17632
17820
|
switch (ext) {
|
|
17633
|
-
case 'txt':
|
|
17634
|
-
case 'log':
|
|
17635
|
-
case 'md':
|
|
17636
|
-
case 'readme': return 'file';
|
|
17637
|
-
case 'pdf': return 'file';
|
|
17638
|
-
case 'jpg':
|
|
17639
|
-
case 'jpeg':
|
|
17640
|
-
case 'png':
|
|
17641
|
-
case 'gif':
|
|
17642
|
-
case 'svg':
|
|
17643
|
-
case 'webp': return 'file';
|
|
17644
|
-
case 'mp4':
|
|
17645
|
-
case 'avi':
|
|
17646
|
-
case 'mov':
|
|
17647
|
-
case 'mkv':
|
|
17648
|
-
case 'webm': return 'file';
|
|
17649
|
-
case 'mp3':
|
|
17650
|
-
case 'wav':
|
|
17651
|
-
case 'flac':
|
|
17652
|
-
case 'ogg':
|
|
17653
|
-
case 'aac': return 'file';
|
|
17654
|
-
case 'zip':
|
|
17655
|
-
case 'tar':
|
|
17656
|
-
case 'gz':
|
|
17657
|
-
case 'rar':
|
|
17658
|
-
case '7z': return 'file';
|
|
17659
|
-
case 'js':
|
|
17660
|
-
case 'ts':
|
|
17661
|
-
case 'html':
|
|
17662
|
-
case 'css':
|
|
17663
|
-
case 'py':
|
|
17664
|
-
case 'java':
|
|
17665
|
-
case 'cpp':
|
|
17666
|
-
case 'c': return 'file';
|
|
17667
|
-
case 'json':
|
|
17668
|
-
case 'xml':
|
|
17669
|
-
case 'yaml':
|
|
17670
|
-
case 'yml':
|
|
17671
|
-
case 'toml': return 'file';
|
|
17672
17821
|
case 'iso':
|
|
17673
17822
|
case 'img':
|
|
17674
|
-
case 'dmg': return 'harddisk';
|
|
17675
|
-
default: return 'file';
|
|
17823
|
+
case 'dmg': return tnIconMarker('harddisk', 'mdi');
|
|
17824
|
+
default: return tnIconMarker('file', 'mdi');
|
|
17676
17825
|
}
|
|
17677
17826
|
}
|
|
17678
|
-
/**
|
|
17679
|
-
* Get the library type for the icon
|
|
17680
|
-
* @param item FileSystemItem
|
|
17681
|
-
* @returns 'custom' for TrueNAS custom icons, 'mdi' for Material Design Icons
|
|
17682
|
-
*/
|
|
17683
|
-
getItemIconLibrary(item) {
|
|
17684
|
-
// Use custom library for dataset icon
|
|
17685
|
-
if (item.type === 'dataset') {
|
|
17686
|
-
return 'custom';
|
|
17687
|
-
}
|
|
17688
|
-
// Use mdi for all other icons
|
|
17689
|
-
return 'mdi';
|
|
17690
|
-
}
|
|
17691
17827
|
getZfsBadge(item) {
|
|
17692
17828
|
switch (item.type) {
|
|
17693
17829
|
case 'dataset': return 'DS';
|
|
@@ -17702,45 +17838,11 @@ class TnFilePickerPopupComponent {
|
|
|
17702
17838
|
isSelected(item) {
|
|
17703
17839
|
return this.selectedItems().includes(item.path);
|
|
17704
17840
|
}
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
if (row.disabled) {
|
|
17711
|
-
classes.push('disabled');
|
|
17712
|
-
}
|
|
17713
|
-
return classes;
|
|
17714
|
-
};
|
|
17715
|
-
getFileInfo(item) {
|
|
17716
|
-
const parts = [];
|
|
17717
|
-
if (item.size !== undefined) {
|
|
17718
|
-
parts.push(this.formatFileSize(item.size));
|
|
17719
|
-
}
|
|
17720
|
-
if (item.modified) {
|
|
17721
|
-
parts.push(item.modified.toLocaleDateString());
|
|
17722
|
-
}
|
|
17723
|
-
return parts.join(' • ');
|
|
17724
|
-
}
|
|
17725
|
-
formatFileSize(bytes) {
|
|
17726
|
-
if (bytes === 0) {
|
|
17727
|
-
return '0 B';
|
|
17728
|
-
}
|
|
17729
|
-
const k = 1024;
|
|
17730
|
-
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
17731
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
17732
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
17733
|
-
}
|
|
17734
|
-
getTypeDisplayName(type) {
|
|
17735
|
-
switch (type) {
|
|
17736
|
-
case 'file': return 'File';
|
|
17737
|
-
case 'folder': return 'Folder';
|
|
17738
|
-
case 'dataset': return 'Dataset';
|
|
17739
|
-
case 'zvol': return 'Zvol';
|
|
17740
|
-
case 'mountpoint': return 'Mount Point';
|
|
17741
|
-
default: return type;
|
|
17742
|
-
}
|
|
17743
|
-
}
|
|
17841
|
+
/**
|
|
17842
|
+
* Highlights selected rows through the table's supported active-row
|
|
17843
|
+
* styling (`activeWhen`) instead of reaching into its DOM.
|
|
17844
|
+
*/
|
|
17845
|
+
isRowSelected = (row) => this.isSelected(row);
|
|
17744
17846
|
formatDate(date) {
|
|
17745
17847
|
const now = new Date();
|
|
17746
17848
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
@@ -17768,13 +17870,14 @@ class TnFilePickerPopupComponent {
|
|
|
17768
17870
|
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${timePart}`;
|
|
17769
17871
|
}
|
|
17770
17872
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17771
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFilePickerPopupComponent, isStandalone: true, selector: "tn-file-picker-popup", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, allowCreate: { classPropertyName: "allowCreate", publicName: "allowCreate", isSignal: true, isRequired: false, transformFunction: null }, allowDatasetCreate: { classPropertyName: "allowDatasetCreate", publicName: "allowDatasetCreate", isSignal: true, isRequired: false, transformFunction: null }, allowZvolCreate: { classPropertyName: "allowZvolCreate", publicName: "allowZvolCreate", isSignal: true, isRequired: false, transformFunction: null }, currentPath: { classPropertyName: "currentPath", publicName: "currentPath", isSignal: true, isRequired: false, transformFunction: null }, fileItems: { classPropertyName: "fileItems", publicName: "fileItems", isSignal: true, isRequired: false, transformFunction: null }, selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, creationLoading: { classPropertyName: "creationLoading", publicName: "creationLoading", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemDoubleClick: "itemDoubleClick", pathNavigate: "pathNavigate", createFolder: "createFolder", clearSelection: "clearSelection", close: "close", submit: "submit", cancel: "cancel", submitFolderName: "submitFolderName", cancelFolderCreation: "cancelFolderCreation" }, host: { classAttribute: "tn-file-picker-popup" }, ngImport: i0, template: "<!-- Header with breadcrumb navigation -->\n<div class=\"tn-file-picker-header\">\n <nav class=\"tn-file-picker-breadcrumb\" aria-label=\"File path\">\n @for (segment of currentPath() | tnTruncatePath; track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n [class.current]=\"last\"\n [class.parent-nav]=\"segment.name === '..'\"\n [disabled]=\"last\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\n\n <div class=\"tn-file-picker-actions\">\n @if (allowCreate()) {\n <tn-button\n variant=\"outline\"\n label=\"New Folder\"\n [disabled]=\"isCreateDisabled()\"\n (onClick)=\"onCreateFolder()\" />\n }\n </div>\n</div>\n\n<!-- Loading indicator -->\n@if (loading()) {\n <div class=\"tn-file-picker-loading\">\n <tn-icon name=\"loading\" library=\"mdi\" />\n <span>Loading...</span>\n </div>\n}\n\n<!-- File table -->\n@if (!loading()) {\n <div class=\"tn-file-picker-content\">\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\">\n\n <!-- Selection column -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n <input \n type=\"checkbox\" \n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\">\n </ng-template>\n </ng-container>\n }\n\n <!-- Name column -->\n <ng-container tnColumnDef=\"name\">\n <ng-template tnHeaderCellDef>Name</ng-template>\n <ng-template let-item tnCellDef>\n\n <!-- NORMAL MODE: Display name -->\n @if (!item.isCreating) {\n <div\n class=\"file-name-cell\"\n [class.disabled]=\"!!item.disabled\"\n [class.zfs-object]=\"isZfsObject(item)\"\n [attr.tabindex]=\"item.disabled ? null : 0\"\n [attr.role]=\"'button'\"\n (click)=\"onItemClick(item)\"\n (dblclick)=\"onItemDoubleClick(item)\"\n (keydown.enter)=\"onItemDoubleClick(item)\"\n (keydown.space)=\"onItemClick(item)\">\n <tn-icon\n [name]=\"getItemIcon(item)\"\n [library]=\"getItemIconLibrary(item)\"\n [class]=\"'file-icon file-icon-' + item.type\" />\n <span class=\"file-name\">{{ item.name }}</span>\n\n <!-- ZFS badge -->\n @if (isZfsObject(item)) {\n <span\n [class]=\"'zfs-badge zfs-badge-' + item.type\">\n {{ getZfsBadge(item) }}\n </span>\n }\n\n <!-- Permission indicator -->\n @if (item.permissions === 'none') {\n <tn-icon\n name=\"lock\"\n library=\"mdi\"\n class=\"permission-icon\" />\n }\n </div>\n }\n\n <!-- EDIT MODE: Inline name input with error display -->\n @if (item.isCreating) {\n <div class=\"file-name-cell-wrapper\">\n <div class=\"file-name-cell editing\" [class.has-error]=\"!!item.creationError\">\n <tn-icon\n name=\"folder\"\n library=\"mdi\"\n class=\"file-icon file-icon-folder\" />\n <input\n #folderNameInput\n type=\"text\"\n role=\"textbox\"\n aria-label=\"Folder name\"\n class=\"folder-name-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n [class.error]=\"!!item.creationError\"\n [value]=\"item.name\"\n [disabled]=\"creationLoading()\"\n [attr.data-autofocus]=\"true\"\n (keydown)=\"onFolderNameKeyDown($event, item)\"\n (blur)=\"onFolderNameInputBlur($event, item)\">\n\n <!-- Loading indicator during submission -->\n @if (creationLoading()) {\n <tn-icon\n name=\"loading\"\n library=\"mdi\"\n class=\"creation-loading-icon\" />\n }\n </div>\n\n <!-- Inline error message -->\n @if (item.creationError) {\n <div class=\"folder-creation-error\">\n <tn-icon name=\"alert-circle\" library=\"mdi\" class=\"error-icon\" />\n <span class=\"error-text\">{{ item.creationError }}</span>\n </div>\n }\n </div>\n }\n\n </ng-template>\n </ng-container>\n\n <!-- Size column -->\n <ng-container tnColumnDef=\"size\">\n <ng-template tnHeaderCellDef>Size</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.size !== undefined) {\n <span>{{ item.size | tnFileSize }}</span>\n }\n @if (item.size === undefined && item.type === 'folder') {\n <span class=\"folder-indicator\">--</span>\n }\n </ng-template>\n </ng-container>\n\n <!-- Modified column -->\n <ng-container tnColumnDef=\"modified\">\n <ng-template tnHeaderCellDef>Modified</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.modified) {\n <span>{{ formatDate(item.modified) }}</span>\n }\n </ng-template>\n </ng-container>\n\n\n </tn-table>\n\n <!-- Empty state -->\n @if (filteredFileItems().length === 0) {\n <div class=\"empty-state\">\n <tn-icon name=\"folder-open\" library=\"mdi\" customSize=\"48px\" />\n <p>No items found</p>\n </div>\n }\n </div>\n}\n\n<!-- Footer -->\n@if (!loading()) {\n <div class=\"tn-file-picker-footer\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n }\n @if (selectedItems().length === 0) {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n <div class=\"footer-actions\">\n <tn-button\n label=\"Select\"\n [disabled]=\"selectedItems().length === 0\"\n (onClick)=\"onSubmit()\" />\n </div>\n </div>\n}", styles: [":host{display:block;background:var(--tn-bg1, white);color:var(--tn-fg1, #333);padding:0;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014;border-radius:8px;border:1px solid var(--tn-lines, #e0e0e0);min-width:400px;max-width:600px;min-height:500px;max-height:600px;font-family:var(--tn-font-family-body);display:flex;flex-direction:column;overflow:hidden}.tn-file-picker-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tn-content-padding, 24px);padding-bottom:16px;border-bottom:1px solid var(--tn-lines)}.tn-file-picker-breadcrumb{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 8px;border-radius:4px;font-size:1rem;white-space:nowrap;transition:background-color .15s ease-in-out}.tn-file-picker-breadcrumb .breadcrumb-segment:hover:not(:disabled){background:var(--tn-bg2)}.tn-file-picker-breadcrumb .breadcrumb-segment:disabled,.tn-file-picker-breadcrumb .breadcrumb-segment.current{color:var(--tn-fg1);cursor:default;font-weight:500}.tn-file-picker-breadcrumb .breadcrumb-segment:not(:last-child):after{content:\"/\";margin-left:8px;color:var(--tn-alt-fg1)}.tn-file-picker-actions{display:flex;align-items:center;gap:8px}.tn-file-picker-actions tn-button{font-size:1rem}.tn-file-picker-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px;color:var(--tn-fg2)}.tn-file-picker-loading tn-icon{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tn-file-picker-content{flex:1;min-height:0;overflow-y:auto}.file-list-viewport{width:100%;height:100%}.file-list-viewport .cdk-virtual-scroll-content-wrapper{width:100%}tn-table{width:100%}tn-table th,tn-table .tn-table__header-cell{font-weight:600;color:var(--tn-fg1);padding:12px 16px;border-bottom:2px solid var(--tn-lines)}tn-table td,tn-table .tn-table__cell{padding:8px 16px;border-bottom:1px solid var(--tn-lines)}.file-checkbox{display:flex;align-items:center}.file-checkbox input[type=checkbox]{margin:0;width:16px;height:16px}.file-name-cell{display:flex;align-items:center;gap:8px;cursor:pointer}.file-name-cell.disabled{opacity:.5;color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-name{color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-icon{opacity:.6}.file-name-cell.disabled:has(.file-icon-folder),.file-name-cell.disabled:has(.file-icon-dataset),.file-name-cell.disabled:has(.file-icon-mountpoint){cursor:pointer}.file-name-cell.disabled:not(:has(.file-icon-folder)):not(:has(.file-icon-dataset)):not(:has(.file-icon-mountpoint)){cursor:not-allowed}.file-name-cell.editing{display:flex;align-items:center;gap:8px;padding:2px;cursor:default}.file-name-cell.editing .folder-name-input{flex:1;border:2px solid var(--tn-primary, #0066cc);padding:4px 8px;font-size:inherit;font-family:inherit;background:var(--tn-bg1, white);color:var(--tn-fg1, black);outline:none;border-radius:3px;min-width:200px}.file-name-cell.editing .folder-name-input:focus{border-color:var(--tn-primary, #0066cc);box-shadow:0 0 0 3px #0066cc1a}.file-name-cell.editing .folder-name-input.error{border-color:var(--tn-error, #d32f2f)}.file-name-cell.editing .folder-name-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2, #f5f5f5)}.file-name-cell.editing .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary, #0066cc);flex-shrink:0}.file-name-cell-wrapper{display:flex;flex-direction:column;gap:4px}.folder-creation-error{display:flex;align-items:center;gap:6px;padding:4px 8px 4px 36px;margin-bottom:12px;background:#d32f2f1a;border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.folder-creation-error .error-icon{flex-shrink:0;width:20px;height:20px}.folder-creation-error .error-text{flex:1}.file-icon{display:flex;align-items:center;justify-content:center;font-size:var(--tn-icon-md, 20px);flex-shrink:0;line-height:1}.file-icon.file-icon-folder{color:var(--tn-primary)}.file-icon.file-icon-dataset{color:var(--tn-blue, #007db3)}.file-icon.file-icon-zvol{color:var(--tn-green, #71BF44)}.file-icon.file-icon-mountpoint{color:var(--tn-orange, #E68D37)}.file-name{flex:1;font-weight:500;line-height:1.4}.zfs-badge{display:inline-flex;align-items:center;background:var(--tn-alt-bg2);color:var(--tn-alt-fg2);font-size:.625rem;font-weight:600;padding:2px 6px;border-radius:12px;text-transform:uppercase;letter-spacing:.5px;line-height:1}.zfs-badge.zfs-badge-dataset{background:var(--tn-blue);color:#fff}.zfs-badge.zfs-badge-zvol{background:var(--tn-green);color:#fff}.zfs-badge.zfs-badge-mountpoint{background:var(--tn-orange);color:#fff}.permission-icon{display:flex;align-items:center;justify-content:center;color:var(--tn-red);font-size:var(--tn-icon-sm, 16px);line-height:1}.file-type{font-size:1rem;padding:2px 8px;border-radius:12px}.file-type.type-folder{background:var(--tn-alt-bg1);color:var(--tn-alt-fg2)}.file-type.type-file{background:var(--tn-bg2);color:var(--tn-fg2)}.file-type.type-dataset{background:#007db31a;color:var(--tn-blue)}.file-type.type-zvol{background:#71bf441a;color:var(--tn-green)}.file-type.type-mountpoint{background:#e68d371a;color:var(--tn-orange)}.folder-indicator{color:var(--tn-alt-fg1);font-style:italic}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--tn-alt-fg1);text-align:center}.empty-state tn-icon{margin-bottom:16px;opacity:.5}.empty-state p{margin:0;font-size:1rem}.tn-file-picker-footer{display:flex;align-items:center;justify-content:space-between;padding:16px var(--tn-content-padding, 24px);border-top:1px solid var(--tn-lines);background:var(--tn-bg2);border-bottom-left-radius:8px;border-bottom-right-radius:8px}.selection-count{font-size:1rem;color:var(--tn-fg2);font-weight:500}.footer-actions{display:flex;gap:8px}@media(prefers-reduced-motion:reduce){.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){:host{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnTableComponent, selector: "tn-table", inputs: ["dataSource", "displayedColumns", "trackBy", "emptyMessage", "emptyIcon", "selectable", "expandable", "bordered", "isRowExpandable", "activeRow", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable"], outputs: ["sortChange", "selectionChange", "rowClick"] }, { kind: "directive", type: TnTableColumnDirective, selector: "[tnColumnDef]", inputs: ["tnColumnDef", "sortable", "width"], exportAs: ["tnColumnDef"] }, { kind: "directive", type: TnHeaderCellDefDirective, selector: "[tnHeaderCellDef]" }, { kind: "directive", type: TnCellDefDirective, selector: "[tnCellDef]" }, { kind: "ngmodule", type: ScrollingModule }, { kind: "ngmodule", type: A11yModule }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
|
|
17873
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFilePickerPopupComponent, isStandalone: true, selector: "tn-file-picker-popup", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, createActions: { classPropertyName: "createActions", publicName: "createActions", isSignal: true, isRequired: false, transformFunction: null }, currentPath: { classPropertyName: "currentPath", publicName: "currentPath", isSignal: true, isRequired: false, transformFunction: null }, rootPath: { classPropertyName: "rootPath", publicName: "rootPath", isSignal: true, isRequired: false, transformFunction: null }, fileItems: { classPropertyName: "fileItems", publicName: "fileItems", isSignal: true, isRequired: false, transformFunction: null }, selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemDoubleClick: "itemDoubleClick", pathNavigate: "pathNavigate", createAction: "createAction", created: "created", clearSelection: "clearSelection", close: "close", submit: "submit", cancel: "cancel" }, host: { classAttribute: "tn-file-picker-popup" }, ngImport: i0, template: "<!-- Header with breadcrumb navigation -->\n<div class=\"tn-file-picker-header\">\n <nav class=\"tn-file-picker-breadcrumb\" aria-label=\"File path\">\n @for (segment of currentPath() | tnTruncatePath : rootPath(); track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n [class.current]=\"last\"\n [disabled]=\"last\"\n [attr.aria-label]=\"segment.name === '\u2026' || !segment.name ? 'Navigate to ' + segment.path : null\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\n</div>\n\n<!-- Loading indicator -->\n@if (loading()) {\n <div class=\"tn-file-picker-loading\">\n <tn-icon name=\"loading\" library=\"mdi\" />\n <span>Loading...</span>\n </div>\n}\n\n<!-- File table -->\n@if (!loading()) {\n <div class=\"tn-file-picker-content\">\n\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\"\n [clickable]=\"true\"\n [activeWhen]=\"isRowSelected\"\n (rowClick)=\"onItemClick($event)\"\n (rowDoubleClick)=\"onItemDoubleClick($event)\">\n\n <!-- Selection column -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n @if (!item.inlineCreate) {\n <tn-checkbox\n [hideLabel]=\"true\"\n [label]=\"'Select ' + item.name\"\n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\" />\n }\n </ng-template>\n </ng-container>\n }\n\n <!-- Name column -->\n <ng-container tnColumnDef=\"name\">\n <ng-template tnHeaderCellDef>Name</ng-template>\n <ng-template let-item tnCellDef>\n\n <!-- Inline creation row for create actions with a `create` callback -->\n @if (item.inlineCreate) {\n @if (inlineCreation(); as creation) {\n <div class=\"inline-create-cell\">\n <div class=\"inline-create-row\" [class.has-error]=\"!!creation.error\">\n <tn-icon class=\"file-icon\" [name]=\"creation.action.icon || 'mdi-folder'\" />\n <input\n type=\"text\"\n class=\"inline-create-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n [attr.aria-label]=\"creation.action.label + ' name'\"\n [attr.aria-invalid]=\"creation.error ? true : null\"\n [attr.aria-describedby]=\"creation.error ? inlineErrorId : null\"\n [class.error]=\"!!creation.error\"\n [disabled]=\"creation.loading\"\n [value]=\"creation.name\"\n (input)=\"onInlineCreationInput($event)\"\n (keydown)=\"onInlineCreationKeyDown($event)\"\n (blur)=\"onInlineCreationBlur()\">\n @if (creation.loading) {\n <tn-icon name=\"mdi-loading\" class=\"creation-loading-icon\" aria-hidden=\"true\" />\n }\n </div>\n @if (creation.error) {\n <div class=\"inline-create-error\" role=\"alert\" [id]=\"inlineErrorId\">\n <tn-icon name=\"mdi-alert-circle\" class=\"error-icon\" aria-hidden=\"true\" />\n <span class=\"error-text\">{{ creation.error }}</span>\n </div>\n }\n </div>\n }\n } @else {\n <div\n class=\"file-name-cell\"\n [class.selected]=\"isSelected(item)\"\n [class.disabled]=\"!!item.dimmed\"\n [class.zfs-object]=\"isZfsObject(item)\">\n <tn-icon\n [name]=\"getItemIcon(item)\"\n [class]=\"'file-icon file-icon-' + item.type\" />\n <span class=\"file-name\">{{ item.name }}</span>\n\n <!-- ZFS badge -->\n @if (isZfsObject(item)) {\n <span\n [class]=\"'zfs-badge zfs-badge-' + item.type\">\n {{ getZfsBadge(item) }}\n </span>\n }\n\n <!-- Permission indicator -->\n @if (item.permissions === 'none') {\n <tn-icon\n name=\"lock\"\n library=\"mdi\"\n class=\"permission-icon\" />\n }\n\n <!-- Navigation affordance: single click opens the directory -->\n @if (isNavigatable(item)) {\n <button\n type=\"button\"\n class=\"navigate-button\"\n [attr.aria-label]=\"'Open ' + item.name\"\n [attr.title]=\"'Open ' + item.name\"\n (click)=\"onNavigateClick($event, item)\"\n (dblclick)=\"$event.stopPropagation()\">\n <tn-icon name=\"mdi-chevron-right\" size=\"sm\" />\n </button>\n }\n </div>\n }\n </ng-template>\n </ng-container>\n\n <!-- Size column -->\n <ng-container tnColumnDef=\"size\">\n <ng-template tnHeaderCellDef>Size</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.size !== undefined) {\n <span>{{ item.size | tnFileSize }}</span>\n }\n @if (item.size === undefined && item.type === 'folder') {\n <span class=\"folder-indicator\">--</span>\n }\n </ng-template>\n </ng-container>\n\n <!-- Modified column -->\n <ng-container tnColumnDef=\"modified\">\n <ng-template tnHeaderCellDef>Modified</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.modified) {\n <span>{{ formatDate(item.modified) }}</span>\n }\n </ng-template>\n </ng-container>\n\n\n </tn-table>\n\n <!-- Empty state -->\n @if (filteredFileItems().length === 0) {\n <div class=\"empty-state\">\n <tn-icon name=\"folder-open\" library=\"mdi\" customSize=\"48px\" />\n <p>No items found</p>\n </div>\n }\n </div>\n}\n\n<!-- Footer -->\n@if (!loading()) {\n <div class=\"tn-file-picker-footer\">\n <div class=\"footer-selection\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n <tn-button\n variant=\"outline\"\n label=\"Clear Selection\"\n (onClick)=\"onClearSelection()\" />\n } @else if (allowCurrentDirectorySelection()) {\n <span class=\"selection-count current-directory-selection\">\n Current directory selected\n </span>\n } @else {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n </div>\n <div class=\"footer-actions\">\n @for (action of createActions(); track action.id) {\n <tn-button\n variant=\"outline\"\n [label]=\"action.label\"\n [disabled]=\"!!inlineCreation()\"\n (onClick)=\"onCreateAction(action)\" />\n }\n <!-- Disabled while the inline creation row is open: its blur auto-submit\n would race the selection submit -->\n <tn-button\n label=\"Select\"\n [disabled]=\"(selectedItems().length === 0 && !allowCurrentDirectorySelection()) || !!inlineCreation()\"\n (onClick)=\"onSubmit()\" />\n </div>\n </div>\n}", styles: [":host{display:block;background:var(--tn-bg1, white);color:var(--tn-fg1, #333);padding:0;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014;border-radius:8px;border:1px solid var(--tn-lines, #e0e0e0);width:520px;max-width:calc(100vw - 32px);min-height:500px;max-height:600px;font-family:var(--tn-font-family-body);display:flex;flex-direction:column;overflow:hidden}.tn-file-picker-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tn-content-padding, 24px);padding-bottom:16px;border-bottom:1px solid var(--tn-lines)}.tn-file-picker-breadcrumb{display:flex;align-items:center;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 2px;border-radius:4px;font-size:1rem;white-space:nowrap;transition:background-color .15s ease-in-out}.tn-file-picker-breadcrumb .breadcrumb-segment:hover:not(:disabled){background:var(--tn-bg2)}.tn-file-picker-breadcrumb .breadcrumb-segment:disabled,.tn-file-picker-breadcrumb .breadcrumb-segment.current{color:var(--tn-fg1);cursor:default;font-weight:500}.tn-file-picker-breadcrumb .breadcrumb-segment:not(:last-child):after{content:\"/\";margin-left:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment:first-child:before{content:\"/\";margin-right:2px;color:var(--tn-alt-fg1)}.tn-file-picker-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px;color:var(--tn-fg2)}.tn-file-picker-loading tn-icon{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tn-file-picker-content{flex:1;min-height:0;overflow-y:auto}.inline-create-cell{display:flex;flex-direction:column;gap:4px;flex:1;min-width:0}.inline-create-row{display:flex;align-items:center;gap:8px}.inline-create-row .inline-create-input{flex:1;padding:4px 8px;border:2px solid var(--tn-primary);border-radius:3px;background:var(--tn-bg1);color:var(--tn-fg1);font-size:inherit;font-family:inherit;outline:none;min-width:0}.inline-create-row .inline-create-input.error{border-color:var(--tn-error, #d32f2f)}.inline-create-row .inline-create-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2)}.inline-create-row .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary);flex-shrink:0}.inline-create-error{display:flex;align-items:center;gap:6px;margin-top:4px;padding:4px 8px;background:color-mix(in srgb,var(--tn-error, #d32f2f) 10%,transparent);border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.inline-create-error .error-icon{flex-shrink:0}.inline-create-error .error-text{flex:1}.file-list-viewport{width:100%;height:100%}.file-list-viewport .cdk-virtual-scroll-content-wrapper{width:100%}tn-table{width:100%}tn-table th,tn-table .tn-table__header-cell{font-weight:600;color:var(--tn-fg1);padding:12px 16px;border-bottom:2px solid var(--tn-lines)}tn-table td,tn-table .tn-table__cell{padding:8px 16px;border-bottom:1px solid var(--tn-lines)}.file-name-cell{display:flex;align-items:center;gap:8px;cursor:pointer}.file-name-cell.disabled{opacity:.5;color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-name{color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-icon{opacity:.6}.file-name-cell.disabled:has(.file-icon-folder),.file-name-cell.disabled:has(.file-icon-dataset),.file-name-cell.disabled:has(.file-icon-mountpoint){cursor:pointer}.file-name-cell.disabled:not(:has(.file-icon-folder)):not(:has(.file-icon-dataset)):not(:has(.file-icon-mountpoint)){cursor:not-allowed}.file-icon{display:flex;align-items:center;justify-content:center;font-size:var(--tn-icon-md, 20px);flex-shrink:0;line-height:1}.file-icon.file-icon-folder{color:var(--tn-primary)}.file-icon.file-icon-dataset{color:var(--tn-blue, #007db3)}.file-icon.file-icon-zvol{color:var(--tn-green, #71BF44)}.file-icon.file-icon-mountpoint{color:var(--tn-orange, #E68D37)}.file-name{flex:1;font-weight:500;line-height:1.4}.zfs-badge{display:inline-flex;align-items:center;background:var(--tn-alt-bg2);color:var(--tn-alt-fg2);font-size:.625rem;font-weight:600;padding:2px 6px;border-radius:12px;text-transform:uppercase;letter-spacing:.5px;line-height:1}.zfs-badge.zfs-badge-dataset{background:var(--tn-blue);color:#fff}.zfs-badge.zfs-badge-zvol{background:var(--tn-green);color:#fff}.zfs-badge.zfs-badge-mountpoint{background:var(--tn-orange);color:#fff}.permission-icon{display:flex;align-items:center;justify-content:center;color:var(--tn-red);font-size:var(--tn-icon-sm, 16px);line-height:1}.navigate-button{display:flex;align-items:center;justify-content:center;margin-left:auto;padding:2px;background:none;border:none;border-radius:4px;color:var(--tn-fg2);cursor:pointer}.navigate-button:hover,.navigate-button:focus-visible{background:var(--tn-bg3);color:var(--tn-primary)}.file-type{font-size:1rem;padding:2px 8px;border-radius:12px}.file-type.type-folder{background:var(--tn-alt-bg1);color:var(--tn-alt-fg2)}.file-type.type-file{background:var(--tn-bg2);color:var(--tn-fg2)}.file-type.type-dataset{background:#007db31a;color:var(--tn-blue)}.file-type.type-zvol{background:#71bf441a;color:var(--tn-green)}.file-type.type-mountpoint{background:#e68d371a;color:var(--tn-orange)}.folder-indicator{color:var(--tn-alt-fg1);font-style:italic}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--tn-alt-fg1);text-align:center}.empty-state tn-icon{margin-bottom:16px;opacity:.5}.empty-state p{margin:0;font-size:1rem}.tn-file-picker-footer{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px var(--tn-content-padding, 24px);border-top:1px solid var(--tn-lines);background:var(--tn-bg2);border-bottom-left-radius:8px;border-bottom-right-radius:8px}.tn-file-picker-footer .footer-selection{display:flex;align-items:center;gap:12px}.selection-count{font-size:1rem;color:var(--tn-fg2);font-weight:500}.footer-actions{display:flex;gap:8px}@media(prefers-reduced-motion:reduce){.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){:host{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnTableComponent, selector: "tn-table", inputs: ["dataSource", "displayedColumns", "trackBy", "emptyMessage", "emptyIcon", "selectable", "expandable", "bordered", "isRowExpandable", "activeRow", "activeWhen", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable"], outputs: ["sortChange", "selectionChange", "rowClick", "rowDoubleClick"] }, { kind: "directive", type: TnTableColumnDirective, selector: "[tnColumnDef]", inputs: ["tnColumnDef", "sortable", "width"], exportAs: ["tnColumnDef"] }, { kind: "directive", type: TnHeaderCellDefDirective, selector: "[tnHeaderCellDef]" }, { kind: "directive", type: TnCellDefDirective, selector: "[tnCellDef]" }, { kind: "ngmodule", type: ScrollingModule }, { kind: "ngmodule", type: A11yModule }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
|
|
17772
17874
|
}
|
|
17773
17875
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, decorators: [{
|
|
17774
17876
|
type: Component,
|
|
17775
17877
|
args: [{ selector: 'tn-file-picker-popup', standalone: true, imports: [
|
|
17776
17878
|
TnIconComponent,
|
|
17777
17879
|
TnButtonComponent,
|
|
17880
|
+
TnCheckboxComponent,
|
|
17778
17881
|
TnTableComponent,
|
|
17779
17882
|
TnTableColumnDirective,
|
|
17780
17883
|
TnHeaderCellDefDirective,
|
|
@@ -17785,15 +17888,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
17785
17888
|
TruncatePathPipe
|
|
17786
17889
|
], host: {
|
|
17787
17890
|
'class': 'tn-file-picker-popup'
|
|
17788
|
-
}, template: "<!-- Header with breadcrumb navigation -->\n<div class=\"tn-file-picker-header\">\n <nav class=\"tn-file-picker-breadcrumb\" aria-label=\"File path\">\n @for (segment of currentPath() | tnTruncatePath; track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n [class.current]=\"last\"\n [class.parent-nav]=\"segment.name === '..'\"\n [disabled]=\"last\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\n\n <div class=\"tn-file-picker-actions\">\n @if (allowCreate()) {\n <tn-button\n variant=\"outline\"\n label=\"New Folder\"\n [disabled]=\"isCreateDisabled()\"\n (onClick)=\"onCreateFolder()\" />\n }\n </div>\n</div>\n\n<!-- Loading indicator -->\n@if (loading()) {\n <div class=\"tn-file-picker-loading\">\n <tn-icon name=\"loading\" library=\"mdi\" />\n <span>Loading...</span>\n </div>\n}\n\n<!-- File table -->\n@if (!loading()) {\n <div class=\"tn-file-picker-content\">\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\">\n\n <!-- Selection column -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n <input \n type=\"checkbox\" \n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\">\n </ng-template>\n </ng-container>\n }\n\n <!-- Name column -->\n <ng-container tnColumnDef=\"name\">\n <ng-template tnHeaderCellDef>Name</ng-template>\n <ng-template let-item tnCellDef>\n\n <!-- NORMAL MODE: Display name -->\n @if (!item.isCreating) {\n <div\n class=\"file-name-cell\"\n [class.disabled]=\"!!item.disabled\"\n [class.zfs-object]=\"isZfsObject(item)\"\n [attr.tabindex]=\"item.disabled ? null : 0\"\n [attr.role]=\"'button'\"\n (click)=\"onItemClick(item)\"\n (dblclick)=\"onItemDoubleClick(item)\"\n (keydown.enter)=\"onItemDoubleClick(item)\"\n (keydown.space)=\"onItemClick(item)\">\n <tn-icon\n [name]=\"getItemIcon(item)\"\n [library]=\"getItemIconLibrary(item)\"\n [class]=\"'file-icon file-icon-' + item.type\" />\n <span class=\"file-name\">{{ item.name }}</span>\n\n <!-- ZFS badge -->\n @if (isZfsObject(item)) {\n <span\n [class]=\"'zfs-badge zfs-badge-' + item.type\">\n {{ getZfsBadge(item) }}\n </span>\n }\n\n <!-- Permission indicator -->\n @if (item.permissions === 'none') {\n <tn-icon\n name=\"lock\"\n library=\"mdi\"\n class=\"permission-icon\" />\n }\n </div>\n }\n\n <!-- EDIT MODE: Inline name input with error display -->\n @if (item.isCreating) {\n <div class=\"file-name-cell-wrapper\">\n <div class=\"file-name-cell editing\" [class.has-error]=\"!!item.creationError\">\n <tn-icon\n name=\"folder\"\n library=\"mdi\"\n class=\"file-icon file-icon-folder\" />\n <input\n #folderNameInput\n type=\"text\"\n role=\"textbox\"\n aria-label=\"Folder name\"\n class=\"folder-name-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n [class.error]=\"!!item.creationError\"\n [value]=\"item.name\"\n [disabled]=\"creationLoading()\"\n [attr.data-autofocus]=\"true\"\n (keydown)=\"onFolderNameKeyDown($event, item)\"\n (blur)=\"onFolderNameInputBlur($event, item)\">\n\n <!-- Loading indicator during submission -->\n @if (creationLoading()) {\n <tn-icon\n name=\"loading\"\n library=\"mdi\"\n class=\"creation-loading-icon\" />\n }\n </div>\n\n <!-- Inline error message -->\n @if (item.creationError) {\n <div class=\"folder-creation-error\">\n <tn-icon name=\"alert-circle\" library=\"mdi\" class=\"error-icon\" />\n <span class=\"error-text\">{{ item.creationError }}</span>\n </div>\n }\n </div>\n }\n\n </ng-template>\n </ng-container>\n\n <!-- Size column -->\n <ng-container tnColumnDef=\"size\">\n <ng-template tnHeaderCellDef>Size</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.size !== undefined) {\n <span>{{ item.size | tnFileSize }}</span>\n }\n @if (item.size === undefined && item.type === 'folder') {\n <span class=\"folder-indicator\">--</span>\n }\n </ng-template>\n </ng-container>\n\n <!-- Modified column -->\n <ng-container tnColumnDef=\"modified\">\n <ng-template tnHeaderCellDef>Modified</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.modified) {\n <span>{{ formatDate(item.modified) }}</span>\n }\n </ng-template>\n </ng-container>\n\n\n </tn-table>\n\n <!-- Empty state -->\n @if (filteredFileItems().length === 0) {\n <div class=\"empty-state\">\n <tn-icon name=\"folder-open\" library=\"mdi\" customSize=\"48px\" />\n <p>No items found</p>\n </div>\n }\n </div>\n}\n\n<!-- Footer -->\n@if (!loading()) {\n <div class=\"tn-file-picker-footer\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n }\n @if (selectedItems().length === 0) {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n <div class=\"footer-actions\">\n <tn-button\n label=\"Select\"\n [disabled]=\"selectedItems().length === 0\"\n (onClick)=\"onSubmit()\" />\n </div>\n </div>\n}", styles: [":host{display:block;background:var(--tn-bg1, white);color:var(--tn-fg1, #333);padding:0;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014;border-radius:8px;border:1px solid var(--tn-lines, #e0e0e0);min-width:400px;max-width:600px;min-height:500px;max-height:600px;font-family:var(--tn-font-family-body);display:flex;flex-direction:column;overflow:hidden}.tn-file-picker-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tn-content-padding, 24px);padding-bottom:16px;border-bottom:1px solid var(--tn-lines)}.tn-file-picker-breadcrumb{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 8px;border-radius:4px;font-size:1rem;white-space:nowrap;transition:background-color .15s ease-in-out}.tn-file-picker-breadcrumb .breadcrumb-segment:hover:not(:disabled){background:var(--tn-bg2)}.tn-file-picker-breadcrumb .breadcrumb-segment:disabled,.tn-file-picker-breadcrumb .breadcrumb-segment.current{color:var(--tn-fg1);cursor:default;font-weight:500}.tn-file-picker-breadcrumb .breadcrumb-segment:not(:last-child):after{content:\"/\";margin-left:8px;color:var(--tn-alt-fg1)}.tn-file-picker-actions{display:flex;align-items:center;gap:8px}.tn-file-picker-actions tn-button{font-size:1rem}.tn-file-picker-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px;color:var(--tn-fg2)}.tn-file-picker-loading tn-icon{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tn-file-picker-content{flex:1;min-height:0;overflow-y:auto}.file-list-viewport{width:100%;height:100%}.file-list-viewport .cdk-virtual-scroll-content-wrapper{width:100%}tn-table{width:100%}tn-table th,tn-table .tn-table__header-cell{font-weight:600;color:var(--tn-fg1);padding:12px 16px;border-bottom:2px solid var(--tn-lines)}tn-table td,tn-table .tn-table__cell{padding:8px 16px;border-bottom:1px solid var(--tn-lines)}.file-checkbox{display:flex;align-items:center}.file-checkbox input[type=checkbox]{margin:0;width:16px;height:16px}.file-name-cell{display:flex;align-items:center;gap:8px;cursor:pointer}.file-name-cell.disabled{opacity:.5;color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-name{color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-icon{opacity:.6}.file-name-cell.disabled:has(.file-icon-folder),.file-name-cell.disabled:has(.file-icon-dataset),.file-name-cell.disabled:has(.file-icon-mountpoint){cursor:pointer}.file-name-cell.disabled:not(:has(.file-icon-folder)):not(:has(.file-icon-dataset)):not(:has(.file-icon-mountpoint)){cursor:not-allowed}.file-name-cell.editing{display:flex;align-items:center;gap:8px;padding:2px;cursor:default}.file-name-cell.editing .folder-name-input{flex:1;border:2px solid var(--tn-primary, #0066cc);padding:4px 8px;font-size:inherit;font-family:inherit;background:var(--tn-bg1, white);color:var(--tn-fg1, black);outline:none;border-radius:3px;min-width:200px}.file-name-cell.editing .folder-name-input:focus{border-color:var(--tn-primary, #0066cc);box-shadow:0 0 0 3px #0066cc1a}.file-name-cell.editing .folder-name-input.error{border-color:var(--tn-error, #d32f2f)}.file-name-cell.editing .folder-name-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2, #f5f5f5)}.file-name-cell.editing .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary, #0066cc);flex-shrink:0}.file-name-cell-wrapper{display:flex;flex-direction:column;gap:4px}.folder-creation-error{display:flex;align-items:center;gap:6px;padding:4px 8px 4px 36px;margin-bottom:12px;background:#d32f2f1a;border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.folder-creation-error .error-icon{flex-shrink:0;width:20px;height:20px}.folder-creation-error .error-text{flex:1}.file-icon{display:flex;align-items:center;justify-content:center;font-size:var(--tn-icon-md, 20px);flex-shrink:0;line-height:1}.file-icon.file-icon-folder{color:var(--tn-primary)}.file-icon.file-icon-dataset{color:var(--tn-blue, #007db3)}.file-icon.file-icon-zvol{color:var(--tn-green, #71BF44)}.file-icon.file-icon-mountpoint{color:var(--tn-orange, #E68D37)}.file-name{flex:1;font-weight:500;line-height:1.4}.zfs-badge{display:inline-flex;align-items:center;background:var(--tn-alt-bg2);color:var(--tn-alt-fg2);font-size:.625rem;font-weight:600;padding:2px 6px;border-radius:12px;text-transform:uppercase;letter-spacing:.5px;line-height:1}.zfs-badge.zfs-badge-dataset{background:var(--tn-blue);color:#fff}.zfs-badge.zfs-badge-zvol{background:var(--tn-green);color:#fff}.zfs-badge.zfs-badge-mountpoint{background:var(--tn-orange);color:#fff}.permission-icon{display:flex;align-items:center;justify-content:center;color:var(--tn-red);font-size:var(--tn-icon-sm, 16px);line-height:1}.file-type{font-size:1rem;padding:2px 8px;border-radius:12px}.file-type.type-folder{background:var(--tn-alt-bg1);color:var(--tn-alt-fg2)}.file-type.type-file{background:var(--tn-bg2);color:var(--tn-fg2)}.file-type.type-dataset{background:#007db31a;color:var(--tn-blue)}.file-type.type-zvol{background:#71bf441a;color:var(--tn-green)}.file-type.type-mountpoint{background:#e68d371a;color:var(--tn-orange)}.folder-indicator{color:var(--tn-alt-fg1);font-style:italic}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--tn-alt-fg1);text-align:center}.empty-state tn-icon{margin-bottom:16px;opacity:.5}.empty-state p{margin:0;font-size:1rem}.tn-file-picker-footer{display:flex;align-items:center;justify-content:space-between;padding:16px var(--tn-content-padding, 24px);border-top:1px solid var(--tn-lines);background:var(--tn-bg2);border-bottom-left-radius:8px;border-bottom-right-radius:8px}.selection-count{font-size:1rem;color:var(--tn-fg2);font-weight:500}.footer-actions{display:flex;gap:8px}@media(prefers-reduced-motion:reduce){.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){:host{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"] }]
|
|
17789
|
-
}], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], multiSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSelect", required: false }] }],
|
|
17891
|
+
}, template: "<!-- Header with breadcrumb navigation -->\n<div class=\"tn-file-picker-header\">\n <nav class=\"tn-file-picker-breadcrumb\" aria-label=\"File path\">\n @for (segment of currentPath() | tnTruncatePath : rootPath(); track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n [class.current]=\"last\"\n [disabled]=\"last\"\n [attr.aria-label]=\"segment.name === '\u2026' || !segment.name ? 'Navigate to ' + segment.path : null\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\n</div>\n\n<!-- Loading indicator -->\n@if (loading()) {\n <div class=\"tn-file-picker-loading\">\n <tn-icon name=\"loading\" library=\"mdi\" />\n <span>Loading...</span>\n </div>\n}\n\n<!-- File table -->\n@if (!loading()) {\n <div class=\"tn-file-picker-content\">\n\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\"\n [clickable]=\"true\"\n [activeWhen]=\"isRowSelected\"\n (rowClick)=\"onItemClick($event)\"\n (rowDoubleClick)=\"onItemDoubleClick($event)\">\n\n <!-- Selection column -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n @if (!item.inlineCreate) {\n <tn-checkbox\n [hideLabel]=\"true\"\n [label]=\"'Select ' + item.name\"\n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\" />\n }\n </ng-template>\n </ng-container>\n }\n\n <!-- Name column -->\n <ng-container tnColumnDef=\"name\">\n <ng-template tnHeaderCellDef>Name</ng-template>\n <ng-template let-item tnCellDef>\n\n <!-- Inline creation row for create actions with a `create` callback -->\n @if (item.inlineCreate) {\n @if (inlineCreation(); as creation) {\n <div class=\"inline-create-cell\">\n <div class=\"inline-create-row\" [class.has-error]=\"!!creation.error\">\n <tn-icon class=\"file-icon\" [name]=\"creation.action.icon || 'mdi-folder'\" />\n <input\n type=\"text\"\n class=\"inline-create-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n [attr.aria-label]=\"creation.action.label + ' name'\"\n [attr.aria-invalid]=\"creation.error ? true : null\"\n [attr.aria-describedby]=\"creation.error ? inlineErrorId : null\"\n [class.error]=\"!!creation.error\"\n [disabled]=\"creation.loading\"\n [value]=\"creation.name\"\n (input)=\"onInlineCreationInput($event)\"\n (keydown)=\"onInlineCreationKeyDown($event)\"\n (blur)=\"onInlineCreationBlur()\">\n @if (creation.loading) {\n <tn-icon name=\"mdi-loading\" class=\"creation-loading-icon\" aria-hidden=\"true\" />\n }\n </div>\n @if (creation.error) {\n <div class=\"inline-create-error\" role=\"alert\" [id]=\"inlineErrorId\">\n <tn-icon name=\"mdi-alert-circle\" class=\"error-icon\" aria-hidden=\"true\" />\n <span class=\"error-text\">{{ creation.error }}</span>\n </div>\n }\n </div>\n }\n } @else {\n <div\n class=\"file-name-cell\"\n [class.selected]=\"isSelected(item)\"\n [class.disabled]=\"!!item.dimmed\"\n [class.zfs-object]=\"isZfsObject(item)\">\n <tn-icon\n [name]=\"getItemIcon(item)\"\n [class]=\"'file-icon file-icon-' + item.type\" />\n <span class=\"file-name\">{{ item.name }}</span>\n\n <!-- ZFS badge -->\n @if (isZfsObject(item)) {\n <span\n [class]=\"'zfs-badge zfs-badge-' + item.type\">\n {{ getZfsBadge(item) }}\n </span>\n }\n\n <!-- Permission indicator -->\n @if (item.permissions === 'none') {\n <tn-icon\n name=\"lock\"\n library=\"mdi\"\n class=\"permission-icon\" />\n }\n\n <!-- Navigation affordance: single click opens the directory -->\n @if (isNavigatable(item)) {\n <button\n type=\"button\"\n class=\"navigate-button\"\n [attr.aria-label]=\"'Open ' + item.name\"\n [attr.title]=\"'Open ' + item.name\"\n (click)=\"onNavigateClick($event, item)\"\n (dblclick)=\"$event.stopPropagation()\">\n <tn-icon name=\"mdi-chevron-right\" size=\"sm\" />\n </button>\n }\n </div>\n }\n </ng-template>\n </ng-container>\n\n <!-- Size column -->\n <ng-container tnColumnDef=\"size\">\n <ng-template tnHeaderCellDef>Size</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.size !== undefined) {\n <span>{{ item.size | tnFileSize }}</span>\n }\n @if (item.size === undefined && item.type === 'folder') {\n <span class=\"folder-indicator\">--</span>\n }\n </ng-template>\n </ng-container>\n\n <!-- Modified column -->\n <ng-container tnColumnDef=\"modified\">\n <ng-template tnHeaderCellDef>Modified</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.modified) {\n <span>{{ formatDate(item.modified) }}</span>\n }\n </ng-template>\n </ng-container>\n\n\n </tn-table>\n\n <!-- Empty state -->\n @if (filteredFileItems().length === 0) {\n <div class=\"empty-state\">\n <tn-icon name=\"folder-open\" library=\"mdi\" customSize=\"48px\" />\n <p>No items found</p>\n </div>\n }\n </div>\n}\n\n<!-- Footer -->\n@if (!loading()) {\n <div class=\"tn-file-picker-footer\">\n <div class=\"footer-selection\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n <tn-button\n variant=\"outline\"\n label=\"Clear Selection\"\n (onClick)=\"onClearSelection()\" />\n } @else if (allowCurrentDirectorySelection()) {\n <span class=\"selection-count current-directory-selection\">\n Current directory selected\n </span>\n } @else {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n </div>\n <div class=\"footer-actions\">\n @for (action of createActions(); track action.id) {\n <tn-button\n variant=\"outline\"\n [label]=\"action.label\"\n [disabled]=\"!!inlineCreation()\"\n (onClick)=\"onCreateAction(action)\" />\n }\n <!-- Disabled while the inline creation row is open: its blur auto-submit\n would race the selection submit -->\n <tn-button\n label=\"Select\"\n [disabled]=\"(selectedItems().length === 0 && !allowCurrentDirectorySelection()) || !!inlineCreation()\"\n (onClick)=\"onSubmit()\" />\n </div>\n </div>\n}", styles: [":host{display:block;background:var(--tn-bg1, white);color:var(--tn-fg1, #333);padding:0;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014;border-radius:8px;border:1px solid var(--tn-lines, #e0e0e0);width:520px;max-width:calc(100vw - 32px);min-height:500px;max-height:600px;font-family:var(--tn-font-family-body);display:flex;flex-direction:column;overflow:hidden}.tn-file-picker-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tn-content-padding, 24px);padding-bottom:16px;border-bottom:1px solid var(--tn-lines)}.tn-file-picker-breadcrumb{display:flex;align-items:center;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 2px;border-radius:4px;font-size:1rem;white-space:nowrap;transition:background-color .15s ease-in-out}.tn-file-picker-breadcrumb .breadcrumb-segment:hover:not(:disabled){background:var(--tn-bg2)}.tn-file-picker-breadcrumb .breadcrumb-segment:disabled,.tn-file-picker-breadcrumb .breadcrumb-segment.current{color:var(--tn-fg1);cursor:default;font-weight:500}.tn-file-picker-breadcrumb .breadcrumb-segment:not(:last-child):after{content:\"/\";margin-left:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment:first-child:before{content:\"/\";margin-right:2px;color:var(--tn-alt-fg1)}.tn-file-picker-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px;color:var(--tn-fg2)}.tn-file-picker-loading tn-icon{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tn-file-picker-content{flex:1;min-height:0;overflow-y:auto}.inline-create-cell{display:flex;flex-direction:column;gap:4px;flex:1;min-width:0}.inline-create-row{display:flex;align-items:center;gap:8px}.inline-create-row .inline-create-input{flex:1;padding:4px 8px;border:2px solid var(--tn-primary);border-radius:3px;background:var(--tn-bg1);color:var(--tn-fg1);font-size:inherit;font-family:inherit;outline:none;min-width:0}.inline-create-row .inline-create-input.error{border-color:var(--tn-error, #d32f2f)}.inline-create-row .inline-create-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2)}.inline-create-row .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary);flex-shrink:0}.inline-create-error{display:flex;align-items:center;gap:6px;margin-top:4px;padding:4px 8px;background:color-mix(in srgb,var(--tn-error, #d32f2f) 10%,transparent);border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.inline-create-error .error-icon{flex-shrink:0}.inline-create-error .error-text{flex:1}.file-list-viewport{width:100%;height:100%}.file-list-viewport .cdk-virtual-scroll-content-wrapper{width:100%}tn-table{width:100%}tn-table th,tn-table .tn-table__header-cell{font-weight:600;color:var(--tn-fg1);padding:12px 16px;border-bottom:2px solid var(--tn-lines)}tn-table td,tn-table .tn-table__cell{padding:8px 16px;border-bottom:1px solid var(--tn-lines)}.file-name-cell{display:flex;align-items:center;gap:8px;cursor:pointer}.file-name-cell.disabled{opacity:.5;color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-name{color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-icon{opacity:.6}.file-name-cell.disabled:has(.file-icon-folder),.file-name-cell.disabled:has(.file-icon-dataset),.file-name-cell.disabled:has(.file-icon-mountpoint){cursor:pointer}.file-name-cell.disabled:not(:has(.file-icon-folder)):not(:has(.file-icon-dataset)):not(:has(.file-icon-mountpoint)){cursor:not-allowed}.file-icon{display:flex;align-items:center;justify-content:center;font-size:var(--tn-icon-md, 20px);flex-shrink:0;line-height:1}.file-icon.file-icon-folder{color:var(--tn-primary)}.file-icon.file-icon-dataset{color:var(--tn-blue, #007db3)}.file-icon.file-icon-zvol{color:var(--tn-green, #71BF44)}.file-icon.file-icon-mountpoint{color:var(--tn-orange, #E68D37)}.file-name{flex:1;font-weight:500;line-height:1.4}.zfs-badge{display:inline-flex;align-items:center;background:var(--tn-alt-bg2);color:var(--tn-alt-fg2);font-size:.625rem;font-weight:600;padding:2px 6px;border-radius:12px;text-transform:uppercase;letter-spacing:.5px;line-height:1}.zfs-badge.zfs-badge-dataset{background:var(--tn-blue);color:#fff}.zfs-badge.zfs-badge-zvol{background:var(--tn-green);color:#fff}.zfs-badge.zfs-badge-mountpoint{background:var(--tn-orange);color:#fff}.permission-icon{display:flex;align-items:center;justify-content:center;color:var(--tn-red);font-size:var(--tn-icon-sm, 16px);line-height:1}.navigate-button{display:flex;align-items:center;justify-content:center;margin-left:auto;padding:2px;background:none;border:none;border-radius:4px;color:var(--tn-fg2);cursor:pointer}.navigate-button:hover,.navigate-button:focus-visible{background:var(--tn-bg3);color:var(--tn-primary)}.file-type{font-size:1rem;padding:2px 8px;border-radius:12px}.file-type.type-folder{background:var(--tn-alt-bg1);color:var(--tn-alt-fg2)}.file-type.type-file{background:var(--tn-bg2);color:var(--tn-fg2)}.file-type.type-dataset{background:#007db31a;color:var(--tn-blue)}.file-type.type-zvol{background:#71bf441a;color:var(--tn-green)}.file-type.type-mountpoint{background:#e68d371a;color:var(--tn-orange)}.folder-indicator{color:var(--tn-alt-fg1);font-style:italic}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--tn-alt-fg1);text-align:center}.empty-state tn-icon{margin-bottom:16px;opacity:.5}.empty-state p{margin:0;font-size:1rem}.tn-file-picker-footer{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px var(--tn-content-padding, 24px);border-top:1px solid var(--tn-lines);background:var(--tn-bg2);border-bottom-left-radius:8px;border-bottom-right-radius:8px}.tn-file-picker-footer .footer-selection{display:flex;align-items:center;gap:12px}.selection-count{font-size:1rem;color:var(--tn-fg2);font-weight:500}.footer-actions{display:flex;gap:8px}@media(prefers-reduced-motion:reduce){.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){:host{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"] }]
|
|
17892
|
+
}], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], multiSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSelect", required: false }] }], createActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "createActions", required: false }] }], currentPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPath", required: false }] }], rootPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootPath", required: false }] }], fileItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileItems", required: false }] }], selectedItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedItems", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], fileExtensions: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileExtensions", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], itemDoubleClick: [{ type: i0.Output, args: ["itemDoubleClick"] }], pathNavigate: [{ type: i0.Output, args: ["pathNavigate"] }], createAction: [{ type: i0.Output, args: ["createAction"] }], created: [{ type: i0.Output, args: ["created"] }], clearSelection: [{ type: i0.Output, args: ["clearSelection"] }], close: [{ type: i0.Output, args: ["close"] }], submit: [{ type: i0.Output, args: ["submit"] }], cancel: [{ type: i0.Output, args: ["cancel"] }] } });
|
|
17790
17893
|
|
|
17791
17894
|
class TnFilePickerComponent {
|
|
17895
|
+
/**
|
|
17896
|
+
* What can be selected: a single mode, or an explicit list of selectable
|
|
17897
|
+
* item types, e.g. ['folder', 'dataset']. Whenever a directory-like type
|
|
17898
|
+
* (folder, dataset, mountpoint) is selectable, an empty selection stands
|
|
17899
|
+
* for the directory being browsed and submitting it picks `currentPath` —
|
|
17900
|
+
* this keeps empty directories selectable.
|
|
17901
|
+
*/
|
|
17792
17902
|
mode = input('any', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
17793
17903
|
multiSelect = input(false, ...(ngDevMode ? [{ debugName: "multiSelect" }] : []));
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
allowZvolCreate = input(false, ...(ngDevMode ? [{ debugName: "allowZvolCreate" }] : []));
|
|
17904
|
+
/** Consumer-defined creation flows shown as buttons in the popup footer. */
|
|
17905
|
+
createActions = input([], ...(ngDevMode ? [{ debugName: "createActions" }] : []));
|
|
17797
17906
|
allowManualInput = input(true, ...(ngDevMode ? [{ debugName: "allowManualInput" }] : []));
|
|
17798
17907
|
placeholder = input('Select file or folder', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
17799
17908
|
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
@@ -17803,12 +17912,14 @@ class TnFilePickerComponent {
|
|
|
17803
17912
|
*/
|
|
17804
17913
|
testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
17805
17914
|
startPath = input('/mnt', ...(ngDevMode ? [{ debugName: "startPath" }] : []));
|
|
17915
|
+
/** Restricts navigation — users cannot navigate above this path. */
|
|
17806
17916
|
rootPath = input(undefined, ...(ngDevMode ? [{ debugName: "rootPath" }] : []));
|
|
17807
17917
|
fileExtensions = input(undefined, ...(ngDevMode ? [{ debugName: "fileExtensions" }] : []));
|
|
17808
17918
|
callbacks = input(undefined, ...(ngDevMode ? [{ debugName: "callbacks" }] : []));
|
|
17809
17919
|
selectionChange = output();
|
|
17810
17920
|
pathChange = output();
|
|
17811
|
-
|
|
17921
|
+
/** Re-emitted from the popup when one of the `createActions` buttons is pressed. */
|
|
17922
|
+
createAction = output();
|
|
17812
17923
|
error = output();
|
|
17813
17924
|
wrapperEl = viewChild.required('wrapper');
|
|
17814
17925
|
filePickerTemplate = viewChild.required('filePickerTemplate');
|
|
@@ -17823,11 +17934,12 @@ class TnFilePickerComponent {
|
|
|
17823
17934
|
selectedItems = signal([], ...(ngDevMode ? [{ debugName: "selectedItems" }] : []));
|
|
17824
17935
|
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
17825
17936
|
hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
17826
|
-
creatingItemTempId = signal(null, ...(ngDevMode ? [{ debugName: "creatingItemTempId" }] : []));
|
|
17827
|
-
creationLoading = signal(false, ...(ngDevMode ? [{ debugName: "creationLoading" }] : []));
|
|
17828
17937
|
formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
|
|
17829
17938
|
// Computed disabled state (combines input and form state)
|
|
17830
17939
|
isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
17940
|
+
// Root the picker is confined to when rootPath is not provided, with
|
|
17941
|
+
// trailing slashes collapsed so confinement checks compare exact paths
|
|
17942
|
+
effectiveRootPath = computed(() => normalizeRootPath(this.rootPath() ?? '/mnt'), ...(ngDevMode ? [{ debugName: "effectiveRootPath" }] : []));
|
|
17831
17943
|
// ControlValueAccessor implementation
|
|
17832
17944
|
onChange = (_value) => { };
|
|
17833
17945
|
onTouched = () => { };
|
|
@@ -17835,8 +17947,7 @@ class TnFilePickerComponent {
|
|
|
17835
17947
|
elementRef = inject(ElementRef);
|
|
17836
17948
|
viewContainerRef = inject(ViewContainerRef);
|
|
17837
17949
|
ngOnInit() {
|
|
17838
|
-
this.currentPath.set(this.startPath());
|
|
17839
|
-
this.selectedPath.set(this.multiSelect() ? '' : '');
|
|
17950
|
+
this.currentPath.set(this.clampToRoot(this.startPath()));
|
|
17840
17951
|
}
|
|
17841
17952
|
ngOnDestroy() {
|
|
17842
17953
|
this.destroy$.next();
|
|
@@ -17866,27 +17977,43 @@ class TnFilePickerComponent {
|
|
|
17866
17977
|
this.formDisabled.set(isDisabled);
|
|
17867
17978
|
}
|
|
17868
17979
|
// Event handlers
|
|
17869
|
-
|
|
17980
|
+
/**
|
|
17981
|
+
* Commits a manually typed path. Bound to the input's `change` event
|
|
17982
|
+
* (Enter or blur), not `input`: validating per keystroke would emit an
|
|
17983
|
+
* error for every incomplete prefix of a valid path ('/m', '/mn', …)
|
|
17984
|
+
* and spam `validatePath` with paths the user never meant.
|
|
17985
|
+
*/
|
|
17986
|
+
onPathCommit(event) {
|
|
17870
17987
|
const target = event.target;
|
|
17871
|
-
const
|
|
17988
|
+
const path = target.value;
|
|
17872
17989
|
if (this.allowManualInput()) {
|
|
17873
|
-
//
|
|
17874
|
-
|
|
17990
|
+
// Clearing the input clears the selection
|
|
17991
|
+
if (!path) {
|
|
17992
|
+
this.onClearSelection();
|
|
17993
|
+
this.onTouched();
|
|
17994
|
+
return;
|
|
17995
|
+
}
|
|
17996
|
+
// Manually typed paths honor the same confinement as navigation
|
|
17997
|
+
if (!isPathWithinRoot(path, this.effectiveRootPath())) {
|
|
17998
|
+
this.emitError('validation', `Path is outside of ${this.effectiveRootPath()}`, path);
|
|
17999
|
+
this.onTouched();
|
|
18000
|
+
return;
|
|
18001
|
+
}
|
|
17875
18002
|
const cb = this.callbacks();
|
|
17876
18003
|
if (cb?.validatePath) {
|
|
17877
|
-
cb.validatePath(
|
|
18004
|
+
cb.validatePath(path).then(isValid => {
|
|
17878
18005
|
if (isValid) {
|
|
17879
|
-
this.updateSelection(
|
|
18006
|
+
this.updateSelection(path);
|
|
17880
18007
|
}
|
|
17881
18008
|
else {
|
|
17882
|
-
this.emitError('validation', `Invalid path: ${
|
|
18009
|
+
this.emitError('validation', `Invalid path: ${path}`, path);
|
|
17883
18010
|
}
|
|
17884
18011
|
}).catch(err => {
|
|
17885
|
-
this.emitError('validation', err.message || 'Path validation failed',
|
|
18012
|
+
this.emitError('validation', err.message || 'Path validation failed', path);
|
|
17886
18013
|
});
|
|
17887
18014
|
}
|
|
17888
18015
|
else {
|
|
17889
|
-
this.updateSelection(
|
|
18016
|
+
this.updateSelection(path);
|
|
17890
18017
|
}
|
|
17891
18018
|
}
|
|
17892
18019
|
this.onTouched();
|
|
@@ -17909,19 +18036,14 @@ class TnFilePickerComponent {
|
|
|
17909
18036
|
}
|
|
17910
18037
|
// File browser methods
|
|
17911
18038
|
onItemClick(item) {
|
|
17912
|
-
if (item.disabled
|
|
18039
|
+
if (item.disabled) {
|
|
17913
18040
|
return;
|
|
17914
18041
|
}
|
|
17915
18042
|
if (this.multiSelect()) {
|
|
17916
18043
|
const selected = this.selectedItems();
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
selected
|
|
17920
|
-
}
|
|
17921
|
-
else {
|
|
17922
|
-
selected.push(item.path);
|
|
17923
|
-
}
|
|
17924
|
-
this.selectedItems.set([...selected]);
|
|
18044
|
+
this.selectedItems.set(selected.includes(item.path)
|
|
18045
|
+
? selected.filter((path) => path !== item.path)
|
|
18046
|
+
: [...selected, item.path]);
|
|
17925
18047
|
}
|
|
17926
18048
|
else {
|
|
17927
18049
|
// Single select - just update selection state, don't apply yet
|
|
@@ -17929,26 +18051,30 @@ class TnFilePickerComponent {
|
|
|
17929
18051
|
}
|
|
17930
18052
|
}
|
|
17931
18053
|
onItemDoubleClick(item) {
|
|
17932
|
-
if (item.isCreating || this.creatingItemTempId()) {
|
|
17933
|
-
return;
|
|
17934
|
-
}
|
|
17935
18054
|
// Define navigatable types
|
|
17936
18055
|
const isNavigatable = ['folder', 'dataset', 'mountpoint'].includes(item.type);
|
|
17937
18056
|
// Allow navigation even if disabled, as long as it's a navigatable type
|
|
17938
18057
|
if (isNavigatable) {
|
|
17939
18058
|
this.navigateToPath(item.path);
|
|
17940
18059
|
}
|
|
17941
|
-
else if (!item.disabled) {
|
|
17942
|
-
// Double-click on selectable item submits immediately
|
|
18060
|
+
else if (!item.disabled && !this.multiSelect()) {
|
|
18061
|
+
// Double-click on a selectable item submits immediately. In
|
|
18062
|
+
// multi-select this shortcut would silently replace the selection the
|
|
18063
|
+
// user built up with the double-clicked item, so it is single-select
|
|
18064
|
+
// only (the two clicks already toggled the checkbox twice — a no-op).
|
|
17943
18065
|
this.selectedItems.set([item.path]);
|
|
17944
18066
|
this.onSubmit();
|
|
17945
18067
|
}
|
|
17946
18068
|
}
|
|
17947
18069
|
onSubmit() {
|
|
17948
18070
|
// Apply the selection and close the popup
|
|
17949
|
-
|
|
18071
|
+
let selected = this.selectedItems();
|
|
17950
18072
|
if (selected.length === 0) {
|
|
17951
|
-
|
|
18073
|
+
if (!allowsCurrentDirectorySelection(this.mode())) {
|
|
18074
|
+
return;
|
|
18075
|
+
}
|
|
18076
|
+
// An empty selection stands for the directory being browsed
|
|
18077
|
+
selected = [this.currentPath()];
|
|
17952
18078
|
}
|
|
17953
18079
|
// Clear any existing error state
|
|
17954
18080
|
this.hasError.set(false);
|
|
@@ -17970,39 +18096,60 @@ class TnFilePickerComponent {
|
|
|
17970
18096
|
this.close();
|
|
17971
18097
|
}
|
|
17972
18098
|
navigateToPath(path) {
|
|
17973
|
-
|
|
17974
|
-
|
|
17975
|
-
|
|
18099
|
+
void this.loadDirectory(this.clampToRoot(path));
|
|
18100
|
+
}
|
|
18101
|
+
/** An inline `create` flow finished — show the new item selected and applied. */
|
|
18102
|
+
onCreated(path) {
|
|
18103
|
+
void this.selectPath(path);
|
|
18104
|
+
}
|
|
18105
|
+
/**
|
|
18106
|
+
* Re-fetches the listing of the directory currently being browsed. Call it
|
|
18107
|
+
* when the directory changed outside the picker, e.g. after a
|
|
18108
|
+
* `createAction` flow created something in it.
|
|
18109
|
+
*/
|
|
18110
|
+
async refresh() {
|
|
18111
|
+
await this.loadDirectory(this.currentPath());
|
|
18112
|
+
}
|
|
18113
|
+
/**
|
|
18114
|
+
* Programmatically selects `path`, e.g. the mountpoint a `createAction`
|
|
18115
|
+
* flow just created: browses to its parent so the refreshed listing shows
|
|
18116
|
+
* the new item selected, and applies the path as the picker's value (form
|
|
18117
|
+
* value and `selectionChange`). The popup stays open so the user can
|
|
18118
|
+
* confirm with Select or keep browsing. Paths outside `rootPath` are
|
|
18119
|
+
* rejected with a `validation` error — the picker's value honors the same
|
|
18120
|
+
* confinement as navigation and manual input.
|
|
18121
|
+
*
|
|
18122
|
+
* This is a single-path operation: like a manually typed path, it
|
|
18123
|
+
* REPLACES any pending multi-selection rather than appending to it —
|
|
18124
|
+
* the intent is to focus the one path (e.g. a just-created item), not to
|
|
18125
|
+
* merge it into picks the user may have abandoned.
|
|
18126
|
+
*/
|
|
18127
|
+
async selectPath(path) {
|
|
18128
|
+
if (!isPathWithinRoot(path, this.effectiveRootPath())) {
|
|
18129
|
+
this.emitError('validation', `Path is outside of ${this.effectiveRootPath()}`, path);
|
|
17976
18130
|
return;
|
|
17977
18131
|
}
|
|
17978
|
-
|
|
18132
|
+
// The parent still needs clamping: the root's own parent is outside it
|
|
18133
|
+
const parent = path.substring(0, path.lastIndexOf('/'));
|
|
18134
|
+
await this.loadDirectory(this.clampToRoot(parent || this.effectiveRootPath()));
|
|
18135
|
+
this.updateSelection(path);
|
|
17979
18136
|
}
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
18137
|
+
clampToRoot(path) {
|
|
18138
|
+
const root = this.effectiveRootPath();
|
|
18139
|
+
return isPathWithinRoot(path, root) ? path : root;
|
|
18140
|
+
}
|
|
18141
|
+
/**
|
|
18142
|
+
* Browsing to another directory drops a not-yet-applied single-select
|
|
18143
|
+
* selection: keeping it would leave the footer claiming an item from a
|
|
18144
|
+
* directory the user is no longer looking at, and would mask the implicit
|
|
18145
|
+
* current-directory selection. Multi-select keeps its selection so items
|
|
18146
|
+
* can be picked across directories. The applied value (`selectedPath`,
|
|
18147
|
+
* form value) is untouched either way.
|
|
18148
|
+
*/
|
|
18149
|
+
clearStalePendingSelection(directoryChanged) {
|
|
18150
|
+
if (directoryChanged && !this.multiSelect()) {
|
|
18151
|
+
this.selectedItems.set([]);
|
|
17985
18152
|
}
|
|
17986
|
-
// Generate temporary ID
|
|
17987
|
-
const tempId = `temp-${Date.now()}`;
|
|
17988
|
-
// Create pending item
|
|
17989
|
-
const pendingFolder = {
|
|
17990
|
-
path: `${this.currentPath()}/__pending__/${tempId}`,
|
|
17991
|
-
name: 'New Folder',
|
|
17992
|
-
type: 'folder',
|
|
17993
|
-
isCreating: true,
|
|
17994
|
-
tempId: tempId,
|
|
17995
|
-
modified: new Date()
|
|
17996
|
-
};
|
|
17997
|
-
// Add to top of file list
|
|
17998
|
-
const currentItems = this.fileItems();
|
|
17999
|
-
this.fileItems.set([pendingFolder, ...currentItems]);
|
|
18000
|
-
this.creatingItemTempId.set(tempId);
|
|
18001
|
-
// Still emit event for parent components
|
|
18002
|
-
this.createFolder.emit({
|
|
18003
|
-
parentPath: this.currentPath(),
|
|
18004
|
-
folderName: 'New Folder'
|
|
18005
|
-
});
|
|
18006
18153
|
}
|
|
18007
18154
|
onClearSelection() {
|
|
18008
18155
|
this.selectedItems.set([]);
|
|
@@ -18010,92 +18157,14 @@ class TnFilePickerComponent {
|
|
|
18010
18157
|
this.onChange(this.multiSelect() ? [] : '');
|
|
18011
18158
|
this.selectionChange.emit(this.multiSelect() ? [] : '');
|
|
18012
18159
|
}
|
|
18013
|
-
async onSubmitFolderName(name, tempId) {
|
|
18014
|
-
// Validate folder name
|
|
18015
|
-
const validation = this.validateFolderName(name);
|
|
18016
|
-
if (!validation.valid) {
|
|
18017
|
-
// Update the item with error message
|
|
18018
|
-
this.updateCreatingItemError(tempId, validation.error);
|
|
18019
|
-
return;
|
|
18020
|
-
}
|
|
18021
|
-
const cb = this.callbacks();
|
|
18022
|
-
if (!cb?.createFolder) {
|
|
18023
|
-
this.updateCreatingItemError(tempId, 'Create folder callback not provided');
|
|
18024
|
-
return;
|
|
18025
|
-
}
|
|
18026
|
-
// Clear any previous errors
|
|
18027
|
-
this.updateCreatingItemError(tempId, undefined);
|
|
18028
|
-
this.creationLoading.set(true);
|
|
18029
|
-
try {
|
|
18030
|
-
// Call the callback with parent path and user-entered name
|
|
18031
|
-
await cb.createFolder(this.currentPath(), name.trim());
|
|
18032
|
-
// Remove pending item
|
|
18033
|
-
this.removePendingItem(tempId);
|
|
18034
|
-
this.creatingItemTempId.set(null);
|
|
18035
|
-
// Reload directory to show the newly created folder
|
|
18036
|
-
await this.loadDirectory(this.currentPath());
|
|
18037
|
-
}
|
|
18038
|
-
catch (err) {
|
|
18039
|
-
console.error('Failed to create folder:', err);
|
|
18040
|
-
// Show error inline, keep input editable for retry
|
|
18041
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to create folder';
|
|
18042
|
-
this.updateCreatingItemError(tempId, errorMessage);
|
|
18043
|
-
this.emitError('creation', errorMessage, this.currentPath());
|
|
18044
|
-
}
|
|
18045
|
-
finally {
|
|
18046
|
-
this.creationLoading.set(false);
|
|
18047
|
-
}
|
|
18048
|
-
}
|
|
18049
|
-
onCancelFolderCreation(tempId) {
|
|
18050
|
-
this.removePendingItem(tempId);
|
|
18051
|
-
this.creatingItemTempId.set(null);
|
|
18052
|
-
this.creationLoading.set(false);
|
|
18053
|
-
}
|
|
18054
|
-
removePendingItem(tempId) {
|
|
18055
|
-
const items = this.fileItems().filter(item => item.tempId !== tempId);
|
|
18056
|
-
this.fileItems.set(items);
|
|
18057
|
-
}
|
|
18058
|
-
updateCreatingItemError(tempId, error) {
|
|
18059
|
-
const items = this.fileItems().map(item => {
|
|
18060
|
-
if (item.tempId === tempId) {
|
|
18061
|
-
return { ...item, creationError: error };
|
|
18062
|
-
}
|
|
18063
|
-
return item;
|
|
18064
|
-
});
|
|
18065
|
-
this.fileItems.set(items);
|
|
18066
|
-
}
|
|
18067
|
-
validateFolderName(name) {
|
|
18068
|
-
const trimmed = name.trim();
|
|
18069
|
-
if (!trimmed) {
|
|
18070
|
-
return { valid: false, error: 'Folder name cannot be empty' };
|
|
18071
|
-
}
|
|
18072
|
-
if (trimmed.length > 255) {
|
|
18073
|
-
return { valid: false, error: 'Folder name too long (max 255 characters)' };
|
|
18074
|
-
}
|
|
18075
|
-
// Check for invalid characters (common across file systems)
|
|
18076
|
-
const invalidChars = /[<>:"|?*\x00-\x1f]/;
|
|
18077
|
-
if (invalidChars.test(trimmed)) {
|
|
18078
|
-
return { valid: false, error: 'Invalid characters in folder name' };
|
|
18079
|
-
}
|
|
18080
|
-
// Disallow folder names that are just dots
|
|
18081
|
-
if (/^\.+$/.test(trimmed)) {
|
|
18082
|
-
return { valid: false, error: 'Invalid folder name' };
|
|
18083
|
-
}
|
|
18084
|
-
// Check for duplicate names in current directory
|
|
18085
|
-
const existingNames = this.fileItems()
|
|
18086
|
-
.filter(item => !item.isCreating)
|
|
18087
|
-
.map(item => item.name.toLowerCase());
|
|
18088
|
-
if (existingNames.includes(trimmed.toLowerCase())) {
|
|
18089
|
-
return { valid: false, error: 'A folder with this name already exists' };
|
|
18090
|
-
}
|
|
18091
|
-
return { valid: true };
|
|
18092
|
-
}
|
|
18093
18160
|
async loadDirectory(path) {
|
|
18161
|
+
const directoryChanged = path !== this.currentPath();
|
|
18094
18162
|
const cb = this.callbacks();
|
|
18095
18163
|
if (!cb?.getChildren) {
|
|
18096
18164
|
// Default mock data for development
|
|
18097
18165
|
this.fileItems.set(this.getMockFileItems(path));
|
|
18098
18166
|
this.currentPath.set(path);
|
|
18167
|
+
this.clearStalePendingSelection(directoryChanged);
|
|
18099
18168
|
this.pathChange.emit(path);
|
|
18100
18169
|
return;
|
|
18101
18170
|
}
|
|
@@ -18104,10 +18173,10 @@ class TnFilePickerComponent {
|
|
|
18104
18173
|
const items = await cb.getChildren(path);
|
|
18105
18174
|
this.fileItems.set(items || []);
|
|
18106
18175
|
this.currentPath.set(path);
|
|
18176
|
+
this.clearStalePendingSelection(directoryChanged);
|
|
18107
18177
|
this.pathChange.emit(path);
|
|
18108
18178
|
}
|
|
18109
18179
|
catch (err) {
|
|
18110
|
-
console.error('❌ Error loading directory:', err);
|
|
18111
18180
|
this.emitError('navigation', err instanceof Error ? err.message : 'Failed to load directory', path);
|
|
18112
18181
|
}
|
|
18113
18182
|
finally {
|
|
@@ -18144,6 +18213,12 @@ class TnFilePickerComponent {
|
|
|
18144
18213
|
}
|
|
18145
18214
|
];
|
|
18146
18215
|
}
|
|
18216
|
+
/**
|
|
18217
|
+
* Applies `path` as the picker's whole value. In multi-select this
|
|
18218
|
+
* replaces the selection with the single path — both callers (typed
|
|
18219
|
+
* paths, `selectPath()`) express one deliberate choice, so appending
|
|
18220
|
+
* would be the surprising behavior.
|
|
18221
|
+
*/
|
|
18147
18222
|
updateSelection(path) {
|
|
18148
18223
|
// Clear any existing error state since popup selections are valid
|
|
18149
18224
|
this.hasError.set(false);
|
|
@@ -18160,26 +18235,6 @@ class TnFilePickerComponent {
|
|
|
18160
18235
|
}
|
|
18161
18236
|
this.selectionChange.emit(this.multiSelect() ? this.selectedItems() : path);
|
|
18162
18237
|
}
|
|
18163
|
-
updateSelectionFromItems() {
|
|
18164
|
-
// Clear any existing error state since popup selections are valid
|
|
18165
|
-
this.hasError.set(false);
|
|
18166
|
-
const selected = this.selectedItems();
|
|
18167
|
-
this.selectedPath.set(selected.join(', '));
|
|
18168
|
-
this.onChange(this.multiSelect() ? selected : selected[0] || '');
|
|
18169
|
-
this.selectionChange.emit(this.multiSelect() ? selected : selected[0] || '');
|
|
18170
|
-
}
|
|
18171
|
-
toFullPath(displayPath) {
|
|
18172
|
-
if (!displayPath) {
|
|
18173
|
-
return '/mnt';
|
|
18174
|
-
}
|
|
18175
|
-
if (displayPath === '/') {
|
|
18176
|
-
return '/mnt';
|
|
18177
|
-
}
|
|
18178
|
-
if (displayPath.startsWith('/')) {
|
|
18179
|
-
return '/mnt' + displayPath;
|
|
18180
|
-
}
|
|
18181
|
-
return '/mnt/' + displayPath;
|
|
18182
|
-
}
|
|
18183
18238
|
emitError(type, message, path) {
|
|
18184
18239
|
this.hasError.set(true);
|
|
18185
18240
|
this.error.emit({ type, message, path });
|
|
@@ -18239,13 +18294,13 @@ class TnFilePickerComponent {
|
|
|
18239
18294
|
this.overlayRef.attach(this.portal);
|
|
18240
18295
|
}
|
|
18241
18296
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18242
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.0", type: TnFilePickerComponent, isStandalone: true, selector: "tn-file-picker", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null },
|
|
18297
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.0", type: TnFilePickerComponent, isStandalone: true, selector: "tn-file-picker", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, createActions: { classPropertyName: "createActions", publicName: "createActions", isSignal: true, isRequired: false, transformFunction: null }, allowManualInput: { classPropertyName: "allowManualInput", publicName: "allowManualInput", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, startPath: { classPropertyName: "startPath", publicName: "startPath", isSignal: true, isRequired: false, transformFunction: null }, rootPath: { classPropertyName: "rootPath", publicName: "rootPath", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null }, callbacks: { classPropertyName: "callbacks", publicName: "callbacks", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", pathChange: "pathChange", createAction: "createAction", error: "error" }, host: { properties: { "class.error": "hasError()" }, classAttribute: "tn-file-picker" }, providers: [
|
|
18243
18298
|
{
|
|
18244
18299
|
provide: NG_VALUE_ACCESSOR,
|
|
18245
18300
|
useExisting: forwardRef(() => TnFilePickerComponent),
|
|
18246
18301
|
multi: true
|
|
18247
18302
|
}
|
|
18248
|
-
], viewQueries: [{ propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }, { propertyName: "filePickerTemplate", first: true, predicate: ["filePickerTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-file-picker-container\" tnTestIdType=\"file-picker\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-file-picker-wrapper\" style=\"padding-right: 40px;\">\n <input\n type=\"text\"\n class=\"tn-file-picker-input\"\n [class.error]=\"hasError()\"\n [value]=\"selectedPath()
|
|
18303
|
+
], viewQueries: [{ propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }, { propertyName: "filePickerTemplate", first: true, predicate: ["filePickerTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-file-picker-container\" tnTestIdType=\"file-picker\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-file-picker-wrapper\" style=\"padding-right: 40px;\">\n <input\n type=\"text\"\n class=\"tn-file-picker-input\"\n [class.error]=\"hasError()\"\n [value]=\"selectedPath()\"\n [placeholder]=\"placeholder()\"\n [readonly]=\"!allowManualInput()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onPathCommit($event)\">\n\n <button\n type=\"button\"\n class=\"tn-file-picker-toggle\"\n aria-label=\"Open file picker\"\n [disabled]=\"isDisabled()\"\n (click)=\"openFilePicker()\">\n <tn-icon name=\"folder\" library=\"mdi\" />\n </button>\n </div>\n \n <ng-template #filePickerTemplate>\n <tn-file-picker-popup\n class=\"tn-file-picker-popup\"\n [mode]=\"mode()\"\n [multiSelect]=\"multiSelect()\"\n [createActions]=\"createActions()\"\n [currentPath]=\"currentPath()\"\n [rootPath]=\"effectiveRootPath()\"\n [fileItems]=\"fileItems()\"\n [selectedItems]=\"selectedItems()\"\n [loading]=\"loading()\"\n [fileExtensions]=\"fileExtensions()\"\n (itemClick)=\"onItemClick($event)\"\n (itemDoubleClick)=\"onItemDoubleClick($event)\"\n (pathNavigate)=\"navigateToPath($event)\"\n (createAction)=\"createAction.emit($event)\"\n (created)=\"onCreated($event)\"\n (clearSelection)=\"onClearSelection()\"\n (submit)=\"onSubmit()\"\n (cancel)=\"onCancel()\"\n (close)=\"close()\" />\n </ng-template>\n</div>", styles: [":host{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-file-picker-container{position:relative;display:flex;align-items:center;width:100%}.tn-file-picker-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-file-picker-input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;outline:none;box-sizing:border-box;font-family:inherit}.tn-file-picker-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-file-picker-input:focus{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-file-picker-input:disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-file-picker-input.error{border-color:var(--tn-error, #dc3545)}.tn-file-picker-input.error:focus{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-file-picker-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;color:var(--tn-fg1);border-radius:4px}.tn-file-picker-toggle:hover{background:var(--tn-bg2, #f0f0f0)}.tn-file-picker-toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-file-picker-toggle:disabled{cursor:not-allowed;opacity:.5}.tn-file-picker-toggle tn-icon{font-size:var(--tn-icon-md, 20px)}:host:focus-within .tn-file-picker-input{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}:host.error .tn-file-picker-input{border-color:var(--tn-error, #dc3545)}:host.error .tn-file-picker-input:focus{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}@media(prefers-reduced-motion:reduce){.tn-file-picker-input,.tn-file-picker-toggle,.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){.tn-file-picker-input{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host ::ng-deep .tn-file-picker-overlay .tn-file-picker-dialog{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnFilePickerPopupComponent, selector: "tn-file-picker-popup", inputs: ["mode", "multiSelect", "createActions", "currentPath", "rootPath", "fileItems", "selectedItems", "loading", "fileExtensions"], outputs: ["itemClick", "itemDoubleClick", "pathNavigate", "createAction", "created", "clearSelection", "close", "submit", "cancel"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
18249
18304
|
}
|
|
18250
18305
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerComponent, decorators: [{
|
|
18251
18306
|
type: Component,
|
|
@@ -18256,7 +18311,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
18256
18311
|
OverlayModule,
|
|
18257
18312
|
PortalModule,
|
|
18258
18313
|
A11yModule,
|
|
18259
|
-
StripMntPrefixPipe,
|
|
18260
18314
|
TnTestIdDirective
|
|
18261
18315
|
], providers: [
|
|
18262
18316
|
{
|
|
@@ -18267,8 +18321,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
18267
18321
|
], host: {
|
|
18268
18322
|
'class': 'tn-file-picker',
|
|
18269
18323
|
'[class.error]': 'hasError()'
|
|
18270
|
-
}, template: "<div class=\"tn-file-picker-container\" tnTestIdType=\"file-picker\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-file-picker-wrapper\" style=\"padding-right: 40px;\">\n <input\n type=\"text\"\n class=\"tn-file-picker-input\"\n [class.error]=\"hasError()\"\n [value]=\"selectedPath()
|
|
18271
|
-
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], multiSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSelect", required: false }] }],
|
|
18324
|
+
}, template: "<div class=\"tn-file-picker-container\" tnTestIdType=\"file-picker\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-file-picker-wrapper\" style=\"padding-right: 40px;\">\n <input\n type=\"text\"\n class=\"tn-file-picker-input\"\n [class.error]=\"hasError()\"\n [value]=\"selectedPath()\"\n [placeholder]=\"placeholder()\"\n [readonly]=\"!allowManualInput()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onPathCommit($event)\">\n\n <button\n type=\"button\"\n class=\"tn-file-picker-toggle\"\n aria-label=\"Open file picker\"\n [disabled]=\"isDisabled()\"\n (click)=\"openFilePicker()\">\n <tn-icon name=\"folder\" library=\"mdi\" />\n </button>\n </div>\n \n <ng-template #filePickerTemplate>\n <tn-file-picker-popup\n class=\"tn-file-picker-popup\"\n [mode]=\"mode()\"\n [multiSelect]=\"multiSelect()\"\n [createActions]=\"createActions()\"\n [currentPath]=\"currentPath()\"\n [rootPath]=\"effectiveRootPath()\"\n [fileItems]=\"fileItems()\"\n [selectedItems]=\"selectedItems()\"\n [loading]=\"loading()\"\n [fileExtensions]=\"fileExtensions()\"\n (itemClick)=\"onItemClick($event)\"\n (itemDoubleClick)=\"onItemDoubleClick($event)\"\n (pathNavigate)=\"navigateToPath($event)\"\n (createAction)=\"createAction.emit($event)\"\n (created)=\"onCreated($event)\"\n (clearSelection)=\"onClearSelection()\"\n (submit)=\"onSubmit()\"\n (cancel)=\"onCancel()\"\n (close)=\"close()\" />\n </ng-template>\n</div>", styles: [":host{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-file-picker-container{position:relative;display:flex;align-items:center;width:100%}.tn-file-picker-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-file-picker-input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;outline:none;box-sizing:border-box;font-family:inherit}.tn-file-picker-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-file-picker-input:focus{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-file-picker-input:disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-file-picker-input.error{border-color:var(--tn-error, #dc3545)}.tn-file-picker-input.error:focus{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-file-picker-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;color:var(--tn-fg1);border-radius:4px}.tn-file-picker-toggle:hover{background:var(--tn-bg2, #f0f0f0)}.tn-file-picker-toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-file-picker-toggle:disabled{cursor:not-allowed;opacity:.5}.tn-file-picker-toggle tn-icon{font-size:var(--tn-icon-md, 20px)}:host:focus-within .tn-file-picker-input{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}:host.error .tn-file-picker-input{border-color:var(--tn-error, #dc3545)}:host.error .tn-file-picker-input:focus{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}@media(prefers-reduced-motion:reduce){.tn-file-picker-input,.tn-file-picker-toggle,.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){.tn-file-picker-input{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host ::ng-deep .tn-file-picker-overlay .tn-file-picker-dialog{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"] }]
|
|
18325
|
+
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], multiSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSelect", required: false }] }], createActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "createActions", required: false }] }], allowManualInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowManualInput", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], startPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPath", required: false }] }], rootPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootPath", required: false }] }], fileExtensions: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileExtensions", required: false }] }], callbacks: [{ type: i0.Input, args: [{ isSignal: true, alias: "callbacks", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], pathChange: [{ type: i0.Output, args: ["pathChange"] }], createAction: [{ type: i0.Output, args: ["createAction"] }], error: [{ type: i0.Output, args: ["error"] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }], filePickerTemplate: [{ type: i0.ViewChild, args: ['filePickerTemplate', { isSignal: true }] }] } });
|
|
18272
18326
|
|
|
18273
18327
|
/**
|
|
18274
18328
|
* Harness for interacting with tn-empty in tests.
|
|
@@ -19231,5 +19285,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
19231
19285
|
* Generated bundle index. Do not edit.
|
|
19232
19286
|
*/
|
|
19233
19287
|
|
|
19234
|
-
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder,
|
|
19288
|
+
export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeDataSource, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, allowsCurrentDirectorySelection, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, getSelectableTypes, injectTnFormFieldAria, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
|
|
19235
19289
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|