@truenas/ui-components 0.4.1 → 0.4.3
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.
|
@@ -6108,6 +6108,12 @@ class TnSlideToggleComponent {
|
|
|
6108
6108
|
this.formDisabled.set(isDisabled);
|
|
6109
6109
|
}
|
|
6110
6110
|
onToggleChange(event) {
|
|
6111
|
+
// The bubbling native change would reach ancestor (change) bindings in
|
|
6112
|
+
// addition to the component's `change` output — Ivy invokes the binding for
|
|
6113
|
+
// both, firing every listener twice per toggle. The output is the single
|
|
6114
|
+
// public event, so the native event stops here. (This component always
|
|
6115
|
+
// stopped it — unlike checkbox/radio it was never affected — the comment
|
|
6116
|
+
// and the regression spec keep it that way.)
|
|
6111
6117
|
event.stopPropagation();
|
|
6112
6118
|
const target = event.target;
|
|
6113
6119
|
const checked = target.checked;
|
|
@@ -6559,6 +6565,11 @@ class TnCheckboxComponent {
|
|
|
6559
6565
|
this.formDisabled.set(isDisabled);
|
|
6560
6566
|
}
|
|
6561
6567
|
onCheckboxChange(event) {
|
|
6568
|
+
// The bubbling native change would reach ancestor (change) bindings in
|
|
6569
|
+
// addition to the component's `change` output — Ivy invokes the binding for
|
|
6570
|
+
// both, firing every listener twice per toggle. The output is the single
|
|
6571
|
+
// public event, so the native event stops here.
|
|
6572
|
+
event.stopPropagation();
|
|
6562
6573
|
const target = event.target;
|
|
6563
6574
|
const checked = target.checked;
|
|
6564
6575
|
this.internalChecked.set(checked);
|
|
@@ -6852,6 +6863,11 @@ class TnRadioComponent {
|
|
|
6852
6863
|
this.formDisabled.set(isDisabled);
|
|
6853
6864
|
}
|
|
6854
6865
|
onRadioChange(event) {
|
|
6866
|
+
// The bubbling native change would reach ancestor (change) bindings in
|
|
6867
|
+
// addition to the component's `change` output — Ivy invokes the binding for
|
|
6868
|
+
// both, firing every listener twice per toggle. The output is the single
|
|
6869
|
+
// public event, so the native event stops here.
|
|
6870
|
+
event.stopPropagation();
|
|
6855
6871
|
const target = event.target;
|
|
6856
6872
|
this.checked = target.checked;
|
|
6857
6873
|
if (target.checked) {
|
|
@@ -17567,6 +17583,14 @@ class TnFilePickerPopupComponent {
|
|
|
17567
17583
|
selectedItems = input([], ...(ngDevMode ? [{ debugName: "selectedItems" }] : []));
|
|
17568
17584
|
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
17569
17585
|
fileExtensions = input(undefined, ...(ngDevMode ? [{ debugName: "fileExtensions" }] : []));
|
|
17586
|
+
/**
|
|
17587
|
+
* Test-id base the owning picker resolved (explicit `testId`, else the bound
|
|
17588
|
+
* control's name). Scopes every derived id inside the popup so ids stay
|
|
17589
|
+
* unique across pickers; with no base the ids fall back to their bare role
|
|
17590
|
+
* (`button-select`, `option-<name>`), which stays addressable because only
|
|
17591
|
+
* one popup can be open at a time.
|
|
17592
|
+
*/
|
|
17593
|
+
testIdBase = input(undefined, ...(ngDevMode ? [{ debugName: "testIdBase" }] : []));
|
|
17570
17594
|
iconRegistry = inject(TnIconRegistryService);
|
|
17571
17595
|
elementRef = inject(ElementRef);
|
|
17572
17596
|
constructor() {
|
|
@@ -17634,6 +17658,49 @@ class TnFilePickerPopupComponent {
|
|
|
17634
17658
|
* selection.
|
|
17635
17659
|
*/
|
|
17636
17660
|
allowCurrentDirectorySelection = computed(() => allowsCurrentDirectorySelection(this.mode()), ...(ngDevMode ? [{ debugName: "allowCurrentDirectorySelection" }] : []));
|
|
17661
|
+
/**
|
|
17662
|
+
* The `testIdBase` normalized to a flat segment array for role-first ids.
|
|
17663
|
+
* Nothing is dropped here — `composeTestId` (via `[tnTestId]`) filters falsy
|
|
17664
|
+
* segments, so an unset base collapses to the bare role.
|
|
17665
|
+
*/
|
|
17666
|
+
baseSegments = computed(() => {
|
|
17667
|
+
const base = this.testIdBase();
|
|
17668
|
+
return Array.isArray(base) ? base : [base];
|
|
17669
|
+
}, ...(ngDevMode ? [{ debugName: "baseSegments" }] : []));
|
|
17670
|
+
/**
|
|
17671
|
+
* Role-first segments for the footer chrome, mirroring tn-dialog-shell's
|
|
17672
|
+
* `button-close-<base>` convention: `button-select[-<base>]` and
|
|
17673
|
+
* `button-clear-selection[-<base>]`.
|
|
17674
|
+
*/
|
|
17675
|
+
selectTestId = computed(() => ['select', ...this.baseSegments()], ...(ngDevMode ? [{ debugName: "selectTestId" }] : []));
|
|
17676
|
+
clearSelectionTestId = computed(() => ['clear-selection', ...this.baseSegments()], ...(ngDevMode ? [{ debugName: "clearSelectionTestId" }] : []));
|
|
17677
|
+
/** Role-first segments for the inline creation input: `input-create[-<base>]`. */
|
|
17678
|
+
inlineCreateTestId = computed(() => ['create', ...this.baseSegments()], ...(ngDevMode ? [{ debugName: "inlineCreateTestId" }] : []));
|
|
17679
|
+
/**
|
|
17680
|
+
* Base-first segments for a listed item's row (`option[-<base>]-<name>`) and
|
|
17681
|
+
* its multi-select checkbox (`checkbox[-<base>]-<name>`). The item NAME is
|
|
17682
|
+
* the discriminator (not the full path): it is unique within the listed
|
|
17683
|
+
* directory and keeps ids stable as the user browses.
|
|
17684
|
+
*/
|
|
17685
|
+
itemTestId(item) {
|
|
17686
|
+
return scopeTestId(this.testIdBase(), item.name);
|
|
17687
|
+
}
|
|
17688
|
+
/** Role-first segments for an item's navigation chevron: `button-navigate[-<base>]-<name>`. */
|
|
17689
|
+
navigateTestId(item) {
|
|
17690
|
+
return ['navigate', ...this.baseSegments(), item.name];
|
|
17691
|
+
}
|
|
17692
|
+
/**
|
|
17693
|
+
* Role-first segments for a breadcrumb segment: `button-breadcrumb[-<base>]-<name>`.
|
|
17694
|
+
* The truncation ellipsis normalizes to nothing, leaving that segment at
|
|
17695
|
+
* `button-breadcrumb[-<base>]` — still unique, as at most one is rendered.
|
|
17696
|
+
*/
|
|
17697
|
+
breadcrumbTestId(name) {
|
|
17698
|
+
return ['breadcrumb', ...this.baseSegments(), name];
|
|
17699
|
+
}
|
|
17700
|
+
/** Base-first segments for a consumer-defined create action: `button[-<base>]-<action id>`. */
|
|
17701
|
+
createActionTestId(action) {
|
|
17702
|
+
return scopeTestId(this.testIdBase(), action.id);
|
|
17703
|
+
}
|
|
17637
17704
|
filteredFileItems = computed(() => {
|
|
17638
17705
|
const items = this.fileItems();
|
|
17639
17706
|
const extensions = this.fileExtensions();
|
|
@@ -17870,7 +17937,7 @@ class TnFilePickerPopupComponent {
|
|
|
17870
17937
|
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${timePart}`;
|
|
17871
17938
|
}
|
|
17872
17939
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
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 [class.nameless]=\"!segment.name\"\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-breadcrumb .breadcrumb-segment.nameless:not(:last-child):after{content:none}.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" }] });
|
|
17940
|
+
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 }, testIdBase: { classPropertyName: "testIdBase", publicName: "testIdBase", 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 tnTestIdType=\"button\"\n [tnTestId]=\"breadcrumbTestId(segment.name)\"\n [class.current]=\"last\"\n [class.nameless]=\"!segment.name\"\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 [testId]=\"itemTestId(item)\"\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 tnTestIdType=\"input\"\n [tnTestId]=\"inlineCreateTestId()\"\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 tnTestIdType=\"option\"\n [tnTestId]=\"itemTestId(item)\"\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 tnTestIdType=\"button\"\n [tnTestId]=\"navigateTestId(item)\"\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 [testId]=\"clearSelectionTestId()\"\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 [testId]=\"createActionTestId(action)\"\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 [testId]=\"selectTestId()\"\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-breadcrumb .breadcrumb-segment.nameless:not(:last-child):after{content:none}.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: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
|
|
17874
17941
|
}
|
|
17875
17942
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, decorators: [{
|
|
17876
17943
|
type: Component,
|
|
@@ -17885,11 +17952,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
17885
17952
|
ScrollingModule,
|
|
17886
17953
|
A11yModule,
|
|
17887
17954
|
FileSizePipe,
|
|
17888
|
-
TruncatePathPipe
|
|
17955
|
+
TruncatePathPipe,
|
|
17956
|
+
TnTestIdDirective
|
|
17889
17957
|
], host: {
|
|
17890
17958
|
'class': 'tn-file-picker-popup'
|
|
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 [class.nameless]=\"!segment.name\"\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-breadcrumb .breadcrumb-segment.nameless:not(:last-child):after{content:none}.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"] }] } });
|
|
17959
|
+
}, 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 tnTestIdType=\"button\"\n [tnTestId]=\"breadcrumbTestId(segment.name)\"\n [class.current]=\"last\"\n [class.nameless]=\"!segment.name\"\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 [testId]=\"itemTestId(item)\"\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 tnTestIdType=\"input\"\n [tnTestId]=\"inlineCreateTestId()\"\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 tnTestIdType=\"option\"\n [tnTestId]=\"itemTestId(item)\"\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 tnTestIdType=\"button\"\n [tnTestId]=\"navigateTestId(item)\"\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 [testId]=\"clearSelectionTestId()\"\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 [testId]=\"createActionTestId(action)\"\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 [testId]=\"selectTestId()\"\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-breadcrumb .breadcrumb-segment.nameless:not(:last-child):after{content:none}.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"] }]
|
|
17960
|
+
}], 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 }] }], testIdBase: [{ type: i0.Input, args: [{ isSignal: true, alias: "testIdBase", 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"] }] } });
|
|
17893
17961
|
|
|
17894
17962
|
class TnFilePickerComponent {
|
|
17895
17963
|
/**
|
|
@@ -17918,13 +17986,27 @@ class TnFilePickerComponent {
|
|
|
17918
17986
|
placeholder = input('Select file or folder', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
17919
17987
|
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
17920
17988
|
/**
|
|
17921
|
-
* Test-id applied to the file-picker container
|
|
17922
|
-
*
|
|
17989
|
+
* Test-id base applied to the file-picker container (`file-picker-` prefixed)
|
|
17990
|
+
* and derived onto the inner chrome: the path input (`input-<base>`), the
|
|
17991
|
+
* browse toggle (`button-toggle-<base>`), and the popup and its contents.
|
|
17992
|
+
* Falls back to the bound control's name (`formControlName` etc.) when unset.
|
|
17993
|
+
* Rendered under whichever attribute name is configured via `TN_TEST_ATTR`
|
|
17994
|
+
* (default `data-testid`).
|
|
17923
17995
|
*/
|
|
17924
17996
|
testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
17925
17997
|
startPath = input('/mnt', ...(ngDevMode ? [{ debugName: "startPath" }] : []));
|
|
17926
17998
|
/** Restricts navigation — users cannot navigate above this path. */
|
|
17927
17999
|
rootPath = input(undefined, ...(ngDevMode ? [{ debugName: "rootPath" }] : []));
|
|
18000
|
+
/**
|
|
18001
|
+
* Root against which the picker's VALUE is expressed — the form value,
|
|
18002
|
+
* `selectionChange` payloads, and the text shown in the input. Browsing
|
|
18003
|
+
* (`rootPath`, `startPath`, `callbacks`, `selectPath`) keeps absolute paths;
|
|
18004
|
+
* the mapping applies only at the value boundary. E.g. with
|
|
18005
|
+
* `valueRoot="/mnt"`, selecting `/mnt/tank/child` shows and emits the
|
|
18006
|
+
* dataset name `tank/child` instead of the mountpoint path. Typed input is
|
|
18007
|
+
* interpreted in the same value space (absolute paths still pass through).
|
|
18008
|
+
*/
|
|
18009
|
+
valueRoot = input(undefined, ...(ngDevMode ? [{ debugName: "valueRoot" }] : []));
|
|
17928
18010
|
fileExtensions = input(undefined, ...(ngDevMode ? [{ debugName: "fileExtensions" }] : []));
|
|
17929
18011
|
callbacks = input(undefined, ...(ngDevMode ? [{ debugName: "callbacks" }] : []));
|
|
17930
18012
|
selectionChange = output();
|
|
@@ -17948,6 +18030,21 @@ class TnFilePickerComponent {
|
|
|
17948
18030
|
formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
|
|
17949
18031
|
// Computed disabled state (combines input and form state)
|
|
17950
18032
|
isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
18033
|
+
/** Explicit `testId` wins; otherwise the bound control's name (formControlName etc.). */
|
|
18034
|
+
resolvedTestId = controlTestId(this.testId);
|
|
18035
|
+
/**
|
|
18036
|
+
* Role-first test-id segments for the browse toggle: `button-toggle-<base>`.
|
|
18037
|
+
* Gated on a usable base — with several pickers on a page a bare
|
|
18038
|
+
* `button-toggle` on each would be non-unique, so unidentified pickers
|
|
18039
|
+
* stay attribute-free (mirroring tn-tree's toggle derivation).
|
|
18040
|
+
*/
|
|
18041
|
+
toggleTestId = computed(() => {
|
|
18042
|
+
const base = this.resolvedTestId();
|
|
18043
|
+
if (composeTestId(undefined, base) === '') {
|
|
18044
|
+
return undefined;
|
|
18045
|
+
}
|
|
18046
|
+
return scopeTestId('toggle', ...(Array.isArray(base) ? base : [base]));
|
|
18047
|
+
}, ...(ngDevMode ? [{ debugName: "toggleTestId" }] : []));
|
|
17951
18048
|
// Root the picker is confined to when rootPath is not provided, with
|
|
17952
18049
|
// trailing slashes collapsed so confinement checks compare exact paths
|
|
17953
18050
|
effectiveRootPath = computed(() => normalizeRootPath(this.rootPath() ?? '/mnt'), ...(ngDevMode ? [{ debugName: "effectiveRootPath" }] : []));
|
|
@@ -17967,15 +18064,26 @@ class TnFilePickerComponent {
|
|
|
17967
18064
|
}
|
|
17968
18065
|
// ControlValueAccessor implementation
|
|
17969
18066
|
writeValue(value) {
|
|
18067
|
+
// The display is canonicalized into value space by round-tripping through
|
|
18068
|
+
// the mapping: a consumer may write an absolute path while valueRoot is
|
|
18069
|
+
// set (e.g. /mnt/tank with valueRoot="/mnt"), and echoing it verbatim
|
|
18070
|
+
// would make the field silently flip to the canonical form (tank) on the
|
|
18071
|
+
// next submit. Selection state keeps internal absolute paths either way;
|
|
18072
|
+
// paths outside the value root round-trip unchanged.
|
|
17970
18073
|
if (this.multiSelect()) {
|
|
17971
|
-
|
|
17972
|
-
|
|
17973
|
-
this.
|
|
18074
|
+
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
18075
|
+
const items = values.map(entry => this.fromValueSpace(entry));
|
|
18076
|
+
this.selectedItems.set(items);
|
|
18077
|
+
// An entry equal to the value root maps to '' and leaves a stray comma
|
|
18078
|
+
// in the join — accepted, per the documented "the value root is not a
|
|
18079
|
+
// selectable value" caveat in toValueSpace.
|
|
18080
|
+
this.selectedPath.set(items.map(item => this.toValueSpace(item)).join(', '));
|
|
17974
18081
|
}
|
|
17975
18082
|
else {
|
|
17976
|
-
|
|
17977
|
-
|
|
17978
|
-
this.selectedItems.set(
|
|
18083
|
+
const single = typeof value === 'string' ? value : value ? value[0] : '';
|
|
18084
|
+
const item = single ? this.fromValueSpace(single) : '';
|
|
18085
|
+
this.selectedItems.set(item ? [item] : []);
|
|
18086
|
+
this.selectedPath.set(item ? this.toValueSpace(item) : '');
|
|
17979
18087
|
}
|
|
17980
18088
|
}
|
|
17981
18089
|
registerOnChange(fn) {
|
|
@@ -17996,7 +18104,8 @@ class TnFilePickerComponent {
|
|
|
17996
18104
|
*/
|
|
17997
18105
|
onPathCommit(event) {
|
|
17998
18106
|
const target = event.target;
|
|
17999
|
-
|
|
18107
|
+
// Typed text is expressed in value space, like the text the input displays
|
|
18108
|
+
const path = this.fromValueSpace(target.value);
|
|
18000
18109
|
if (this.allowManualInput()) {
|
|
18001
18110
|
// Clearing the input clears the selection
|
|
18002
18111
|
if (!path) {
|
|
@@ -18096,15 +18205,16 @@ class TnFilePickerComponent {
|
|
|
18096
18205
|
// Clear any existing error state
|
|
18097
18206
|
this.hasError.set(false);
|
|
18098
18207
|
if (this.multiSelect()) {
|
|
18099
|
-
|
|
18100
|
-
this.
|
|
18101
|
-
this.
|
|
18208
|
+
const values = selected.map(path => this.toValueSpace(path));
|
|
18209
|
+
this.selectedPath.set(values.join(', '));
|
|
18210
|
+
this.onChange(values);
|
|
18211
|
+
this.selectionChange.emit(values);
|
|
18102
18212
|
}
|
|
18103
18213
|
else {
|
|
18104
|
-
const
|
|
18105
|
-
this.selectedPath.set(
|
|
18106
|
-
this.onChange(
|
|
18107
|
-
this.selectionChange.emit(
|
|
18214
|
+
const value = this.toValueSpace(selected[0]);
|
|
18215
|
+
this.selectedPath.set(value);
|
|
18216
|
+
this.onChange(value);
|
|
18217
|
+
this.selectionChange.emit(value);
|
|
18108
18218
|
}
|
|
18109
18219
|
this.close();
|
|
18110
18220
|
}
|
|
@@ -18239,18 +18349,51 @@ class TnFilePickerComponent {
|
|
|
18239
18349
|
updateSelection(path) {
|
|
18240
18350
|
// Clear any existing error state since popup selections are valid
|
|
18241
18351
|
this.hasError.set(false);
|
|
18352
|
+
const value = this.toValueSpace(path);
|
|
18242
18353
|
if (this.multiSelect()) {
|
|
18243
|
-
|
|
18244
|
-
this.
|
|
18245
|
-
this.
|
|
18246
|
-
this.onChange(selected);
|
|
18354
|
+
this.selectedItems.set([path]);
|
|
18355
|
+
this.selectedPath.set(value);
|
|
18356
|
+
this.onChange([value]);
|
|
18247
18357
|
}
|
|
18248
18358
|
else {
|
|
18249
|
-
this.selectedPath.set(
|
|
18359
|
+
this.selectedPath.set(value);
|
|
18250
18360
|
this.selectedItems.set([path]);
|
|
18251
|
-
this.onChange(
|
|
18361
|
+
this.onChange(value);
|
|
18362
|
+
}
|
|
18363
|
+
this.selectionChange.emit(this.multiSelect() ? [value] : value);
|
|
18364
|
+
}
|
|
18365
|
+
// Mirrors effectiveRootPath so both roots normalize trailing slashes once
|
|
18366
|
+
normalizedValueRoot = computed(() => {
|
|
18367
|
+
const valueRoot = this.valueRoot();
|
|
18368
|
+
return valueRoot ? normalizeRootPath(valueRoot) : undefined;
|
|
18369
|
+
}, ...(ngDevMode ? [{ debugName: "normalizedValueRoot" }] : []));
|
|
18370
|
+
/** Maps an internal absolute path into the `valueRoot`-relative value space. */
|
|
18371
|
+
toValueSpace(path) {
|
|
18372
|
+
const root = this.normalizedValueRoot();
|
|
18373
|
+
if (!root) {
|
|
18374
|
+
return path;
|
|
18375
|
+
}
|
|
18376
|
+
// The value root itself maps to '' — the same payload as a cleared
|
|
18377
|
+
// selection, and writeValue('') cannot restore it. Deliberate: a value
|
|
18378
|
+
// root is the container the values are named against (e.g. /mnt for
|
|
18379
|
+
// dataset names), not a selectable value itself. Revisit this mapping
|
|
18380
|
+
// before pairing valueRoot with a selectable root directory.
|
|
18381
|
+
if (path === root) {
|
|
18382
|
+
return '';
|
|
18383
|
+
}
|
|
18384
|
+
if (root === '/') {
|
|
18385
|
+
return path.startsWith('/') ? path.slice(1) : path;
|
|
18386
|
+
}
|
|
18387
|
+
// Paths outside the value root (e.g. typed absolute paths) pass through
|
|
18388
|
+
return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
|
|
18389
|
+
}
|
|
18390
|
+
/** Maps a `valueRoot`-relative value back into an internal absolute path. */
|
|
18391
|
+
fromValueSpace(value) {
|
|
18392
|
+
const root = this.normalizedValueRoot();
|
|
18393
|
+
if (!root || !value || value.startsWith('/')) {
|
|
18394
|
+
return value;
|
|
18252
18395
|
}
|
|
18253
|
-
|
|
18396
|
+
return root === '/' ? `/${value}` : `${root}/${value}`;
|
|
18254
18397
|
}
|
|
18255
18398
|
emitError(type, message, path) {
|
|
18256
18399
|
this.hasError.set(true);
|
|
@@ -18311,13 +18454,13 @@ class TnFilePickerComponent {
|
|
|
18311
18454
|
this.overlayRef.attach(this.portal);
|
|
18312
18455
|
}
|
|
18313
18456
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18314
|
-
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 }, openOnClick: { classPropertyName: "openOnClick", publicName: "openOnClick", 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: [
|
|
18457
|
+
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 }, openOnClick: { classPropertyName: "openOnClick", publicName: "openOnClick", 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 }, valueRoot: { classPropertyName: "valueRoot", publicName: "valueRoot", 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: [
|
|
18315
18458
|
{
|
|
18316
18459
|
provide: NG_VALUE_ACCESSOR,
|
|
18317
18460
|
useExisting: forwardRef(() => TnFilePickerComponent),
|
|
18318
18461
|
multi: true
|
|
18319
18462
|
}
|
|
18320
|
-
], 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]=\"
|
|
18463
|
+
], 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]=\"resolvedTestId()\">\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 tnTestIdType=\"input\"\n [tnTestId]=\"resolvedTestId()\"\n [class.error]=\"hasError()\"\n [value]=\"selectedPath()\"\n [placeholder]=\"placeholder()\"\n [readonly]=\"!allowManualInput()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onInputClick()\"\n (change)=\"onPathCommit($event)\">\n\n <button\n type=\"button\"\n class=\"tn-file-picker-toggle\"\n aria-label=\"Open file picker\"\n tnTestIdType=\"button\"\n [tnTestId]=\"toggleTestId()\"\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 tnTestIdType=\"file-picker-popup\"\n [tnTestId]=\"resolvedTestId()\"\n [testIdBase]=\"resolvedTestId()\"\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", "testIdBase"], 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"] }] });
|
|
18321
18464
|
}
|
|
18322
18465
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerComponent, decorators: [{
|
|
18323
18466
|
type: Component,
|
|
@@ -18338,8 +18481,349 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
18338
18481
|
], host: {
|
|
18339
18482
|
'class': 'tn-file-picker',
|
|
18340
18483
|
'[class.error]': 'hasError()'
|
|
18341
|
-
}, template: "<div class=\"tn-file-picker-container\" tnTestIdType=\"file-picker\" [tnTestId]=\"
|
|
18342
|
-
}], 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 }] }], openOnClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnClick", 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 }] }] } });
|
|
18484
|
+
}, template: "<div class=\"tn-file-picker-container\" tnTestIdType=\"file-picker\" [tnTestId]=\"resolvedTestId()\">\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 tnTestIdType=\"input\"\n [tnTestId]=\"resolvedTestId()\"\n [class.error]=\"hasError()\"\n [value]=\"selectedPath()\"\n [placeholder]=\"placeholder()\"\n [readonly]=\"!allowManualInput()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onInputClick()\"\n (change)=\"onPathCommit($event)\">\n\n <button\n type=\"button\"\n class=\"tn-file-picker-toggle\"\n aria-label=\"Open file picker\"\n tnTestIdType=\"button\"\n [tnTestId]=\"toggleTestId()\"\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 tnTestIdType=\"file-picker-popup\"\n [tnTestId]=\"resolvedTestId()\"\n [testIdBase]=\"resolvedTestId()\"\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"] }]
|
|
18485
|
+
}], 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 }] }], openOnClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnClick", 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 }] }], valueRoot: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueRoot", 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 }] }] } });
|
|
18486
|
+
|
|
18487
|
+
/**
|
|
18488
|
+
* Harness for interacting with tn-file-picker in tests.
|
|
18489
|
+
*
|
|
18490
|
+
* The core API (`getValue` / `setValue` / `isDisabled`) mirrors webui's
|
|
18491
|
+
* `IxExplorerHarness` so tests migrate with minimal changes: `setValue`
|
|
18492
|
+
* commits a path the way a user would — by typing into the path input and
|
|
18493
|
+
* triggering its `change` event. Two deliberate divergences from ix-explorer:
|
|
18494
|
+
* the component renders no label of its own (labels come from a wrapping
|
|
18495
|
+
* `tn-form-field`), so instances are filtered by `testId` or `placeholder`
|
|
18496
|
+
* instead; and `setValue` takes a single path only — tn-file-picker does not
|
|
18497
|
+
* split typed input on commas in multi-select mode (a comma is part of the
|
|
18498
|
+
* path), so build multi-selections through the popup with
|
|
18499
|
+
* `clickItem` + `clickSelect`.
|
|
18500
|
+
*
|
|
18501
|
+
* Popup helpers (`open`, `getItemNames`, `clickItem`, …) drive the browse
|
|
18502
|
+
* flow. The popup renders in a CDK overlay outside the component, so these
|
|
18503
|
+
* find it at the document root — with several pickers on a page they address
|
|
18504
|
+
* whichever popup is open (only one can be, thanks to the overlay backdrop).
|
|
18505
|
+
*
|
|
18506
|
+
* @example
|
|
18507
|
+
* ```typescript
|
|
18508
|
+
* // Type a path, ix-explorer style
|
|
18509
|
+
* const picker = await loader.getHarness(TnFilePickerHarness.with({ testId: 'file-picker-source-path' }));
|
|
18510
|
+
* await picker.setValue('/mnt/tank/media');
|
|
18511
|
+
* expect(await picker.getValue()).toBe('/mnt/tank/media');
|
|
18512
|
+
*
|
|
18513
|
+
* // Browse and pick an item through the popup
|
|
18514
|
+
* await picker.open();
|
|
18515
|
+
* await picker.clickItem('Documents');
|
|
18516
|
+
* await picker.clickSelect();
|
|
18517
|
+
* ```
|
|
18518
|
+
*/
|
|
18519
|
+
class TnFilePickerHarness extends ComponentHarness {
|
|
18520
|
+
/**
|
|
18521
|
+
* The selector for the host element of a `TnFilePickerComponent` instance.
|
|
18522
|
+
*/
|
|
18523
|
+
static hostSelector = 'tn-file-picker';
|
|
18524
|
+
_container = this.locatorFor('.tn-file-picker-container');
|
|
18525
|
+
_input = this.locatorFor('input.tn-file-picker-input');
|
|
18526
|
+
_toggle = this.locatorFor('button.tn-file-picker-toggle');
|
|
18527
|
+
// The popup renders in a CDK overlay (outside the host), so it is located
|
|
18528
|
+
// from the document root rather than the host subtree.
|
|
18529
|
+
_popup = this.documentRootLocatorFactory().locatorForOptional('tn-file-picker-popup');
|
|
18530
|
+
/**
|
|
18531
|
+
* Gets a `HarnessPredicate` that can be used to search for a file picker
|
|
18532
|
+
* with specific attributes.
|
|
18533
|
+
*
|
|
18534
|
+
* The `testId` filter (like {@link getTestId}) reads the default test
|
|
18535
|
+
* attributes (`data-testid`, falling back to `data-test`); an app that
|
|
18536
|
+
* overrides `TN_TEST_ATTR` to a custom attribute name will silently
|
|
18537
|
+
* match nothing.
|
|
18538
|
+
*
|
|
18539
|
+
* @param options Options for filtering which file picker instances are considered a match.
|
|
18540
|
+
* @returns A `HarnessPredicate` configured with the given options.
|
|
18541
|
+
*
|
|
18542
|
+
* @example
|
|
18543
|
+
* ```typescript
|
|
18544
|
+
* // Find by testId (the rendered attribute, i.e. including the 'file-picker-' prefix)
|
|
18545
|
+
* const picker = await loader.getHarness(TnFilePickerHarness.with({ testId: 'file-picker-source-path' }));
|
|
18546
|
+
*
|
|
18547
|
+
* // Find by placeholder
|
|
18548
|
+
* const picker = await loader.getHarness(TnFilePickerHarness.with({ placeholder: 'Select folder' }));
|
|
18549
|
+
* ```
|
|
18550
|
+
*/
|
|
18551
|
+
static with(options = {}) {
|
|
18552
|
+
return new HarnessPredicate(TnFilePickerHarness, options)
|
|
18553
|
+
.addOption('placeholder', options.placeholder, async (harness, placeholder) => (await harness.getPlaceholder()) === placeholder)
|
|
18554
|
+
.addOption('testId', options.testId, async (harness, testId) => (await harness.getTestId()) === testId);
|
|
18555
|
+
}
|
|
18556
|
+
/**
|
|
18557
|
+
* Gets the current value shown in the path input.
|
|
18558
|
+
*
|
|
18559
|
+
* Note: with `multiSelect`, the component displays the selected paths
|
|
18560
|
+
* joined with `', '` (comma-space), so that is what this resolves to.
|
|
18561
|
+
*
|
|
18562
|
+
* @returns Promise resolving to the input's value.
|
|
18563
|
+
*
|
|
18564
|
+
* @example
|
|
18565
|
+
* ```typescript
|
|
18566
|
+
* expect(await picker.getValue()).toBe('/mnt/tank/media');
|
|
18567
|
+
* ```
|
|
18568
|
+
*/
|
|
18569
|
+
async getValue() {
|
|
18570
|
+
const input = await this._input();
|
|
18571
|
+
return (await input.getProperty('value')) ?? '';
|
|
18572
|
+
}
|
|
18573
|
+
/**
|
|
18574
|
+
* Sets the picker's value by typing a single path into the path input and
|
|
18575
|
+
* committing it via the input's `change` event, exactly as a user pressing
|
|
18576
|
+
* Enter would.
|
|
18577
|
+
*
|
|
18578
|
+
* Unlike `IxExplorerHarness.setValue` there is no `string[]` overload:
|
|
18579
|
+
* tn-file-picker treats typed input as ONE path even in multi-select mode
|
|
18580
|
+
* (commas are legal in paths, so they are never list separators). In
|
|
18581
|
+
* multi-select, committing a typed path REPLACES the whole selection with
|
|
18582
|
+
* that single path; build a real multi-selection through the popup with
|
|
18583
|
+
* {@link clickItem} + {@link clickSelect}.
|
|
18584
|
+
*
|
|
18585
|
+
* Commits go through the component's normal validation: paths outside
|
|
18586
|
+
* `rootPath` are rejected (the value stays unchanged and an `error` is
|
|
18587
|
+
* emitted), and `callbacks.validatePath` is consulted when provided.
|
|
18588
|
+
* Setting an empty string clears the selection. Has no effect when
|
|
18589
|
+
* `allowManualInput` is false (the input is readonly).
|
|
18590
|
+
*
|
|
18591
|
+
* @param value The path to set.
|
|
18592
|
+
*
|
|
18593
|
+
* @example
|
|
18594
|
+
* ```typescript
|
|
18595
|
+
* await picker.setValue('/mnt/tank/media');
|
|
18596
|
+
* ```
|
|
18597
|
+
*/
|
|
18598
|
+
async setValue(value) {
|
|
18599
|
+
const input = await this._input();
|
|
18600
|
+
await input.clear();
|
|
18601
|
+
if (value) {
|
|
18602
|
+
await input.sendKeys(value);
|
|
18603
|
+
}
|
|
18604
|
+
return input.dispatchEvent('change');
|
|
18605
|
+
}
|
|
18606
|
+
/**
|
|
18607
|
+
* Gets the path input's placeholder text.
|
|
18608
|
+
*
|
|
18609
|
+
* @returns Promise resolving to the placeholder string, or null if unset.
|
|
18610
|
+
*
|
|
18611
|
+
* @example
|
|
18612
|
+
* ```typescript
|
|
18613
|
+
* expect(await picker.getPlaceholder()).toBe('Select file or folder');
|
|
18614
|
+
* ```
|
|
18615
|
+
*/
|
|
18616
|
+
async getPlaceholder() {
|
|
18617
|
+
const input = await this._input();
|
|
18618
|
+
return input.getAttribute('placeholder');
|
|
18619
|
+
}
|
|
18620
|
+
/**
|
|
18621
|
+
* Checks whether the picker is disabled (via the `disabled` input or a
|
|
18622
|
+
* disabled form control).
|
|
18623
|
+
*
|
|
18624
|
+
* @returns Promise resolving to true if the picker is disabled.
|
|
18625
|
+
*
|
|
18626
|
+
* @example
|
|
18627
|
+
* ```typescript
|
|
18628
|
+
* expect(await picker.isDisabled()).toBe(false);
|
|
18629
|
+
* ```
|
|
18630
|
+
*/
|
|
18631
|
+
async isDisabled() {
|
|
18632
|
+
const input = await this._input();
|
|
18633
|
+
return (await input.getProperty('disabled')) ?? false;
|
|
18634
|
+
}
|
|
18635
|
+
/**
|
|
18636
|
+
* Checks whether manual path entry is disallowed (`allowManualInput` is
|
|
18637
|
+
* false), rendering the path input readonly.
|
|
18638
|
+
*
|
|
18639
|
+
* @returns Promise resolving to true if the input is readonly.
|
|
18640
|
+
*
|
|
18641
|
+
* @example
|
|
18642
|
+
* ```typescript
|
|
18643
|
+
* expect(await picker.isReadonly()).toBe(false);
|
|
18644
|
+
* ```
|
|
18645
|
+
*/
|
|
18646
|
+
async isReadonly() {
|
|
18647
|
+
const input = await this._input();
|
|
18648
|
+
return (await input.getProperty('readOnly')) ?? false;
|
|
18649
|
+
}
|
|
18650
|
+
/**
|
|
18651
|
+
* Gets the test-id attribute value from the container.
|
|
18652
|
+
*
|
|
18653
|
+
* Reads the default test attributes (`data-testid`, falling back to
|
|
18654
|
+
* `data-test`). A custom `TN_TEST_ATTR` attribute name is not visible to
|
|
18655
|
+
* the harness and resolves to null here.
|
|
18656
|
+
*
|
|
18657
|
+
* @returns Promise resolving to the test-id, or null when unset.
|
|
18658
|
+
*
|
|
18659
|
+
* @example
|
|
18660
|
+
* ```typescript
|
|
18661
|
+
* expect(await picker.getTestId()).toBe('file-picker-source-path');
|
|
18662
|
+
* ```
|
|
18663
|
+
*/
|
|
18664
|
+
async getTestId() {
|
|
18665
|
+
const container = await this._container();
|
|
18666
|
+
return (await container.getAttribute('data-testid')) ?? (await container.getAttribute('data-test'));
|
|
18667
|
+
}
|
|
18668
|
+
/**
|
|
18669
|
+
* Opens the browse popup by clicking the folder toggle button. No-op if
|
|
18670
|
+
* the popup is already open.
|
|
18671
|
+
*
|
|
18672
|
+
* @example
|
|
18673
|
+
* ```typescript
|
|
18674
|
+
* await picker.open();
|
|
18675
|
+
* expect(await picker.isOpen()).toBe(true);
|
|
18676
|
+
* ```
|
|
18677
|
+
*/
|
|
18678
|
+
async open() {
|
|
18679
|
+
if (await this.isOpen()) {
|
|
18680
|
+
return;
|
|
18681
|
+
}
|
|
18682
|
+
await (await this._toggle()).click();
|
|
18683
|
+
}
|
|
18684
|
+
/**
|
|
18685
|
+
* Closes the browse popup without applying the pending selection, by
|
|
18686
|
+
* clicking the overlay backdrop (the same dismissal a user clicking
|
|
18687
|
+
* outside performs). No-op if the popup is not open.
|
|
18688
|
+
*
|
|
18689
|
+
* @example
|
|
18690
|
+
* ```typescript
|
|
18691
|
+
* await picker.close();
|
|
18692
|
+
* expect(await picker.isOpen()).toBe(false);
|
|
18693
|
+
* ```
|
|
18694
|
+
*/
|
|
18695
|
+
async close() {
|
|
18696
|
+
const backdrop = await this.documentRootLocatorFactory()
|
|
18697
|
+
.locatorForOptional('.cdk-overlay-backdrop')();
|
|
18698
|
+
if (backdrop) {
|
|
18699
|
+
await backdrop.click();
|
|
18700
|
+
}
|
|
18701
|
+
}
|
|
18702
|
+
/**
|
|
18703
|
+
* Checks whether the browse popup is currently open.
|
|
18704
|
+
*
|
|
18705
|
+
* @returns Promise resolving to true if the popup is open.
|
|
18706
|
+
*
|
|
18707
|
+
* @example
|
|
18708
|
+
* ```typescript
|
|
18709
|
+
* expect(await picker.isOpen()).toBe(false);
|
|
18710
|
+
* ```
|
|
18711
|
+
*/
|
|
18712
|
+
async isOpen() {
|
|
18713
|
+
return (await this._popup()) !== null;
|
|
18714
|
+
}
|
|
18715
|
+
/**
|
|
18716
|
+
* Gets the labels of the breadcrumb segments in the popup header. Long
|
|
18717
|
+
* paths are truncated by the component, so a segment may be `'…'`.
|
|
18718
|
+
*
|
|
18719
|
+
* @returns Promise resolving to the breadcrumb segment labels.
|
|
18720
|
+
*
|
|
18721
|
+
* @example
|
|
18722
|
+
* ```typescript
|
|
18723
|
+
* await picker.open();
|
|
18724
|
+
* expect(await picker.getBreadcrumbSegments()).toEqual(['mnt']);
|
|
18725
|
+
* ```
|
|
18726
|
+
*/
|
|
18727
|
+
async getBreadcrumbSegments() {
|
|
18728
|
+
const segments = await this.documentRootLocatorFactory()
|
|
18729
|
+
.locatorForAll('tn-file-picker-popup .breadcrumb-segment')();
|
|
18730
|
+
return Promise.all(segments.map(async (segment) => (await segment.text()).trim()));
|
|
18731
|
+
}
|
|
18732
|
+
/**
|
|
18733
|
+
* Gets the names of the items listed in the open popup.
|
|
18734
|
+
*
|
|
18735
|
+
* @returns Promise resolving to the visible item names.
|
|
18736
|
+
*
|
|
18737
|
+
* @example
|
|
18738
|
+
* ```typescript
|
|
18739
|
+
* await picker.open();
|
|
18740
|
+
* expect(await picker.getItemNames()).toContain('Documents');
|
|
18741
|
+
* ```
|
|
18742
|
+
*/
|
|
18743
|
+
async getItemNames() {
|
|
18744
|
+
const names = await this.documentRootLocatorFactory()
|
|
18745
|
+
.locatorForAll('tn-file-picker-popup .file-name')();
|
|
18746
|
+
return Promise.all(names.map(async (name) => (await name.text()).trim()));
|
|
18747
|
+
}
|
|
18748
|
+
/**
|
|
18749
|
+
* Clicks the popup row with the given item name, toggling its selection
|
|
18750
|
+
* (single-select replaces the pending selection; multi-select toggles the
|
|
18751
|
+
* item in and out). The selection is applied with {@link clickSelect}.
|
|
18752
|
+
*
|
|
18753
|
+
* @param name The item name as displayed in the list.
|
|
18754
|
+
*
|
|
18755
|
+
* @example
|
|
18756
|
+
* ```typescript
|
|
18757
|
+
* await picker.open();
|
|
18758
|
+
* await picker.clickItem('Documents');
|
|
18759
|
+
* await picker.clickSelect();
|
|
18760
|
+
* ```
|
|
18761
|
+
*/
|
|
18762
|
+
async clickItem(name) {
|
|
18763
|
+
const names = await this.documentRootLocatorFactory()
|
|
18764
|
+
.locatorForAll('tn-file-picker-popup .file-name')();
|
|
18765
|
+
for (const nameEl of names) {
|
|
18766
|
+
if ((await nameEl.text()).trim() === name) {
|
|
18767
|
+
return nameEl.click();
|
|
18768
|
+
}
|
|
18769
|
+
}
|
|
18770
|
+
throw new Error(`No item named "${name}" found in the file picker popup.`);
|
|
18771
|
+
}
|
|
18772
|
+
/**
|
|
18773
|
+
* Navigates into a directory-like item (folder, dataset, or mountpoint)
|
|
18774
|
+
* by clicking its navigation chevron.
|
|
18775
|
+
*
|
|
18776
|
+
* @param name The item name as displayed in the list.
|
|
18777
|
+
*
|
|
18778
|
+
* @example
|
|
18779
|
+
* ```typescript
|
|
18780
|
+
* await picker.open();
|
|
18781
|
+
* await picker.navigateToItem('Documents');
|
|
18782
|
+
* ```
|
|
18783
|
+
*/
|
|
18784
|
+
async navigateToItem(name) {
|
|
18785
|
+
const button = await this.documentRootLocatorFactory()
|
|
18786
|
+
.locatorForOptional(`tn-file-picker-popup .navigate-button[aria-label="Open ${name}"]`)();
|
|
18787
|
+
if (!button) {
|
|
18788
|
+
throw new Error(`No navigatable item named "${name}" found in the file picker popup.`);
|
|
18789
|
+
}
|
|
18790
|
+
return button.click();
|
|
18791
|
+
}
|
|
18792
|
+
/**
|
|
18793
|
+
* Clicks the popup's Select button, applying the pending selection as the
|
|
18794
|
+
* picker's value and closing the popup.
|
|
18795
|
+
*
|
|
18796
|
+
* @example
|
|
18797
|
+
* ```typescript
|
|
18798
|
+
* await picker.clickItem('Documents');
|
|
18799
|
+
* await picker.clickSelect();
|
|
18800
|
+
* expect(await picker.getValue()).toBe('/mnt/Documents');
|
|
18801
|
+
* ```
|
|
18802
|
+
*/
|
|
18803
|
+
async clickSelect() {
|
|
18804
|
+
const button = await this.documentRootLocatorFactory()
|
|
18805
|
+
.locatorFor(TnButtonHarness.with({ label: 'Select', ancestor: '.tn-file-picker-footer' }))();
|
|
18806
|
+
return button.click();
|
|
18807
|
+
}
|
|
18808
|
+
/**
|
|
18809
|
+
* Clicks the popup's Clear Selection button. Only rendered while items are
|
|
18810
|
+
* selected; throws otherwise.
|
|
18811
|
+
*
|
|
18812
|
+
* @example
|
|
18813
|
+
* ```typescript
|
|
18814
|
+
* await picker.clickItem('Documents');
|
|
18815
|
+
* await picker.clickClearSelection();
|
|
18816
|
+
* ```
|
|
18817
|
+
*/
|
|
18818
|
+
async clickClearSelection() {
|
|
18819
|
+
const button = await this.documentRootLocatorFactory()
|
|
18820
|
+
.locatorForOptional(TnButtonHarness.with({ label: 'Clear Selection', ancestor: '.tn-file-picker-footer' }))();
|
|
18821
|
+
if (!button) {
|
|
18822
|
+
throw new Error('No Clear Selection button found — nothing is selected in the file picker popup.');
|
|
18823
|
+
}
|
|
18824
|
+
return button.click();
|
|
18825
|
+
}
|
|
18826
|
+
}
|
|
18343
18827
|
|
|
18344
18828
|
/**
|
|
18345
18829
|
* Harness for interacting with tn-empty in tests.
|
|
@@ -19302,5 +19786,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
19302
19786
|
* Generated bundle index. Do not edit.
|
|
19303
19787
|
*/
|
|
19304
19788
|
|
|
19305
|
-
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 };
|
|
19789
|
+
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, TnFilePickerHarness, 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 };
|
|
19306
19790
|
//# sourceMappingURL=truenas-ui-components.mjs.map
|