@truenas/ui-components 0.4.2 → 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) {
@@ -17981,6 +17997,16 @@ class TnFilePickerComponent {
17981
17997
  startPath = input('/mnt', ...(ngDevMode ? [{ debugName: "startPath" }] : []));
17982
17998
  /** Restricts navigation — users cannot navigate above this path. */
17983
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" }] : []));
17984
18010
  fileExtensions = input(undefined, ...(ngDevMode ? [{ debugName: "fileExtensions" }] : []));
17985
18011
  callbacks = input(undefined, ...(ngDevMode ? [{ debugName: "callbacks" }] : []));
17986
18012
  selectionChange = output();
@@ -18038,15 +18064,26 @@ class TnFilePickerComponent {
18038
18064
  }
18039
18065
  // ControlValueAccessor implementation
18040
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.
18041
18073
  if (this.multiSelect()) {
18042
- this.selectedItems.set(Array.isArray(value) ? value : value ? [value] : []);
18043
- // For multi-select, show full paths separated by commas
18044
- this.selectedPath.set(this.selectedItems().join(', '));
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(', '));
18045
18081
  }
18046
18082
  else {
18047
- // Store the full path internally
18048
- this.selectedPath.set(typeof value === 'string' ? value : '');
18049
- this.selectedItems.set(value ? [typeof value === 'string' ? value : value[0]] : []);
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) : '');
18050
18087
  }
18051
18088
  }
18052
18089
  registerOnChange(fn) {
@@ -18067,7 +18104,8 @@ class TnFilePickerComponent {
18067
18104
  */
18068
18105
  onPathCommit(event) {
18069
18106
  const target = event.target;
18070
- const path = target.value;
18107
+ // Typed text is expressed in value space, like the text the input displays
18108
+ const path = this.fromValueSpace(target.value);
18071
18109
  if (this.allowManualInput()) {
18072
18110
  // Clearing the input clears the selection
18073
18111
  if (!path) {
@@ -18167,15 +18205,16 @@ class TnFilePickerComponent {
18167
18205
  // Clear any existing error state
18168
18206
  this.hasError.set(false);
18169
18207
  if (this.multiSelect()) {
18170
- this.selectedPath.set(selected.join(', '));
18171
- this.onChange(selected);
18172
- this.selectionChange.emit(selected);
18208
+ const values = selected.map(path => this.toValueSpace(path));
18209
+ this.selectedPath.set(values.join(', '));
18210
+ this.onChange(values);
18211
+ this.selectionChange.emit(values);
18173
18212
  }
18174
18213
  else {
18175
- const path = selected[0];
18176
- this.selectedPath.set(path);
18177
- this.onChange(path);
18178
- this.selectionChange.emit(path);
18214
+ const value = this.toValueSpace(selected[0]);
18215
+ this.selectedPath.set(value);
18216
+ this.onChange(value);
18217
+ this.selectionChange.emit(value);
18179
18218
  }
18180
18219
  this.close();
18181
18220
  }
@@ -18310,18 +18349,51 @@ class TnFilePickerComponent {
18310
18349
  updateSelection(path) {
18311
18350
  // Clear any existing error state since popup selections are valid
18312
18351
  this.hasError.set(false);
18352
+ const value = this.toValueSpace(path);
18313
18353
  if (this.multiSelect()) {
18314
- const selected = [path];
18315
- this.selectedItems.set(selected);
18316
- this.selectedPath.set(selected.join(', '));
18317
- this.onChange(selected);
18354
+ this.selectedItems.set([path]);
18355
+ this.selectedPath.set(value);
18356
+ this.onChange([value]);
18318
18357
  }
18319
18358
  else {
18320
- this.selectedPath.set(path);
18359
+ this.selectedPath.set(value);
18321
18360
  this.selectedItems.set([path]);
18322
- this.onChange(path);
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;
18323
18395
  }
18324
- this.selectionChange.emit(this.multiSelect() ? this.selectedItems() : path);
18396
+ return root === '/' ? `/${value}` : `${root}/${value}`;
18325
18397
  }
18326
18398
  emitError(type, message, path) {
18327
18399
  this.hasError.set(true);
@@ -18382,7 +18454,7 @@ class TnFilePickerComponent {
18382
18454
  this.overlayRef.attach(this.portal);
18383
18455
  }
18384
18456
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18385
- 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: [
18386
18458
  {
18387
18459
  provide: NG_VALUE_ACCESSOR,
18388
18460
  useExisting: forwardRef(() => TnFilePickerComponent),
@@ -18410,7 +18482,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
18410
18482
  'class': 'tn-file-picker',
18411
18483
  '[class.error]': 'hasError()'
18412
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"] }]
18413
- }], 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 }] }] } });
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 }] }] } });
18414
18486
 
18415
18487
  /**
18416
18488
  * Harness for interacting with tn-file-picker in tests.