@maro-tech/form 0.0.12 → 0.0.14

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.
@@ -1,11 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { signal, Injectable, inject, InjectionToken, input, output, ViewChild, Component, ChangeDetectorRef, DestroyRef, EventEmitter, ElementRef, forwardRef, booleanAttribute, HostListener, Output, Input, ChangeDetectionStrategy, ViewContainerRef, computed } from '@angular/core';
3
3
  import { HttpClient } from '@angular/common/http';
4
+ import { map, Subject, debounceTime, distinctUntilChanged, forkJoin, Observable, from, switchMap, of } from 'rxjs';
4
5
  import * as i1$2 from '@angular/common';
5
6
  import { CommonModule } from '@angular/common';
6
7
  import * as i2 from '@angular/forms';
7
8
  import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
8
- import { Subject, debounceTime, distinctUntilChanged, forkJoin, Observable, from, switchMap, of } from 'rxjs';
9
9
  import { BrowserMultiFormatOneDReader } from '@zxing/browser';
10
10
  import { DecodeHintType, BarcodeFormat } from '@zxing/library';
11
11
  import { UiButtonComponent } from '@maro-tech/core';
@@ -48,6 +48,11 @@ class MaroFormsApiService {
48
48
  const suffix = query ? `?q=${encodeURIComponent(query)}` : '';
49
49
  return this.http.get(`/api/suggest/${encodeURIComponent(name)}${suffix}`);
50
50
  }
51
+ uploadFile(file) {
52
+ const formData = new FormData();
53
+ formData.append('file', file);
54
+ return this.http.post('/api/files/', formData).pipe(map((response) => ({ imageId: response.id, type: response.type })));
55
+ }
51
56
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MaroFormsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
52
57
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MaroFormsApiService, providedIn: 'root' });
53
58
  }
@@ -2463,12 +2468,12 @@ class UiDynamicFormComponent {
2463
2468
  const next = { ...this.value };
2464
2469
  for (const field of this.getLeafFields()) {
2465
2470
  const defaultValue = field.type === 'checkbox' ? false : '';
2466
- const nextValue = this.value[field.id] !== undefined
2471
+ const rawValue = this.value[field.id] !== undefined
2467
2472
  ? this.value[field.id]
2468
2473
  : field.default !== undefined
2469
2474
  ? field.default
2470
2475
  : defaultValue;
2471
- next[field.id] = nextValue;
2476
+ next[field.id] = this.normalizeFieldValue(field, rawValue);
2472
2477
  }
2473
2478
  this.modelSignal.set(next);
2474
2479
  this.formContext.setValue(next);
@@ -2491,6 +2496,13 @@ class UiDynamicFormComponent {
2491
2496
  return updated;
2492
2497
  });
2493
2498
  }
2499
+ normalizeFieldValue(field, value) {
2500
+ if (field.type === 'photo' && field.multiple) {
2501
+ const values = Array.isArray(value) ? value : String(value || '').split(',');
2502
+ return values.map((item) => String(item || '').trim()).filter(Boolean);
2503
+ }
2504
+ return value;
2505
+ }
2494
2506
  onBarcodeValueChange(field, value) {
2495
2507
  this.updateField(field.id, value);
2496
2508
  this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
@@ -2890,6 +2902,7 @@ class UiActionFormModalComponent {
2890
2902
  recordId = input('', ...(ngDevMode ? [{ debugName: "recordId" }] : /* istanbul ignore next */ []));
2891
2903
  size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2892
2904
  isOpen = input(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
2905
+ cancelLabel = input('Відмінити', ...(ngDevMode ? [{ debugName: "cancelLabel" }] : /* istanbul ignore next */ []));
2893
2906
  initialValue = input({}, ...(ngDevMode ? [{ debugName: "initialValue" }] : /* istanbul ignore next */ []));
2894
2907
  uploadPhoto = input(null, ...(ngDevMode ? [{ debugName: "uploadPhoto" }] : /* istanbul ignore next */ []));
2895
2908
  closed = output();
@@ -2909,6 +2922,8 @@ class UiActionFormModalComponent {
2909
2922
  }), ...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
2910
2923
  http = inject(HttpClient);
2911
2924
  router = inject(Router);
2925
+ formsApi = inject(MaroFormsApiService);
2926
+ defaultUploadPhoto = (file) => this.formsApi.uploadFile(file);
2912
2927
  ngOnChanges(changes) {
2913
2928
  if (this.isOpen() && ('isOpen' in changes || 'formId' in changes || 'recordId' in changes)) {
2914
2929
  this.loadForm();
@@ -2935,10 +2950,11 @@ class UiActionFormModalComponent {
2935
2950
  this.isUploading.set(uploading);
2936
2951
  }
2937
2952
  onFieldActionTriggered(event) {
2938
- const endpoint = this.resolveEndpoint(event.action.api);
2953
+ const payload = this.normalizePayload({ ...this.formValue(), ...(event.action.payload || {}) });
2954
+ const endpoint = this.resolveTemplate(this.resolveEndpoint(event.action.api), payload);
2939
2955
  if (!endpoint)
2940
2956
  return;
2941
- this.http.request(this.resolveMethod(event.action.method), endpoint, { body: { ...this.formValue(), ...(event.action.payload || {}) } }).subscribe({
2957
+ this.http.request(this.resolveMethod(event.action.method), endpoint, { body: payload }).subscribe({
2942
2958
  next: (response) => {
2943
2959
  const data = this.extractData(response);
2944
2960
  if (data)
@@ -2950,15 +2966,19 @@ class UiActionFormModalComponent {
2950
2966
  if (this.isSubmitting())
2951
2967
  return;
2952
2968
  const action = this.submitAction();
2953
- const endpoint = this.resolveEndpoint(this.formConfig()?.api || action?.api);
2969
+ const endpointTemplate = this.formConfig()?.api || action?.api;
2970
+ const endpoint = this.resolveEndpoint(endpointTemplate);
2954
2971
  if (!endpoint)
2955
2972
  return;
2956
- const payload = { ...(this.initialValue() || {}), ...(action?.payload || {}), ...this.formValue() };
2973
+ const payload = this.normalizePayload({ ...(this.initialValue() || {}), ...(action?.payload || {}), ...this.formValue() });
2957
2974
  const recordId = String(this.recordId() || '').trim();
2958
2975
  if (recordId && !Object.prototype.hasOwnProperty.call(payload, 'id'))
2959
2976
  payload['id'] = recordId;
2977
+ const resolvedEndpoint = this.resolveTemplate(endpoint, payload);
2978
+ if (!resolvedEndpoint || resolvedEndpoint.includes('{{'))
2979
+ return;
2960
2980
  this.isSubmitting.set(true);
2961
- this.http.request(this.resolveMethod(action?.method || (recordId ? 'PATCH' : 'POST')), endpoint, { body: payload }).subscribe({
2981
+ this.http.request(this.resolveMethod(action?.method || (recordId ? 'PATCH' : 'POST')), resolvedEndpoint, { body: payload }).subscribe({
2962
2982
  next: (response) => {
2963
2983
  this.isSubmitting.set(false);
2964
2984
  this.completed.emit(response);
@@ -3004,6 +3024,17 @@ class UiActionFormModalComponent {
3004
3024
  return value;
3005
3025
  return `/api${value.startsWith('/') ? value : `/${value}`}`;
3006
3026
  }
3027
+ normalizePayload(payload) {
3028
+ for (const field of this.fields()) {
3029
+ if (field.type !== 'photo' || !field.multiple)
3030
+ continue;
3031
+ const value = payload[field.id];
3032
+ if (typeof value === 'string') {
3033
+ payload[field.id] = value.split(',').map((item) => item.trim()).filter(Boolean);
3034
+ }
3035
+ }
3036
+ return payload;
3037
+ }
3007
3038
  normalizeActionType(type) {
3008
3039
  const normalized = String(type || '').trim().toLowerCase();
3009
3040
  if (normalized === 'api' || normalized === 'action_api')
@@ -3042,12 +3073,12 @@ class UiActionFormModalComponent {
3042
3073
  });
3043
3074
  }
3044
3075
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: UiActionFormModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3045
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: UiActionFormModalComponent, isStandalone: true, selector: "forms-action-form-modal", inputs: { formId: { classPropertyName: "formId", publicName: "formId", isSignal: true, isRequired: false, transformFunction: null }, recordId: { classPropertyName: "recordId", publicName: "recordId", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null }, uploadPhoto: { classPropertyName: "uploadPhoto", publicName: "uploadPhoto", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", completed: "completed" }, usesOnChanges: true, ngImport: i0, template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex min-h-full items-center justify-center overflow-y-auto bg-slate-950/40 p-4 sm:p-6\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"my-0 flex min-h-0 max-h-[calc(100vh-2rem)] w-full flex-col overflow-hidden rounded-2xl bg-slate-50 shadow-2xl sm:max-h-[calc(100vh-3rem)]\" [ngClass]=\"panelClass()\">\n <div class=\"flex shrink-0 items-center justify-between border-b border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <forms-dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [showSubmit]=\"false\" [uploadPhoto]=\"uploadPhoto()\" (valueChange)=\"onValueChange($event)\" (lookupCompleted)=\"onLookupCompleted($event)\" (uploadingChange)=\"onUploadingChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></forms-dynamic-form>\n }\n </div>\n <div class=\"relative z-10 flex shrink-0 justify-end gap-3 border-t border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <ui-button variant=\"ghost\" (click)=\"close()\">Cancel</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting() || isUploading()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiDynamicFormComponent, selector: "forms-dynamic-form", inputs: ["fields", "value", "errors", "submitLabel", "showSubmit", "disabled", "uploadPhoto"], outputs: ["valueChange", "lookupCompleted", "submitted", "filesChange", "uploadingChange", "fieldActionTriggered"] }] });
3076
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: UiActionFormModalComponent, isStandalone: true, selector: "forms-action-form-modal", inputs: { formId: { classPropertyName: "formId", publicName: "formId", isSignal: true, isRequired: false, transformFunction: null }, recordId: { classPropertyName: "recordId", publicName: "recordId", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, cancelLabel: { classPropertyName: "cancelLabel", publicName: "cancelLabel", isSignal: true, isRequired: false, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null }, uploadPhoto: { classPropertyName: "uploadPhoto", publicName: "uploadPhoto", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", completed: "completed" }, usesOnChanges: true, ngImport: i0, template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex min-h-full items-center justify-center overflow-y-auto bg-slate-950/40 p-4 sm:p-6\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"my-0 flex min-h-0 max-h-[calc(100vh-2rem)] w-full flex-col overflow-hidden rounded-2xl bg-slate-50 shadow-2xl sm:max-h-[calc(100vh-3rem)]\" [ngClass]=\"panelClass()\">\n <div class=\"flex shrink-0 items-center justify-between border-b border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <forms-dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [showSubmit]=\"false\" [uploadPhoto]=\"uploadPhoto() || defaultUploadPhoto\" (valueChange)=\"onValueChange($event)\" (lookupCompleted)=\"onLookupCompleted($event)\" (uploadingChange)=\"onUploadingChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></forms-dynamic-form>\n }\n </div>\n <div class=\"relative z-10 flex shrink-0 justify-end gap-3 border-t border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <ui-button variant=\"ghost\" (click)=\"close()\">{{ cancelLabel() }}</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting() || isUploading()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiDynamicFormComponent, selector: "forms-dynamic-form", inputs: ["fields", "value", "errors", "submitLabel", "showSubmit", "disabled", "uploadPhoto"], outputs: ["valueChange", "lookupCompleted", "submitted", "filesChange", "uploadingChange", "fieldActionTriggered"] }] });
3046
3077
  }
3047
3078
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: UiActionFormModalComponent, decorators: [{
3048
3079
  type: Component,
3049
- args: [{ selector: 'forms-action-form-modal', standalone: true, imports: [CommonModule, UiButtonComponent, UiDynamicFormComponent], template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex min-h-full items-center justify-center overflow-y-auto bg-slate-950/40 p-4 sm:p-6\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"my-0 flex min-h-0 max-h-[calc(100vh-2rem)] w-full flex-col overflow-hidden rounded-2xl bg-slate-50 shadow-2xl sm:max-h-[calc(100vh-3rem)]\" [ngClass]=\"panelClass()\">\n <div class=\"flex shrink-0 items-center justify-between border-b border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <forms-dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [showSubmit]=\"false\" [uploadPhoto]=\"uploadPhoto()\" (valueChange)=\"onValueChange($event)\" (lookupCompleted)=\"onLookupCompleted($event)\" (uploadingChange)=\"onUploadingChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></forms-dynamic-form>\n }\n </div>\n <div class=\"relative z-10 flex shrink-0 justify-end gap-3 border-t border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <ui-button variant=\"ghost\" (click)=\"close()\">Cancel</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting() || isUploading()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n" }]
3050
- }], propDecorators: { formId: [{ type: i0.Input, args: [{ isSignal: true, alias: "formId", required: false }] }], recordId: [{ type: i0.Input, args: [{ isSignal: true, alias: "recordId", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], initialValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValue", required: false }] }], uploadPhoto: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadPhoto", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], completed: [{ type: i0.Output, args: ["completed"] }] } });
3080
+ args: [{ selector: 'forms-action-form-modal', standalone: true, imports: [CommonModule, UiButtonComponent, UiDynamicFormComponent], template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex min-h-full items-center justify-center overflow-y-auto bg-slate-950/40 p-4 sm:p-6\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"my-0 flex min-h-0 max-h-[calc(100vh-2rem)] w-full flex-col overflow-hidden rounded-2xl bg-slate-50 shadow-2xl sm:max-h-[calc(100vh-3rem)]\" [ngClass]=\"panelClass()\">\n <div class=\"flex shrink-0 items-center justify-between border-b border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <forms-dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [showSubmit]=\"false\" [uploadPhoto]=\"uploadPhoto() || defaultUploadPhoto\" (valueChange)=\"onValueChange($event)\" (lookupCompleted)=\"onLookupCompleted($event)\" (uploadingChange)=\"onUploadingChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></forms-dynamic-form>\n }\n </div>\n <div class=\"relative z-10 flex shrink-0 justify-end gap-3 border-t border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <ui-button variant=\"ghost\" (click)=\"close()\">{{ cancelLabel() }}</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting() || isUploading()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n" }]
3081
+ }], propDecorators: { formId: [{ type: i0.Input, args: [{ isSignal: true, alias: "formId", required: false }] }], recordId: [{ type: i0.Input, args: [{ isSignal: true, alias: "recordId", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], cancelLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelLabel", required: false }] }], initialValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValue", required: false }] }], uploadPhoto: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadPhoto", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], completed: [{ type: i0.Output, args: ["completed"] }] } });
3051
3082
 
3052
3083
  class UiInputComponent {
3053
3084
  cdr = inject(ChangeDetectorRef);