spiderly 19.5.3 → 19.5.4-preview.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.
@@ -209,7 +209,7 @@ class SpiderlyFormGroup extends FormGroup {
209
209
  constructor(controls) {
210
210
  super(controls);
211
211
  this.trackingId = crypto.randomUUID();
212
- this.initSaveBody = () => null;
212
+ // TODO: Delete controlNamesFromHtml and add UIDoNotGenerate flag into ng entity generator, we shouldn't even add those into parentFormGroup
213
213
  this.controlNamesFromHtml = [];
214
214
  this.getControl = (formControlName) => {
215
215
  if (this.controlNamesFromHtml.findIndex((x) => x === formControlName) === -1)
@@ -285,10 +285,10 @@ class SpiderlyFormArray extends FormArray {
285
285
  class ValidatorAbstractService {
286
286
  constructor(translocoService) {
287
287
  this.translocoService = translocoService;
288
- this.isArrayEmpty = (control) => {
288
+ this.notEmpty = (control) => {
289
289
  const validator = () => {
290
290
  const value = control.value;
291
- const notEmptyRule = typeof value !== 'undefined' && value !== null && value.length !== 0;
291
+ const notEmptyRule = typeof value !== 'undefined' && value !== null && value !== '';
292
292
  const arrayValid = notEmptyRule;
293
293
  return arrayValid
294
294
  ? null
@@ -296,25 +296,32 @@ class ValidatorAbstractService {
296
296
  };
297
297
  validator.hasNotEmptyRule = true;
298
298
  control.required = true;
299
- return validator;
299
+ control.validator = validator;
300
+ control.updateValueAndValidity();
300
301
  };
301
- this.notEmpty = (control) => {
302
+ /** Validates that a SpiderlyFormArray (collection of form controls/groups) is not empty. */
303
+ this.isFormArrayEmpty = (control) => {
302
304
  const validator = () => {
303
- const value = control.value;
304
- const notEmptyRule = typeof value !== 'undefined' && value !== null && value !== '';
305
+ const value = control;
306
+ const notEmptyRule = typeof value !== 'undefined' && value !== null && value.length !== 0;
305
307
  const arrayValid = notEmptyRule;
306
308
  return arrayValid
307
309
  ? null
308
- : { _: this.translocoService.translate('NotEmpty') };
310
+ : {
311
+ _: this.translocoService.translate('ListCanNotBeEmpty', {
312
+ value: control.labelForDisplay,
313
+ }),
314
+ };
309
315
  };
310
316
  validator.hasNotEmptyRule = true;
311
317
  control.required = true;
312
- control.validator = validator;
318
+ control.setValidators(validator);
313
319
  control.updateValueAndValidity();
314
320
  };
315
- this.isFormArrayEmpty = (control) => {
321
+ /** Validates that a SpiderlyFormControl holding an array value (e.g., multi-select dropdown) is not empty. */
322
+ this.isArrayEmpty = (control) => {
316
323
  const validator = () => {
317
- const value = control;
324
+ const value = control.value;
318
325
  const notEmptyRule = typeof value !== 'undefined' && value !== null && value.length !== 0;
319
326
  const arrayValid = notEmptyRule;
320
327
  return arrayValid
@@ -765,7 +772,7 @@ class SpiderlyEditorComponent extends BaseControl {
765
772
  };
766
773
  }
767
774
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyEditorComponent, deps: [{ token: i1.TranslocoService }], target: i0.ɵɵFactoryTarget.Component }); }
768
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyEditorComponent, isStandalone: true, selector: "spiderly-editor", viewQueries: [{ propertyName: "editor", first: true, predicate: Editor, descendants: true }, { propertyName: "tooltip", first: true, predicate: Tooltip, descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Can't put (onBlur) in this control -->\n\n<div style=\"display: flex; flex-direction: column; gap: 0.5rem\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n\n <!-- Disable doesn't work on this control -->\n <p-editor\n *ngIf=\"control\"\n [formControl]=\"control\"\n [pTooltip]=\"getValidationErrrorMessages()\"\n [tooltipEvent]=\"errorMessageTooltipEvent\"\n tooltipPosition=\"bottom\"\n [tooltipDisabled]=\"control.valid\"\n tooltipStyleClass=\"spiderly-tooltip-invalid\"\n [readonly]=\"control.disabled\"\n [class]=\"control.invalid && control.dirty ? 'control-error-border' : ''\"\n [id]=\"control.label\"\n [placeholder]=\"placeholder\"\n (click)=\"onClick()\"\n [style]=\"{ height: '320px' }\"\n ></p-editor>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: EditorModule }, { kind: "component", type: i4$8.Editor, selector: "p-editor", inputs: ["style", "styleClass", "placeholder", "formats", "modules", "bounds", "scrollingContainer", "debug", "readonly"], outputs: ["onInit", "onTextChange", "onSelectionChange"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: RequiredComponent, selector: "required" }] }); }
775
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyEditorComponent, isStandalone: true, selector: "spiderly-editor", viewQueries: [{ propertyName: "editor", first: true, predicate: Editor, descendants: true }, { propertyName: "tooltip", first: true, predicate: Tooltip, descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Can't put (onBlur) in this control -->\n\n<div style=\"display: flex; flex-direction: column; gap: 0.5rem; padding: 0 1px\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n\n <!-- Disable doesn't work on this control -->\n <p-editor\n *ngIf=\"control\"\n [formControl]=\"control\"\n [pTooltip]=\"getValidationErrrorMessages()\"\n [tooltipEvent]=\"errorMessageTooltipEvent\"\n tooltipPosition=\"bottom\"\n [tooltipDisabled]=\"control.valid\"\n tooltipStyleClass=\"spiderly-tooltip-invalid\"\n [readonly]=\"control.disabled\"\n [class]=\"control.invalid && control.dirty ? 'control-error-border' : ''\"\n [id]=\"control.label\"\n [placeholder]=\"placeholder\"\n (click)=\"onClick()\"\n [style]=\"{ height: '320px' }\"\n ></p-editor>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: EditorModule }, { kind: "component", type: i4$8.Editor, selector: "p-editor", inputs: ["style", "styleClass", "placeholder", "formats", "modules", "bounds", "scrollingContainer", "debug", "readonly"], outputs: ["onInit", "onTextChange", "onSelectionChange"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: RequiredComponent, selector: "required" }] }); }
769
776
  }
770
777
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyEditorComponent, decorators: [{
771
778
  type: Component,
@@ -776,7 +783,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
776
783
  EditorModule,
777
784
  TooltipModule,
778
785
  RequiredComponent,
779
- ], template: "<!-- Can't put (onBlur) in this control -->\n\n<div style=\"display: flex; flex-direction: column; gap: 0.5rem\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n\n <!-- Disable doesn't work on this control -->\n <p-editor\n *ngIf=\"control\"\n [formControl]=\"control\"\n [pTooltip]=\"getValidationErrrorMessages()\"\n [tooltipEvent]=\"errorMessageTooltipEvent\"\n tooltipPosition=\"bottom\"\n [tooltipDisabled]=\"control.valid\"\n tooltipStyleClass=\"spiderly-tooltip-invalid\"\n [readonly]=\"control.disabled\"\n [class]=\"control.invalid && control.dirty ? 'control-error-border' : ''\"\n [id]=\"control.label\"\n [placeholder]=\"placeholder\"\n (click)=\"onClick()\"\n [style]=\"{ height: '320px' }\"\n ></p-editor>\n</div>\n" }]
786
+ ], template: "<!-- Can't put (onBlur) in this control -->\n\n<div style=\"display: flex; flex-direction: column; gap: 0.5rem; padding: 0 1px\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <required *ngIf=\"control?.required && showRequired\"></required>\n </div>\n\n <!-- Disable doesn't work on this control -->\n <p-editor\n *ngIf=\"control\"\n [formControl]=\"control\"\n [pTooltip]=\"getValidationErrrorMessages()\"\n [tooltipEvent]=\"errorMessageTooltipEvent\"\n tooltipPosition=\"bottom\"\n [tooltipDisabled]=\"control.valid\"\n tooltipStyleClass=\"spiderly-tooltip-invalid\"\n [readonly]=\"control.disabled\"\n [class]=\"control.invalid && control.dirty ? 'control-error-border' : ''\"\n [id]=\"control.label\"\n [placeholder]=\"placeholder\"\n (click)=\"onClick()\"\n [style]=\"{ height: '320px' }\"\n ></p-editor>\n</div>\n" }]
780
787
  }], ctorParameters: () => [{ type: i1.TranslocoService }], propDecorators: { editor: [{
781
788
  type: ViewChild,
782
789
  args: [Editor]
@@ -887,6 +894,7 @@ function getMimeTypeForFileName(fileName) {
887
894
  '.png': 'image/png',
888
895
  '.webp': 'image/webp',
889
896
  '.gif': 'image/gif',
897
+ '.svg': 'image/svg+xml',
890
898
  '.pdf': 'application/pdf',
891
899
  '.txt': 'text/plain',
892
900
  '.html': 'text/html',
@@ -1033,7 +1041,7 @@ function toCommaSeparatedString(input) {
1033
1041
  return stringList[0] ?? '';
1034
1042
  }
1035
1043
  }
1036
- function isImageFileType(mimeType) {
1044
+ function isFileImageType(mimeType) {
1037
1045
  if (mimeType.startsWith('image/')) {
1038
1046
  return true;
1039
1047
  }
@@ -1136,6 +1144,16 @@ const primitiveArrayTypes = [
1136
1144
  'Date[]',
1137
1145
  'string[]',
1138
1146
  ];
1147
+ function getImageDimensions(file) {
1148
+ return new Promise((resolve) => {
1149
+ const img = new Image();
1150
+ img.onload = () => {
1151
+ resolve({ width: img.width, height: img.height });
1152
+ URL.revokeObjectURL(img.src);
1153
+ };
1154
+ img.src = URL.createObjectURL(file);
1155
+ });
1156
+ }
1139
1157
 
1140
1158
  class SpiderlyMessageService {
1141
1159
  // TODO FT: nece da prikaze poruku ako je neki angular error koji se dogodi tek nakon api poziva
@@ -1221,7 +1239,7 @@ class SpiderlyFileComponent extends BaseControl {
1221
1239
  }
1222
1240
  filesSelected(event) {
1223
1241
  const file = event.files[0];
1224
- if (this.isImageFileType(file.type) &&
1242
+ if (this.isFileImageType(file.type) &&
1225
1243
  this.hasImageDimensionConstraints()) {
1226
1244
  this.files = [];
1227
1245
  this.validatorService
@@ -1240,10 +1258,17 @@ class SpiderlyFileComponent extends BaseControl {
1240
1258
  this.emitFileSelected(file);
1241
1259
  }
1242
1260
  }
1243
- emitFileSelected(file) {
1261
+ async emitFileSelected(file) {
1244
1262
  const formData = new FormData();
1245
1263
  formData.append('file', file, `${this.objectId}-${file.name}`);
1246
- this.onFileSelected.next(new SpiderlyFileSelectEvent({ file: file, formData: formData }));
1264
+ let width;
1265
+ let height;
1266
+ if (this.isFileImageType(file.type)) {
1267
+ const dimensions = await getImageDimensions(file);
1268
+ width = dimensions.width;
1269
+ height = dimensions.height;
1270
+ }
1271
+ this.onFileSelected.next(new SpiderlyFileSelectEvent({ file, formData, width, height }));
1247
1272
  }
1248
1273
  hasImageDimensionConstraints() {
1249
1274
  return this.imageWidth > 0 || this.imageHeight > 0;
@@ -1284,14 +1309,14 @@ class SpiderlyFileComponent extends BaseControl {
1284
1309
  const file = new File([blob], fileName, { type: mimeType });
1285
1310
  return file;
1286
1311
  }
1287
- isImageFileType(mimeType) {
1288
- return isImageFileType(mimeType);
1312
+ isFileImageType(mimeType) {
1313
+ return isFileImageType(mimeType);
1289
1314
  }
1290
1315
  isExcelFileType(mimeType) {
1291
1316
  return isExcelFileType(mimeType);
1292
1317
  }
1293
1318
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFileComponent, deps: [{ token: i1.TranslocoService }, { token: SpiderlyMessageService }, { token: ValidatorAbstractService }], target: i0.ɵɵFactoryTarget.Component }); }
1294
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyFileComponent, isStandalone: true, selector: "spiderly-file", inputs: { objectId: "objectId", fileData: "fileData", acceptedFileTypes: "acceptedFileTypes", required: "required", multiple: "multiple", isCloudinaryFileData: "isCloudinaryFileData", imageWidth: "imageWidth", imageHeight: "imageHeight", files: "files" }, outputs: { onFileSelected: "onFileSelected", onFileRemoved: "onFileRemoved" }, usesInheritance: true, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div style=\"display: flex; flex-direction: column; gap: 0.5rem; padding: 1px\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <!-- It's okay for this control, because for the custom uploads where we are not initializing the control from the backend, there is no need for formControl. -->\n <required *ngIf=\"control?.required || required\"></required>\n </div>\n\n <p-fileUpload\n [files]=\"files\"\n [disabled]=\"disabled\"\n [name]=\"control?.label ?? label\"\n [multiple]=\"multiple\"\n [accept]=\"acceptedFileTypesCommaSeparated\"\n [maxFileSize]=\"1000000\"\n (onSelect)=\"filesSelected($event)\"\n [class]=\"control?.invalid && control?.dirty ? 'control-error-border' : ''\"\n >\n <ng-template\n pTemplate=\"header\"\n let-files\n let-chooseCallback=\"chooseCallback\"\n let-clearCallback=\"clearCallback\"\n let-uploadCallback=\"uploadCallback\"\n >\n <div\n class=\"flex flex-wrap justify-content-between align-items-center flex-1 gap-2\"\n >\n <div class=\"flex gap-2\">\n <spiderly-button\n [disabled]=\"disabled\"\n (onClick)=\"choose($event, chooseCallback)\"\n icon=\"pi pi-upload\"\n [rounded]=\"true\"\n [outlined]=\"true\"\n />\n </div>\n </div>\n </ng-template>\n <ng-template\n pTemplate=\"content\"\n let-files\n let-removeFileCallback=\"removeFileCallback\"\n >\n <div *ngIf=\"files.length > 0\">\n <div class=\"flex justify-content-center p-0 gap-5\">\n <div\n *ngFor=\"let file of files; let index = index\"\n class=\"card m-0 px-3 py-3 flex flex-column align-items-center gap-3\"\n style=\"justify-content: center; overflow: hidden\"\n >\n <div *ngIf=\"isImageFileType(file.type)\" class=\"image-container\">\n <img role=\"presentation\" [src]=\"file.objectURL\" />\n </div>\n <div *ngIf=\"isExcelFileType(file.type)\" class=\"excel-container\">\n <div class=\"excel-details\">\n <i\n class=\"pi pi-file-excel\"\n style=\"color: green; margin-right: 4px\"\n ></i>\n <span class=\"file-name\">{{ file.name }}</span>\n </div>\n </div>\n <spiderly-button\n [disabled]=\"disabled\"\n icon=\"pi pi-times\"\n (onClick)=\"fileRemoved(removeFileCallback, index)\"\n [outlined]=\"true\"\n [rounded]=\"true\"\n severity=\"danger\"\n />\n </div>\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate=\"file\"> </ng-template>\n <ng-template pTemplate=\"empty\">\n <div class=\"flex align-items-center justify-content-center flex-column\">\n <i\n class=\"pi pi-cloud-upload border-2 border-circle p-5 text-8xl text-400 border-400 mt-3\"\n ></i>\n <p class=\"mt-4 mb-0\">{{ t(\"DragAndDropFilesHereToUpload\") }}</p>\n </div>\n </ng-template>\n </p-fileUpload>\n </div>\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: FileUploadModule }, { kind: "component", type: i5$2.FileUpload, selector: "p-fileupload, p-fileUpload", inputs: ["name", "url", "method", "multiple", "accept", "disabled", "auto", "withCredentials", "maxFileSize", "invalidFileSizeMessageSummary", "invalidFileSizeMessageDetail", "invalidFileTypeMessageSummary", "invalidFileTypeMessageDetail", "invalidFileLimitMessageDetail", "invalidFileLimitMessageSummary", "style", "styleClass", "previewWidth", "chooseLabel", "uploadLabel", "cancelLabel", "chooseIcon", "uploadIcon", "cancelIcon", "showUploadButton", "showCancelButton", "mode", "headers", "customUpload", "fileLimit", "uploadStyleClass", "cancelStyleClass", "removeStyleClass", "chooseStyleClass", "chooseButtonProps", "uploadButtonProps", "cancelButtonProps", "files"], outputs: ["onBeforeUpload", "onSend", "onUpload", "onError", "onClear", "onRemove", "onSelect", "onProgress", "uploadHandler", "onImageError", "onRemoveUploadedFile"] }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: RequiredComponent, selector: "required" }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
1319
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyFileComponent, isStandalone: true, selector: "spiderly-file", inputs: { objectId: "objectId", fileData: "fileData", acceptedFileTypes: "acceptedFileTypes", required: "required", multiple: "multiple", isCloudinaryFileData: "isCloudinaryFileData", imageWidth: "imageWidth", imageHeight: "imageHeight", files: "files" }, outputs: { onFileSelected: "onFileSelected", onFileRemoved: "onFileRemoved" }, usesInheritance: true, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div style=\"display: flex; flex-direction: column; gap: 0.5rem; padding: 1px\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <!-- It's okay for this control, because for the custom uploads where we are not initializing the control from the backend, there is no need for formControl. -->\n <required *ngIf=\"control?.required || required\"></required>\n </div>\n\n <p-fileUpload\n [files]=\"files\"\n [disabled]=\"disabled\"\n [name]=\"control?.label ?? label\"\n [multiple]=\"multiple\"\n [accept]=\"acceptedFileTypesCommaSeparated\"\n [maxFileSize]=\"1000000\"\n (onSelect)=\"filesSelected($event)\"\n [class]=\"control?.invalid && control?.dirty ? 'control-error-border' : ''\"\n >\n <ng-template\n pTemplate=\"header\"\n let-files\n let-chooseCallback=\"chooseCallback\"\n let-clearCallback=\"clearCallback\"\n let-uploadCallback=\"uploadCallback\"\n >\n <div\n class=\"flex flex-wrap justify-content-between align-items-center flex-1 gap-2\"\n >\n <div class=\"flex gap-2\">\n <spiderly-button\n [disabled]=\"disabled\"\n (onClick)=\"choose($event, chooseCallback)\"\n icon=\"pi pi-upload\"\n [rounded]=\"true\"\n [outlined]=\"true\"\n />\n </div>\n </div>\n </ng-template>\n <ng-template\n pTemplate=\"content\"\n let-files\n let-removeFileCallback=\"removeFileCallback\"\n >\n <div *ngIf=\"files.length > 0\">\n <div class=\"flex justify-content-center p-0 gap-5\">\n <div\n *ngFor=\"let file of files; let index = index\"\n class=\"card m-0 px-3 py-3 flex flex-column align-items-center gap-3\"\n style=\"justify-content: center; overflow: hidden\"\n >\n <div *ngIf=\"isFileImageType(file.type)\" class=\"image-container\">\n <img role=\"presentation\" [src]=\"file.objectURL\" />\n </div>\n <div *ngIf=\"isExcelFileType(file.type)\" class=\"excel-container\">\n <div class=\"excel-details\">\n <i\n class=\"pi pi-file-excel\"\n style=\"color: green; margin-right: 4px\"\n ></i>\n <span class=\"file-name\">{{ file.name }}</span>\n </div>\n </div>\n <spiderly-button\n [disabled]=\"disabled\"\n icon=\"pi pi-times\"\n (onClick)=\"fileRemoved(removeFileCallback, index)\"\n [outlined]=\"true\"\n [rounded]=\"true\"\n severity=\"danger\"\n />\n </div>\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate=\"file\"> </ng-template>\n <ng-template pTemplate=\"empty\">\n <div class=\"flex align-items-center justify-content-center flex-column\">\n <i\n class=\"pi pi-cloud-upload border-2 border-circle p-5 text-8xl text-400 border-400 mt-3\"\n ></i>\n <p class=\"mt-4 mb-0\">{{ t(\"DragAndDropFilesHereToUpload\") }}</p>\n </div>\n </ng-template>\n </p-fileUpload>\n </div>\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: FileUploadModule }, { kind: "component", type: i5$2.FileUpload, selector: "p-fileupload, p-fileUpload", inputs: ["name", "url", "method", "multiple", "accept", "disabled", "auto", "withCredentials", "maxFileSize", "invalidFileSizeMessageSummary", "invalidFileSizeMessageDetail", "invalidFileTypeMessageSummary", "invalidFileTypeMessageDetail", "invalidFileLimitMessageDetail", "invalidFileLimitMessageSummary", "style", "styleClass", "previewWidth", "chooseLabel", "uploadLabel", "cancelLabel", "chooseIcon", "uploadIcon", "cancelIcon", "showUploadButton", "showCancelButton", "mode", "headers", "customUpload", "fileLimit", "uploadStyleClass", "cancelStyleClass", "removeStyleClass", "chooseStyleClass", "chooseButtonProps", "uploadButtonProps", "cancelButtonProps", "files"], outputs: ["onBeforeUpload", "onSend", "onUpload", "onError", "onClear", "onRemove", "onSelect", "onProgress", "uploadHandler", "onImageError", "onRemoveUploadedFile"] }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: RequiredComponent, selector: "required" }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
1295
1320
  }
1296
1321
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFileComponent, decorators: [{
1297
1322
  type: Component,
@@ -1303,7 +1328,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1303
1328
  RequiredComponent,
1304
1329
  SpiderlyButtonComponent,
1305
1330
  TranslocoDirective,
1306
- ], template: "<ng-container *transloco=\"let t\">\n <div style=\"display: flex; flex-direction: column; gap: 0.5rem; padding: 1px\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <!-- It's okay for this control, because for the custom uploads where we are not initializing the control from the backend, there is no need for formControl. -->\n <required *ngIf=\"control?.required || required\"></required>\n </div>\n\n <p-fileUpload\n [files]=\"files\"\n [disabled]=\"disabled\"\n [name]=\"control?.label ?? label\"\n [multiple]=\"multiple\"\n [accept]=\"acceptedFileTypesCommaSeparated\"\n [maxFileSize]=\"1000000\"\n (onSelect)=\"filesSelected($event)\"\n [class]=\"control?.invalid && control?.dirty ? 'control-error-border' : ''\"\n >\n <ng-template\n pTemplate=\"header\"\n let-files\n let-chooseCallback=\"chooseCallback\"\n let-clearCallback=\"clearCallback\"\n let-uploadCallback=\"uploadCallback\"\n >\n <div\n class=\"flex flex-wrap justify-content-between align-items-center flex-1 gap-2\"\n >\n <div class=\"flex gap-2\">\n <spiderly-button\n [disabled]=\"disabled\"\n (onClick)=\"choose($event, chooseCallback)\"\n icon=\"pi pi-upload\"\n [rounded]=\"true\"\n [outlined]=\"true\"\n />\n </div>\n </div>\n </ng-template>\n <ng-template\n pTemplate=\"content\"\n let-files\n let-removeFileCallback=\"removeFileCallback\"\n >\n <div *ngIf=\"files.length > 0\">\n <div class=\"flex justify-content-center p-0 gap-5\">\n <div\n *ngFor=\"let file of files; let index = index\"\n class=\"card m-0 px-3 py-3 flex flex-column align-items-center gap-3\"\n style=\"justify-content: center; overflow: hidden\"\n >\n <div *ngIf=\"isImageFileType(file.type)\" class=\"image-container\">\n <img role=\"presentation\" [src]=\"file.objectURL\" />\n </div>\n <div *ngIf=\"isExcelFileType(file.type)\" class=\"excel-container\">\n <div class=\"excel-details\">\n <i\n class=\"pi pi-file-excel\"\n style=\"color: green; margin-right: 4px\"\n ></i>\n <span class=\"file-name\">{{ file.name }}</span>\n </div>\n </div>\n <spiderly-button\n [disabled]=\"disabled\"\n icon=\"pi pi-times\"\n (onClick)=\"fileRemoved(removeFileCallback, index)\"\n [outlined]=\"true\"\n [rounded]=\"true\"\n severity=\"danger\"\n />\n </div>\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate=\"file\"> </ng-template>\n <ng-template pTemplate=\"empty\">\n <div class=\"flex align-items-center justify-content-center flex-column\">\n <i\n class=\"pi pi-cloud-upload border-2 border-circle p-5 text-8xl text-400 border-400 mt-3\"\n ></i>\n <p class=\"mt-4 mb-0\">{{ t(\"DragAndDropFilesHereToUpload\") }}</p>\n </div>\n </ng-template>\n </p-fileUpload>\n </div>\n</ng-container>\n" }]
1331
+ ], template: "<ng-container *transloco=\"let t\">\n <div style=\"display: flex; flex-direction: column; gap: 0.5rem; padding: 1px\">\n <div *ngIf=\"getTranslatedLabel() != '' && getTranslatedLabel() != null\">\n <label>{{ getTranslatedLabel() }}</label>\n <!-- It's okay for this control, because for the custom uploads where we are not initializing the control from the backend, there is no need for formControl. -->\n <required *ngIf=\"control?.required || required\"></required>\n </div>\n\n <p-fileUpload\n [files]=\"files\"\n [disabled]=\"disabled\"\n [name]=\"control?.label ?? label\"\n [multiple]=\"multiple\"\n [accept]=\"acceptedFileTypesCommaSeparated\"\n [maxFileSize]=\"1000000\"\n (onSelect)=\"filesSelected($event)\"\n [class]=\"control?.invalid && control?.dirty ? 'control-error-border' : ''\"\n >\n <ng-template\n pTemplate=\"header\"\n let-files\n let-chooseCallback=\"chooseCallback\"\n let-clearCallback=\"clearCallback\"\n let-uploadCallback=\"uploadCallback\"\n >\n <div\n class=\"flex flex-wrap justify-content-between align-items-center flex-1 gap-2\"\n >\n <div class=\"flex gap-2\">\n <spiderly-button\n [disabled]=\"disabled\"\n (onClick)=\"choose($event, chooseCallback)\"\n icon=\"pi pi-upload\"\n [rounded]=\"true\"\n [outlined]=\"true\"\n />\n </div>\n </div>\n </ng-template>\n <ng-template\n pTemplate=\"content\"\n let-files\n let-removeFileCallback=\"removeFileCallback\"\n >\n <div *ngIf=\"files.length > 0\">\n <div class=\"flex justify-content-center p-0 gap-5\">\n <div\n *ngFor=\"let file of files; let index = index\"\n class=\"card m-0 px-3 py-3 flex flex-column align-items-center gap-3\"\n style=\"justify-content: center; overflow: hidden\"\n >\n <div *ngIf=\"isFileImageType(file.type)\" class=\"image-container\">\n <img role=\"presentation\" [src]=\"file.objectURL\" />\n </div>\n <div *ngIf=\"isExcelFileType(file.type)\" class=\"excel-container\">\n <div class=\"excel-details\">\n <i\n class=\"pi pi-file-excel\"\n style=\"color: green; margin-right: 4px\"\n ></i>\n <span class=\"file-name\">{{ file.name }}</span>\n </div>\n </div>\n <spiderly-button\n [disabled]=\"disabled\"\n icon=\"pi pi-times\"\n (onClick)=\"fileRemoved(removeFileCallback, index)\"\n [outlined]=\"true\"\n [rounded]=\"true\"\n severity=\"danger\"\n />\n </div>\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate=\"file\"> </ng-template>\n <ng-template pTemplate=\"empty\">\n <div class=\"flex align-items-center justify-content-center flex-column\">\n <i\n class=\"pi pi-cloud-upload border-2 border-circle p-5 text-8xl text-400 border-400 mt-3\"\n ></i>\n <p class=\"mt-4 mb-0\">{{ t(\"DragAndDropFilesHereToUpload\") }}</p>\n </div>\n </ng-template>\n </p-fileUpload>\n </div>\n</ng-container>\n" }]
1307
1332
  }], ctorParameters: () => [{ type: i1.TranslocoService }, { type: SpiderlyMessageService }, { type: ValidatorAbstractService }], propDecorators: { onFileSelected: [{
1308
1333
  type: Output
1309
1334
  }], onFileRemoved: [{
@@ -1328,10 +1353,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1328
1353
  type: Input
1329
1354
  }] } });
1330
1355
  class SpiderlyFileSelectEvent extends BaseEntity {
1331
- constructor({ file, formData, } = {}) {
1356
+ constructor({ file, formData, width, height, } = {}) {
1332
1357
  super();
1333
1358
  this.file = file;
1334
1359
  this.formData = formData;
1360
+ this.width = width;
1361
+ this.height = height;
1335
1362
  }
1336
1363
  static { this.typeName = 'SpiderlyFileSelectEvent'; }
1337
1364
  }
@@ -1548,12 +1575,12 @@ class UserRole extends BaseEntity {
1548
1575
  }
1549
1576
  class LoginVerificationToken extends BaseEntity {
1550
1577
  static { this.typeName = 'LoginVerificationToken'; }
1551
- constructor({ email, userId, browserId, expireAt, } = {}) {
1578
+ constructor({ email, userId, browserId, expiresAt, } = {}) {
1552
1579
  super();
1553
1580
  this.email = email;
1554
1581
  this.userId = userId;
1555
1582
  this.browserId = browserId;
1556
- this.expireAt = expireAt;
1583
+ this.expiresAt = expiresAt;
1557
1584
  }
1558
1585
  static { this.schema = {
1559
1586
  email: {
@@ -1565,7 +1592,7 @@ class LoginVerificationToken extends BaseEntity {
1565
1592
  browserId: {
1566
1593
  type: 'string',
1567
1594
  },
1568
- expireAt: {
1595
+ expiresAt: {
1569
1596
  type: 'Date',
1570
1597
  },
1571
1598
  }; }
@@ -1622,21 +1649,8 @@ class SpiderlyError extends Error {
1622
1649
  }
1623
1650
  }
1624
1651
 
1625
- class TranslateLabelsAbstractService {
1626
- constructor() { }
1627
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: TranslateLabelsAbstractService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1628
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: TranslateLabelsAbstractService, providedIn: 'root' }); }
1629
- }
1630
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: TranslateLabelsAbstractService, decorators: [{
1631
- type: Injectable,
1632
- args: [{
1633
- providedIn: 'root',
1634
- }]
1635
- }], ctorParameters: () => [] });
1636
-
1637
1652
  class BaseFormService {
1638
- constructor(translateLabelsService, validatorService, messageService, translocoService) {
1639
- this.translateLabelsService = translateLabelsService;
1653
+ constructor(validatorService, messageService, translocoService) {
1640
1654
  this.validatorService = validatorService;
1641
1655
  this.messageService = messageService;
1642
1656
  this.translocoService = translocoService;
@@ -1656,12 +1670,14 @@ class BaseFormService {
1656
1670
  propSchema.type !== 'Namebook[]') {
1657
1671
  if (existingControl instanceof SpiderlyFormArray) {
1658
1672
  this.initFormArray(existingControl, propSchema.nestedConstructor, propInitialValue);
1673
+ this.validatorService.setFormArrayValidator(existingControl, targetClass.typeName);
1659
1674
  }
1660
1675
  else {
1661
1676
  const control = new SpiderlyFormArray([], this.translocoService, this);
1662
1677
  this.initFormArray(control, propSchema.nestedConstructor, propInitialValue);
1663
1678
  control.label = formControlName;
1664
1679
  control.labelForDisplay = this.getTranslatedLabel(formControlName);
1680
+ this.validatorService.setFormArrayValidator(control, targetClass.typeName);
1665
1681
  formGroup.setControl(formControlName, control);
1666
1682
  }
1667
1683
  }
@@ -1681,6 +1697,9 @@ class BaseFormService {
1681
1697
  if (formControlName === 'id' && !propInitialValue) {
1682
1698
  propInitialValue = 0;
1683
1699
  }
1700
+ if (propSchema.type.endsWith('[]') && propInitialValue == null) {
1701
+ propInitialValue = [];
1702
+ }
1684
1703
  if (existingControl instanceof SpiderlyFormControl) {
1685
1704
  existingControl.setValue(propInitialValue);
1686
1705
  }
@@ -1755,7 +1774,7 @@ class BaseFormService {
1755
1774
  else if (formControlName.endsWith('DisplayName')) {
1756
1775
  formControlName = formControlName.replace('DisplayName', '');
1757
1776
  }
1758
- return this.translateLabelsService.translate(formControlName);
1777
+ return this.translocoService.translate(firstCharToUpper(formControlName));
1759
1778
  }
1760
1779
  addNewFormGroupToFormArray(formArray, targetClass, initialValues, index) {
1761
1780
  let helperFormGroup = new SpiderlyFormGroup({});
@@ -1815,6 +1834,11 @@ class BaseFormService {
1815
1834
  });
1816
1835
  }
1817
1836
  else if (control instanceof SpiderlyFormArray) {
1837
+ if (control.errors) {
1838
+ control.markAsDirty();
1839
+ this.messageService.warningMessage(control.errors['_']);
1840
+ invalid = true;
1841
+ }
1818
1842
  control.controls.forEach((nestedControl) => {
1819
1843
  if (!this.isControlValid(nestedControl)) {
1820
1844
  invalid = true;
@@ -1826,7 +1850,7 @@ class BaseFormService {
1826
1850
  }
1827
1851
  return true;
1828
1852
  }
1829
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: BaseFormService, deps: [{ token: TranslateLabelsAbstractService }, { token: ValidatorAbstractService }, { token: SpiderlyMessageService }, { token: i1.TranslocoService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1853
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: BaseFormService, deps: [{ token: ValidatorAbstractService }, { token: SpiderlyMessageService }, { token: i1.TranslocoService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1830
1854
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: BaseFormService, providedIn: 'root' }); }
1831
1855
  }
1832
1856
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: BaseFormService, decorators: [{
@@ -1834,7 +1858,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1834
1858
  args: [{
1835
1859
  providedIn: 'root',
1836
1860
  }]
1837
- }], ctorParameters: () => [{ type: TranslateLabelsAbstractService }, { type: ValidatorAbstractService }, { type: SpiderlyMessageService }, { type: i1.TranslocoService }] });
1861
+ }], ctorParameters: () => [{ type: ValidatorAbstractService }, { type: SpiderlyMessageService }, { type: i1.TranslocoService }] });
1838
1862
 
1839
1863
  class BaseFormCopy {
1840
1864
  constructor(differs, http, messageService, changeDetectorRef, router, route, translocoService, baseFormService) {
@@ -1846,28 +1870,81 @@ class BaseFormCopy {
1846
1870
  this.route = route;
1847
1871
  this.translocoService = translocoService;
1848
1872
  this.baseFormService = baseFormService;
1873
+ /**
1874
+ * The root form group that holds all form controls, typed to `TSaveBody`.
1875
+ * Assign `saveObservableMethod` on it to define the HTTP call used for saving.
1876
+ * The form controls are built automatically from the `TSaveBody` schema when you call
1877
+ * `baseFormService.initFormGroup(this.parentFormGroup, saveBodyClass, saveBody)`.
1878
+ *
1879
+ * @example
1880
+ * ```ts
1881
+ * this.parentFormGroup.saveObservableMethod = this.apiService.saveProduct;
1882
+ *
1883
+ * this.baseFormService.initFormGroup(
1884
+ * this.parentFormGroup,
1885
+ * ProductSaveBody,
1886
+ * saveBody,
1887
+ * );
1888
+ * ```
1889
+ */
1849
1890
  this.parentFormGroup = new SpiderlyFormGroup({});
1891
+ /**
1892
+ * The toast message displayed after a successful save.
1893
+ * Override this to customize the success notification text for a specific entity.
1894
+ * If you want to change the message for all entities, update the `SuccessfulSaveToastDescription` key
1895
+ * in your translation JSON file instead.
1896
+ *
1897
+ * @example
1898
+ * ```ts
1899
+ * this.successfulSaveToastDescription = 'Product saved successfully!';
1900
+ * ```
1901
+ */
1850
1902
  this.successfulSaveToastDescription = this.translocoService.translate('SuccessfulSaveToastDescription');
1851
1903
  //#region Model
1904
+ /**
1905
+ * Executes the save flow for the form. The execution order is:
1906
+ * 1. Builds the save body from the form's raw value.
1907
+ * 2. Calls {@link onBeforeSave} — use this to modify the save body before validation.
1908
+ * 3. Validates the form. If invalid, shows an error message and stops.
1909
+ * 4. Sends the save HTTP request via `saveObservableMethod`.
1910
+ * 5. Calls {@link onAfterSaveRequest} — fires immediately after the request is sent, before the response arrives.
1911
+ * 6. On successful response: shows a success toast, reroutes, and calls {@link onAfterSave}. The form is re-initialized only when `rerouteToParentSlugAfterSave` is `false`.
1912
+ *
1913
+ * @param rerouteToParentSlugAfterSave - When `true` (default), navigates to the parent URL after save. When `false`, re-initializes the form and navigates to the saved object's URL.
1914
+ *
1915
+ * @example
1916
+ * ```html
1917
+ * <button (click)="onSave()">Save</button>
1918
+ * ```
1919
+ *
1920
+ * @example
1921
+ * ```html
1922
+ * <!-- Save and stay on the saved object's page -->
1923
+ * <button (click)="onSave(false)">Save and stay</button>
1924
+ * ```
1925
+ */
1852
1926
  // onSave method is here only because of the hooks, we should move everything except them to the BaseFromService
1853
- this.onSave = (reroute = true) => {
1927
+ this.onSave = (rerouteToParentSlugAfterSave = true) => {
1854
1928
  if (!this.saveBodyClass)
1855
1929
  throw new SpiderlyError('You did not initialize saveBodyClass');
1856
1930
  if (!this.mainUIFormClass)
1857
1931
  throw new SpiderlyError('You did not initialize mainUIFormClass');
1858
- this.saveBody = this.parentFormGroup.initSaveBody();
1859
- this.onBeforeSave(this.saveBody);
1860
- this.saveBody = this.saveBody ?? this.parentFormGroup.getRawValue();
1932
+ let saveBody = this.parentFormGroup.getRawValue();
1933
+ this.onBeforeSave(saveBody);
1861
1934
  const isValid = this.baseFormService.isControlValid(this.parentFormGroup);
1862
1935
  if (isValid) {
1863
1936
  this.parentFormGroup
1864
- .saveObservableMethod(this.saveBody)
1937
+ .saveObservableMethod(saveBody)
1865
1938
  .subscribe((res) => {
1866
1939
  this.messageService.successMessage(this.successfulSaveToastDescription);
1867
- this.baseFormService.initFormGroup(this.parentFormGroup, this.mainUIFormClass, res);
1868
- if (reroute) {
1940
+ if (rerouteToParentSlugAfterSave) {
1941
+ this.rerouteToSavedObject(undefined);
1942
+ }
1943
+ else {
1944
+ saveBody = this.baseFormService.mapMainUIFormToSaveBody(this.mainUIFormClass, res);
1945
+ this.baseFormService.initFormGroup(this.parentFormGroup, this.saveBodyClass, saveBody);
1869
1946
  const saveBodyMainDTOKey = this.baseFormService.getSaveBodyMainDTOKey(this.saveBodyClass);
1870
- const savedObjectId = res[saveBodyMainDTOKey]?.id;
1947
+ const savedObjectId = saveBody[saveBodyMainDTOKey]?.id;
1871
1948
  this.rerouteToSavedObject(savedObjectId); // You always need to have id, because of id == 0 and version change
1872
1949
  }
1873
1950
  this.onAfterSave();
@@ -1878,6 +1955,21 @@ class BaseFormCopy {
1878
1955
  this.baseFormService.showInvalidFieldsMessage();
1879
1956
  }
1880
1957
  };
1958
+ /**
1959
+ * Handles navigation after a successful save.
1960
+ * Override this to customize the post-save navigation behavior.
1961
+ * By default, navigates to the parent URL when `rerouteId` is not provided, or to the saved object's URL otherwise.
1962
+ *
1963
+ * @param rerouteId - The ID of the saved object, used to build the target URL. When not provided, navigates to the parent URL.
1964
+ *
1965
+ * @example
1966
+ * ```ts
1967
+ * // Override to navigate to a custom route after save
1968
+ * rerouteToSavedObject = (rerouteId: number | string): void => {
1969
+ * this.router.navigateByUrl(`/products/${rerouteId}/details`);
1970
+ * };
1971
+ * ```
1972
+ */
1881
1973
  this.rerouteToSavedObject = (rerouteId) => {
1882
1974
  if (rerouteId == null) {
1883
1975
  const currentUrl = this.router.url;
@@ -1890,8 +1982,43 @@ class BaseFormCopy {
1890
1982
  const newUrl = segments.join('/');
1891
1983
  this.router.navigateByUrl(newUrl);
1892
1984
  };
1985
+ /**
1986
+ * Hook that runs **before** form validation and the save request.
1987
+ * Use this to modify the save body or perform any pre-save logic (e.g., transforming data, setting computed fields).
1988
+ *
1989
+ * @param saveBody - The current save body built from the form's raw value. Mutate it directly to change what gets sent to the server.
1990
+ *
1991
+ * @example
1992
+ * ```ts
1993
+ * onBeforeSave = (saveBody?: ProductSaveBody) => {
1994
+ * saveBody.productDTO.fullName = saveBody.productDTO.firstName + ' ' + saveBody.productDTO.lastName;
1995
+ * };
1996
+ * ```
1997
+ */
1893
1998
  this.onBeforeSave = (saveBody) => { };
1999
+ /**
2000
+ * Hook that runs **after** a successful save response is received.
2001
+ * Use this for post-save side effects (e.g., refreshing related data, showing additional notifications).
2002
+ *
2003
+ * @example
2004
+ * ```ts
2005
+ * onAfterSave = () => {
2006
+ * this.loadRelatedProducts();
2007
+ * };
2008
+ * ```
2009
+ */
1894
2010
  this.onAfterSave = () => { };
2011
+ /**
2012
+ * Hook that runs immediately **after** the save HTTP request is sent, but **before** the response arrives.
2013
+ * Use this for side effects that should happen as soon as the request is dispatched (e.g., disabling UI elements, starting a loading indicator).
2014
+ *
2015
+ * @example
2016
+ * ```ts
2017
+ * onAfterSaveRequest = () => {
2018
+ * this.isSaving = true;
2019
+ * };
2020
+ * ```
2021
+ */
1895
2022
  this.onAfterSaveRequest = () => { };
1896
2023
  }
1897
2024
  ngOnInit() { }
@@ -4379,21 +4506,6 @@ class LazyLoadSelectedIdsResult extends BaseEntity {
4379
4506
  class MenuChangeEvent {
4380
4507
  }
4381
4508
 
4382
- class MimeTypes {
4383
- constructor(value) {
4384
- this.value = value;
4385
- }
4386
- static { this.Pdf = new MimeTypes('application/pdf'); }
4387
- static { this.Zip = new MimeTypes('application/zip'); }
4388
- static { this.Jpeg = new MimeTypes('image/jpeg'); }
4389
- static { this.Png = new MimeTypes('image/png'); }
4390
- static { this.Svg = new MimeTypes('image/svg'); }
4391
- static { this.Webp = new MimeTypes('image/webp'); }
4392
- toString() {
4393
- return this.value;
4394
- }
4395
- }
4396
-
4397
4509
  class Namebook extends BaseEntity {
4398
4510
  constructor({ id, displayName, } = {}) {
4399
4511
  super();
@@ -4731,5 +4843,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
4731
4843
  * Generated bundle index. Do not edit.
4732
4844
  */
4733
4845
 
4734
- export { Action, AllClickEvent, ApiSecurityService, AppSidebarComponent, AuthGuard, AuthResult, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormCopy, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, ExternalProvider, Filter, FilterRule, FilterSortMeta, FooterComponent, GoogleButtonComponent, IndexCardComponent, InfoCardComponent, InitCompanyAuthDialogDetails, InitTopBarData, IsAuthorizedForSaveEvent, LastMenuIconIndexClicked, LayoutServiceBase, LazyLoadSelectedIdsResult, Login, LoginComponent, LoginVerificationComponent, LoginVerificationToken, MatchModeCodes, MenuChangeEvent, MenuitemComponent, MimeTypes, Namebook, NotAuthGuard, NotFoundComponent, PROPS_KEY, PaginatedResult, PanelBodyComponent, PanelFooterComponent, PanelHeaderComponent, PrimengOption, ProfileAvatarComponent, ReflectProp, RefreshTokenRequest, RequiredComponent, RowClickEvent, SecurityPermissionCodes, SendLoginVerificationEmailResult, SideMenuTopBarComponent, SidebarMenuComponent, SimpleSaveResult, SpiderlyAutocompleteComponent, SpiderlyButtonBaseComponent, SpiderlyButtonComponent, SpiderlyCalendarComponent, SpiderlyCardComponent, SpiderlyCheckboxComponent, SpiderlyColorPickerComponent, SpiderlyControlsModule, SpiderlyDataTableComponent, SpiderlyDataViewComponent, SpiderlyDeleteConfirmationComponent, SpiderlyDropdownComponent, SpiderlyEditorComponent, SpiderlyErrorHandler, SpiderlyFileComponent, SpiderlyFileSelectEvent, SpiderlyFormArray, SpiderlyFormControl, SpiderlyFormGroup, SpiderlyLayoutComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, TranslateLabelsAbstractService, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, capitalizeFirstChar, createFakeGoogleWrapper, deleteAction, exportListToExcel, firstCharToUpper, getFileNameFromContentDisposition, getHtmlImgDisplayString64, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, httpLoadingInterceptor, isExcelFileType, isImageFileType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, primitiveArrayTypes, pushAction, selectedTab, singleOrDefault, splitPascalCase, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
4846
+ export { Action, AllClickEvent, ApiSecurityService, AppSidebarComponent, AuthGuard, AuthResult, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormCopy, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, ExternalProvider, Filter, FilterRule, FilterSortMeta, FooterComponent, GoogleButtonComponent, IndexCardComponent, InfoCardComponent, InitCompanyAuthDialogDetails, InitTopBarData, IsAuthorizedForSaveEvent, LastMenuIconIndexClicked, LayoutServiceBase, LazyLoadSelectedIdsResult, Login, LoginComponent, LoginVerificationComponent, LoginVerificationToken, MatchModeCodes, MenuChangeEvent, MenuitemComponent, Namebook, NotAuthGuard, NotFoundComponent, PROPS_KEY, PaginatedResult, PanelBodyComponent, PanelFooterComponent, PanelHeaderComponent, PrimengOption, ProfileAvatarComponent, ReflectProp, RefreshTokenRequest, RequiredComponent, RowClickEvent, SecurityPermissionCodes, SendLoginVerificationEmailResult, SideMenuTopBarComponent, SidebarMenuComponent, SimpleSaveResult, SpiderlyAutocompleteComponent, SpiderlyButtonBaseComponent, SpiderlyButtonComponent, SpiderlyCalendarComponent, SpiderlyCardComponent, SpiderlyCheckboxComponent, SpiderlyColorPickerComponent, SpiderlyControlsModule, SpiderlyDataTableComponent, SpiderlyDataViewComponent, SpiderlyDeleteConfirmationComponent, SpiderlyDropdownComponent, SpiderlyEditorComponent, SpiderlyErrorHandler, SpiderlyFileComponent, SpiderlyFileSelectEvent, SpiderlyFormArray, SpiderlyFormControl, SpiderlyFormGroup, SpiderlyLayoutComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, capitalizeFirstChar, createFakeGoogleWrapper, deleteAction, exportListToExcel, firstCharToUpper, getFileNameFromContentDisposition, getHtmlImgDisplayString64, getImageDimensions, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, httpLoadingInterceptor, isExcelFileType, isFileImageType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, primitiveArrayTypes, pushAction, selectedTab, singleOrDefault, splitPascalCase, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
4735
4847
  //# sourceMappingURL=spiderly.mjs.map