@yuuvis/client-framework 3.22.0 → 3.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@ import { MatSelectModule } from '@angular/material/select';
9
9
  import { SystemService, TranslateService, Classification, TranslatePipe, Situation, Utils, Operator, OperatorLabel, CatalogService, LocaleNumberPipe, FileSizePipe, IdmService, UserService, AppCacheService, FREE_TEXT_OPTION, USER_ID_REGEX, STORAGE_ROLES_KEY, SearchUtils, LocaleDatePipe, SearchService, DmsService, ObjectConfigService, DmsObject, BaseObjectTypeField, ClassificationPrefix } from '@yuuvis/client-core';
10
10
  import { AbstractMatFormField, injectNgControl, FormTranslateService, DialogComponent, ScrollButtonsComponent } from '@yuuvis/client-framework/common';
11
11
  import * as i1$3 from '@angular/common';
12
- import { NgClass, CommonModule } from '@angular/common';
12
+ import { CommonModule, NgClass } from '@angular/common';
13
13
  import { MatButtonModule } from '@angular/material/button';
14
14
  import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule, MatDialog } from '@angular/material/dialog';
15
15
  import * as i2 from '@angular/material/icon';
@@ -152,6 +152,96 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
152
152
  args: [{ selector: 'yuv-catalog', imports: [MatSelectModule, ReactiveFormsModule, TranslatePipe], providers: [{ provide: MatFormFieldControl, useExisting: CatalogComponent }], template: "<mat-select [multiple]=\"multiple()\" [disabled]=\"readonly()\" [formControl]=\"formCtrl\">\n @if (!multiple() && !required) {\n <mat-option [value]=\"null\">{{ 'yuv.form.element.catalog.option.none' | translate }}</mat-option>\n }\n @for (o of _options(); track o) {\n <mat-option [value]=\"o.value\">{{ o.label }}</mat-option>\n }\n</mat-select>\n" }]
153
153
  }], propDecorators: { readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], classifications: [{ type: i0.Input, args: [{ isSignal: true, alias: "classifications", required: false }] }], situation: [{ type: i0.Input, args: [{ isSignal: true, alias: "situation", required: false }] }] } });
154
154
 
155
+ const HEX_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
156
+ /**
157
+ * Form input for a color value. Renders a hex text input alongside a native color picker
158
+ * swatch; both stay in sync. The value is a hex color string (e.g. `#2A70AC`).
159
+ *
160
+ * Implements `ControlValueAccessor` so it can be used within Angular forms and Material form
161
+ * fields.
162
+ *
163
+ * @example
164
+ * <mat-form-field>
165
+ * <mat-label>Primary color</mat-label>
166
+ * <yuv-color formControlName="primaryColor"></yuv-color>
167
+ * </mat-form-field>
168
+ */
169
+ class ColorComponent extends AbstractMatFormField {
170
+ constructor() {
171
+ super(...arguments);
172
+ this.#dRef = inject(DestroyRef);
173
+ /** the color swatch (native `<input type="color">`, always a full 6-digit hex) */
174
+ this.colorFc = new FormControl('#000000', { nonNullable: true });
175
+ /** the editable hex text input (source of truth for the value) */
176
+ this.textFc = new FormControl(null);
177
+ this.ngControl = injectNgControl(this);
178
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
179
+ this.propagateChange = (_) => { };
180
+ }
181
+ #dRef;
182
+ writeValue(value) {
183
+ this.value = value;
184
+ this.textFc.setValue(value ?? null, { emitEvent: false });
185
+ if (value && HEX_PATTERN.test(value)) {
186
+ this.colorFc.setValue(value, { emitEvent: false });
187
+ }
188
+ }
189
+ registerOnChange(fn) {
190
+ this.propagateChange = fn;
191
+ }
192
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
193
+ registerOnTouched() { }
194
+ setDisabledState(isDisabled) {
195
+ if (isDisabled) {
196
+ this.colorFc.disable();
197
+ this.textFc.disable();
198
+ }
199
+ else {
200
+ this.colorFc.enable();
201
+ this.textFc.enable();
202
+ }
203
+ this.disabled = isDisabled;
204
+ }
205
+ ngOnInit() {
206
+ const validators = [(control) => this.#hexValidator(control.value)];
207
+ if (this.required)
208
+ validators.push(() => (this.value ? null : { required: true }));
209
+ this.textFc.setValidators(validators);
210
+ this.textFc.statusChanges.pipe(takeUntilDestroyed(this.#dRef)).subscribe(() => {
211
+ this.errorState = FormUtils.getErrorState(this.textFc);
212
+ this.ngControl?.control?.setErrors(this.textFc.errors);
213
+ });
214
+ // hex text drives the value; keep the swatch in sync when the text is a valid hex
215
+ this.textFc.valueChanges.pipe(takeUntilDestroyed(this.#dRef)).subscribe((v) => {
216
+ const hex = v?.trim() || null;
217
+ this.value = hex;
218
+ if (hex && HEX_PATTERN.test(hex)) {
219
+ this.colorFc.setValue(hex, { emitEvent: false });
220
+ }
221
+ this.propagateChange(hex);
222
+ });
223
+ // swatch changes update the hex text (which re-propagates)
224
+ this.colorFc.valueChanges.pipe(takeUntilDestroyed(this.#dRef)).subscribe((v) => {
225
+ this.textFc.setValue(v);
226
+ });
227
+ this.textFc.updateValueAndValidity();
228
+ }
229
+ ngOnDestroy() {
230
+ super.onNgOnDestroy();
231
+ }
232
+ #hexValidator(value) {
233
+ if (!value)
234
+ return null;
235
+ return HEX_PATTERN.test(value.trim()) ? null : { color: { valid: false } };
236
+ }
237
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ColorComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
238
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.20", type: ColorComponent, isStandalone: true, selector: "yuv-color", providers: [{ provide: MatFormFieldControl, useExisting: ColorComponent }], usesInheritance: true, ngImport: i0, template: "<span class=\"yuv-color\">\n <label class=\"yuv-color__swatch\" [style.background-color]=\"colorFc.value\">\n <input type=\"color\" [formControl]=\"colorFc\" aria-hidden=\"true\" tabindex=\"-1\" />\n </label>\n <input class=\"yuv-color__hex\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\" [formControl]=\"textFc\" />\n</span>\n", styles: [".yuv-color{display:inline-flex;align-items:center;gap:var(--ymt-spacing-xs);width:100%;margin-block-start:var(--ymt-spacing-xs)}.yuv-color__swatch{position:relative;flex:0 0 auto;inline-size:var(--ymt-sizing-m);block-size:var(--ymt-sizing-m);border-radius:var(--ymt-corner-xs);border:var(--ymt-outline-width) solid var(--ymt-outline-variant);overflow:hidden;cursor:pointer;outline:2px solid currentColor;outline-offset:-1px}.yuv-color__swatch input[type=color]{position:absolute;inset:0;inline-size:100%;block-size:100%;padding:0;border:none;opacity:0;cursor:pointer}.yuv-color__hex{flex:1 1 auto;min-inline-size:0;border:none;outline:none;background:transparent;color:var(--ymt-text-color);font:var(--ymt-font-body)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] }); }
239
+ }
240
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ColorComponent, decorators: [{
241
+ type: Component,
242
+ args: [{ selector: 'yuv-color', standalone: true, imports: [CommonModule, ReactiveFormsModule], providers: [{ provide: MatFormFieldControl, useExisting: ColorComponent }], template: "<span class=\"yuv-color\">\n <label class=\"yuv-color__swatch\" [style.background-color]=\"colorFc.value\">\n <input type=\"color\" [formControl]=\"colorFc\" aria-hidden=\"true\" tabindex=\"-1\" />\n </label>\n <input class=\"yuv-color__hex\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\" [formControl]=\"textFc\" />\n</span>\n", styles: [".yuv-color{display:inline-flex;align-items:center;gap:var(--ymt-spacing-xs);width:100%;margin-block-start:var(--ymt-spacing-xs)}.yuv-color__swatch{position:relative;flex:0 0 auto;inline-size:var(--ymt-sizing-m);block-size:var(--ymt-sizing-m);border-radius:var(--ymt-corner-xs);border:var(--ymt-outline-width) solid var(--ymt-outline-variant);overflow:hidden;cursor:pointer;outline:2px solid currentColor;outline-offset:-1px}.yuv-color__swatch input[type=color]{position:absolute;inset:0;inline-size:100%;block-size:100%;padding:0;border:none;opacity:0;cursor:pointer}.yuv-color__hex{flex:1 1 auto;min-inline-size:0;border:none;outline:none;background:transparent;color:var(--ymt-text-color);font:var(--ymt-font-body)}\n"] }]
243
+ }] });
244
+
155
245
  class EditTableDataComponent {
156
246
  constructor() {
157
247
  this.#fb = inject(FormBuilder);
@@ -1062,6 +1152,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1062
1152
  }], ctorParameters: () => [], propDecorators: { readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], propertyName: [{ type: i0.Input, args: [{ isSignal: true, alias: "propertyName", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], classifications: [{ type: i0.Input, args: [{ isSignal: true, alias: "classifications", required: false }] }], situation: [{ type: i0.Input, args: [{ isSignal: true, alias: "situation", required: false }] }] } });
1063
1153
 
1064
1154
  var _a;
1155
+ /**
1156
+ * Splits a number into the digits of its mantissa and its decimal exponent, so that
1157
+ * digit counting stays correct for values JavaScript renders in exponential notation
1158
+ * (`1e-7`, `1e21`).
1159
+ */
1160
+ function splitDigits(value) {
1161
+ const [mantissa, exponent] = Math.abs(value).toString().split('e');
1162
+ const [intPart, fractionPart = ''] = mantissa.split('.');
1163
+ return { intPart, fractionPart, exponent: Number(exponent ?? 0) };
1164
+ }
1165
+ /** Decimal places of a number: `12.5` → 1, `12` → 0, `1e-7` → 7. */
1166
+ function decimalPlaces(value) {
1167
+ const { fractionPart, exponent } = splitDigits(value);
1168
+ return Math.max(0, fractionPart.length - exponent);
1169
+ }
1170
+ /** Digits before the decimal separator, sign excluded: `-1234.5` → 4, `0.5` → 1. */
1171
+ function integerDigits(value) {
1172
+ const { intPart, exponent } = splitDigits(value);
1173
+ return Math.max(1, intPart.length + exponent);
1174
+ }
1065
1175
  /**
1066
1176
  * Creates form input for number values.
1067
1177
  *
@@ -1207,16 +1317,16 @@ class NumberComponent extends AbstractMatFormField {
1207
1317
  errors.push({ key: 'number' });
1208
1318
  }
1209
1319
  else {
1210
- const split = control.value.replaceAll(this.transformPipe.separator, '').split(this.transformPipe.decimalSeparator);
1320
+ // Digits are counted on the parsed value, never on the typed text: the text
1321
+ // carries locale separators, a sign and — for the filesize classification —
1322
+ // a unit ("1.5 MB"), none of which say anything about the value being stored.
1211
1323
  // check precision
1212
- if (this.#precision() !== undefined) {
1213
- const prePointDigits = this.#precision() - this.#scale();
1214
- if (split[0]?.length > prePointDigits) {
1215
- errors.push({ key: 'precision', params: { prePointDigits } });
1216
- }
1324
+ const prePointDigits = this.#precision() - this.#scale();
1325
+ if (integerDigits(val) > prePointDigits) {
1326
+ errors.push({ key: 'precision', params: { prePointDigits } });
1217
1327
  }
1218
- // check scale
1219
- if (this.#scale() && split[1]?.length > this.#scale()) {
1328
+ // check scale: a scale of 0 (set explicitly or left unset) allows no decimals at all
1329
+ if (decimalPlaces(val) > this.#scale()) {
1220
1330
  errors.push({ key: 'scale', params: { scale: this.#scale() } });
1221
1331
  }
1222
1332
  // min max
@@ -1632,7 +1742,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1632
1742
  args: [{ selector: 'yuv-organization-set', imports: [FormsModule, YuvAutocompleteModule, MatTooltipModule, MatIconModule, ReactiveFormsModule, TranslatePipe$1], providers: [{ provide: MatFormFieldControl, useExisting: OrganizationSetComponent }], template: "<yuv-autocomplete\n [busy]=\"busy()\"\n [formControl]=\"acFormControl\"\n #autocomplete\n [placeholder]=\"placeholder\"\n [disabled]=\"readonly()\"\n [autocompleteValues]=\"autocompleteRes\"\n [forceSelection]=\"true\"\n (autocompleteFnc)=\"autocompleteFn($event)\"\n [multiple]=\"true\"\n [maxItems]=\"multiselect() ? -1 : 1\"\n>\n <!-- template for item inside the dropdown -->\n <ng-template #optionTemplate let-item>\n <span class=\"chip\">{{ item.value.title }}</span>\n </ng-template>\n\n <!-- template for chip -->\n <ng-template #chipTemplate let-item>\n <span class=\"chip\">\n {{ item.value.title || item.label || '...' }}\n </span>\n </ng-template>\n</yuv-autocomplete>\n\n<mat-icon class=\"ymt-icon--size-s\" [matTooltip]=\"'yuv.form.element.organization-set.classify.icon.title' | translate\">\n {{ multiselect() ? 'group' : 'person' }}\n</mat-icon>\n", styles: [":host{display:flex;align-items:center}:host yuv-autocomplete{flex:1}:host mat-icon{color:var(--ymt-text-color-subtle)}\n"] }]
1633
1743
  }], propDecorators: { situation: [{ type: i0.Input, args: [{ isSignal: true, alias: "situation", required: false }] }], multiselect: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiselect", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], withMetadata: [{ type: i0.Input, args: [{ isSignal: true, alias: "withMetadata", required: false }] }], autocompleteMinLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocompleteMinLength", required: false }] }], classifications: [{ type: i0.Input, args: [{ isSignal: true, alias: "classifications", required: false }] }], types: [{ type: i0.Input, args: [{ isSignal: true, alias: "types", required: false }] }] } });
1634
1744
 
1635
- const mapOrganizationNode = (innerValue, excludeUserId) => map$1((nodes) => nodes
1745
+ /**
1746
+ * Maps IDM results to autocomplete items. The `user` / `role` discriminator is supplied by the
1747
+ * caller instead of being inferred from the node's shape: the organization set already carries
1748
+ * a correct `type`, and every result of `queryUser()` is a user by definition.
1749
+ */
1750
+ const mapOrganizationNode = (innerValue, nodeType, excludeUserId) => map$1((nodes) => nodes
1636
1751
  .filter((node) => !innerValue?.some((value) => value?.id === node.id))
1637
1752
  // `excludeMe` cannot be left to the backend: `/idm/search` has no such parameter at all,
1638
1753
  // and `/idm/users?excludeMe=true` is not guaranteed to honour it. Dropping the current
@@ -1644,7 +1759,7 @@ const mapOrganizationNode = (innerValue, excludeUserId) => map$1((nodes) => node
1644
1759
  value: {
1645
1760
  id: node.id,
1646
1761
  title: node.title,
1647
- type: 'user' in node ? 'user' : 'role'
1762
+ type: nodeType(node)
1648
1763
  }
1649
1764
  })));
1650
1765
  /**
@@ -1928,10 +2043,10 @@ class OrganizationComponent extends AbstractMatFormField {
1928
2043
  this.acFormControl.updateValueAndValidity({ emitEvent: false });
1929
2044
  }
1930
2045
  #resolveOrganization(query, excludeMe, filterRoles) {
1931
- return this.#userService.queryUser(query, excludeMe, filterRoles).pipe(mapOrganizationNode(this.innerValue, excludeMe ? this.#ownUserId() : undefined), catchError(() => of([])));
2046
+ return this.#userService.queryUser(query, excludeMe, filterRoles).pipe(mapOrganizationNode(this.innerValue, () => 'user', excludeMe ? this.#ownUserId() : undefined), catchError(() => of([])));
1932
2047
  }
1933
2048
  #resolveOrganizationSet(query, excludeMe, filterRoles) {
1934
- return this.#idmService.queryOrganizationEntity(query, filterRoles).pipe(mapOrganizationNode(this.innerValue, excludeMe ? this.#ownUserId() : undefined), catchError(() => of([])));
2049
+ return this.#idmService.queryOrganizationEntity(query, filterRoles).pipe(mapOrganizationNode(this.innerValue, (node) => node.type, excludeMe ? this.#ownUserId() : undefined), catchError(() => of([])));
1935
2050
  }
1936
2051
  /** Id of the signed-in user — the value {@link excludeMe} filters out of the suggestions. */
1937
2052
  #ownUserId() {
@@ -2878,5 +2993,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
2878
2993
  * Generated bundle index. Do not edit.
2879
2994
  */
2880
2995
 
2881
- export { CatalogComponent, DataGridComponent, DatetimeComponent, DatetimeRangeComponent, DynamicCatalogComponent, FormUtils, I18nCatalogComponent, NumberComponent, NumberRangeComponent, OrganizationComponent, OrganizationSetComponent, RangeSelectDateComponent, RangeSelectFilesizeComponent, ReferenceComponent, StringComponent, YuvFormsModule };
2996
+ export { CatalogComponent, ColorComponent, DataGridComponent, DatetimeComponent, DatetimeRangeComponent, DynamicCatalogComponent, FormUtils, I18nCatalogComponent, NumberComponent, NumberRangeComponent, OrganizationComponent, OrganizationSetComponent, RangeSelectDateComponent, RangeSelectFilesizeComponent, ReferenceComponent, StringComponent, YuvFormsModule };
2882
2997
  //# sourceMappingURL=yuuvis-client-framework-forms.mjs.map