spiderly 19.8.1 → 19.8.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.
Files changed (36) hide show
  1. package/fesm2022/spiderly.mjs +832 -657
  2. package/fesm2022/spiderly.mjs.map +1 -1
  3. package/lib/components/auth/auth-card/auth-card.component.d.ts +14 -0
  4. package/lib/components/auth/external-login/external-login.component.d.ts +20 -0
  5. package/lib/components/auth/external-provider-icons.d.ts +7 -0
  6. package/lib/components/auth/login/login.component.d.ts +7 -8
  7. package/lib/components/index-card/index-card.component.d.ts +2 -0
  8. package/lib/components/info-card/info-card.component.d.ts +1 -0
  9. package/lib/components/layout/profile-avatar/profile-avatar.component.d.ts +1 -0
  10. package/lib/components/spiderly-buttons/spiderly-button-base/spiderly-button-base.d.ts +3 -1
  11. package/lib/components/spiderly-data-table/spiderly-data-table.component.d.ts +6 -1
  12. package/lib/components/spiderly-data-view/spiderly-data-view.component.d.ts +2 -0
  13. package/lib/components/spiderly-panels/panel-header/panel-header.component.d.ts +2 -0
  14. package/lib/components/spiderly-panels/spiderly-panel/spiderly-panel.component.d.ts +3 -0
  15. package/lib/controls/base-control.d.ts +3 -0
  16. package/lib/controls/base-dropdown-control.d.ts +1 -0
  17. package/lib/controls/spiderly-autocomplete/spiderly-autocomplete.component.d.ts +1 -0
  18. package/lib/controls/spiderly-calendar/spiderly-calendar.component.d.ts +1 -0
  19. package/lib/controls/spiderly-colorpicker/spiderly-colorpicker.component.d.ts +1 -0
  20. package/lib/controls/spiderly-controls.module.d.ts +4 -3
  21. package/lib/controls/spiderly-markdown/spiderly-markdown.component.d.ts +37 -0
  22. package/lib/controls/spiderly-number/spiderly-number.component.d.ts +1 -0
  23. package/lib/controls/spiderly-password/spiderly-password.component.d.ts +1 -0
  24. package/lib/controls/spiderly-textbox/spiderly-textbox.component.d.ts +1 -0
  25. package/lib/entities/security-entities.d.ts +33 -1
  26. package/lib/errors/api-error-codes.d.ts +2 -0
  27. package/lib/interceptors/unauthorized.interceptor.d.ts +6 -0
  28. package/lib/services/api.service.security.d.ts +2 -3
  29. package/lib/services/app-layout.service.base.d.ts +0 -8
  30. package/lib/services/auth.service.base.d.ts +15 -18
  31. package/lib/services/config.service.base.d.ts +0 -2
  32. package/lib/services/helper-functions.d.ts +1 -6
  33. package/package.json +3 -2
  34. package/public-api.d.ts +4 -1
  35. package/lib/components/auth/partials/auth.component.d.ts +0 -19
  36. package/lib/components/spiderly-buttons/google-button/google-button.component.d.ts +0 -14
@@ -17,7 +17,7 @@ import { TooltipModule } from 'primeng/tooltip';
17
17
  import * as FileSaver from 'file-saver';
18
18
  import mime from 'mime';
19
19
  import 'reflect-metadata';
20
- import { map, Subject, throttleTime, BehaviorSubject, of, filter as filter$1, firstValueFrom, finalize as finalize$1, tap as tap$1 } from 'rxjs';
20
+ import { map, Subject, throttleTime, BehaviorSubject, of, filter as filter$1, firstValueFrom, take, finalize as finalize$1, tap as tap$1, throwError } from 'rxjs';
21
21
  import * as i4$2 from 'primeng/checkbox';
22
22
  import { CheckboxModule } from 'primeng/checkbox';
23
23
  import * as i4$3 from 'primeng/colorpicker';
@@ -36,7 +36,10 @@ import * as i3$1 from 'primeng/select';
36
36
  import { SelectModule } from 'primeng/select';
37
37
  import * as i4$8 from 'primeng/editor';
38
38
  import { EditorModule, Editor } from 'primeng/editor';
39
- import * as i5$2 from 'primeng/fileupload';
39
+ import * as i5$2 from 'primeng/tabs';
40
+ import { TabsModule } from 'primeng/tabs';
41
+ import { MarkdownComponent } from 'ngx-markdown';
42
+ import * as i5$3 from 'primeng/fileupload';
40
43
  import { FileUploadModule } from 'primeng/fileupload';
41
44
  import * as i12 from 'primeng/button';
42
45
  import { ButtonModule } from 'primeng/button';
@@ -46,10 +49,9 @@ import * as i3$2 from '@angular/router';
46
49
  import { NavigationEnd, RouterModule } from '@angular/router';
47
50
  import * as i1$1 from 'primeng/api';
48
51
  import { ConfirmationService } from 'primeng/api';
52
+ import { filter, map as map$1, finalize, catchError, delay, tap, takeUntil } from 'rxjs/operators';
49
53
  import * as i1$2 from '@angular/common/http';
50
54
  import { HttpParams, HttpHeaders, HttpErrorResponse, HttpResponse } from '@angular/common/http';
51
- import { map as map$1, finalize, delay, tap, filter, takeUntil, catchError } from 'rxjs/operators';
52
- import * as i3$3 from '@abacritt/angularx-social-login';
53
55
  import { InputOtp } from 'primeng/inputotp';
54
56
  import * as i2$1 from 'primeng/menu';
55
57
  import { MenuModule } from 'primeng/menu';
@@ -80,17 +82,22 @@ const ApiErrorCodes = {
80
82
  UniqueViolation: 'unique_violation',
81
83
  ForeignKeyViolation: 'foreign_key_violation',
82
84
  ConcurrencyConflict: 'concurrency_conflict',
85
+ EmailNotVerified: 'email_not_verified',
86
+ ExternalProviderNotConfigured: 'external_provider_not_configured',
83
87
  };
84
88
 
85
89
  class BaseControl {
86
90
  constructor(translocoService) {
87
91
  this.translocoService = translocoService;
88
92
  this.disabled = false;
93
+ /** Whether the field's label is rendered. Defaults to `true`. */
89
94
  this.showLabel = true;
95
+ /** Whether the required (*) indicator is shown next to the label. Defaults to `true`. */
90
96
  this.showRequired = true;
91
97
  this.label = null; // NgModel/Want custom translation
92
98
  this.controlValid = true; // NgModel
93
99
  this.placeholder = '';
100
+ /** Whether the info tooltip icon is shown next to the label. Defaults to `false`. */
94
101
  this.showTooltip = false;
95
102
  this.tooltipText = null;
96
103
  this.tooltipIcon = 'pi pi-info-circle';
@@ -143,6 +150,7 @@ class BaseDropdownControl extends BaseControl {
143
150
  constructor(translocoService) {
144
151
  super(translocoService);
145
152
  this.translocoService = translocoService;
153
+ /** Whether an addon button is shown next to the dropdown. Defaults to `false`. */
146
154
  this.showAddon = false;
147
155
  this.addonIcon = 'pi pi-ellipsis-h';
148
156
  this.placeholder = this.translocoService.translate('SelectFromTheList');
@@ -390,6 +398,7 @@ class SpiderlyAutocompleteComponent extends BaseAutocompleteControl {
390
398
  this.translocoService = translocoService;
391
399
  this.validatorService = validatorService;
392
400
  this.appendTo = 'body';
401
+ /** Whether a clear button is shown. Defaults to `true`. */
393
402
  this.showClear = true;
394
403
  this.helperFormControl = new SpiderlyFormControl(null, {
395
404
  updateOn: 'change',
@@ -622,10 +631,11 @@ function exportListToExcel(exportListToExcelObservableMethod, filter) {
622
631
  FileSaver.saveAs(res.body, decodeURIComponent(fileName));
623
632
  });
624
633
  }
634
+ function getPrimengNamebookOptions(namebookList) {
635
+ return namebookList.map((x) => ({ label: x.displayName, code: x.id }));
636
+ }
625
637
  function getPrimengDropdownNamebookOptions(getDropdownListObservable, parentEntityId) {
626
- return getDropdownListObservable(parentEntityId ?? 0).pipe(map((res) => {
627
- return res.map((x) => ({ label: x.displayName, code: x.id }));
628
- }));
638
+ return getDropdownListObservable(parentEntityId ?? 0).pipe(map((res) => getPrimengNamebookOptions(res)));
629
639
  }
630
640
  function getPrimengDropdownCodebookOptions(getDropdownListObservable) {
631
641
  return getDropdownListObservable().pipe(map((res) => {
@@ -675,25 +685,6 @@ function kebabToTitleCase(input) {
675
685
  .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
676
686
  .join(' ');
677
687
  }
678
- /**
679
- * Custom styling of the google button - https://medium.com/simform-engineering/implement-custom-google-sign-in-using-angular-16-9c93aeff6252
680
- */
681
- function createFakeGoogleWrapper() {
682
- const googleLoginWrapper = document.createElement('div');
683
- googleLoginWrapper.style.display = 'none';
684
- googleLoginWrapper.classList.add('custom-google-button');
685
- document.body.appendChild(googleLoginWrapper);
686
- window.google.accounts.id.renderButton(googleLoginWrapper, {
687
- type: 'icon',
688
- width: '200',
689
- });
690
- const googleLoginWrapperButton = googleLoginWrapper.querySelector('div[role=button]');
691
- return {
692
- click: () => {
693
- googleLoginWrapperButton?.click();
694
- },
695
- };
696
- }
697
688
  const PROPS_KEY = Symbol('props');
698
689
  function ReflectProp(target, propertyKey) {
699
690
  const props = Reflect.getMetadata(PROPS_KEY, target) || [];
@@ -720,6 +711,7 @@ class SpiderlyCalendarComponent extends BaseControl {
720
711
  constructor(translocoService) {
721
712
  super(translocoService);
722
713
  this.translocoService = translocoService;
714
+ /** Whether the time picker is shown in addition to the date. Defaults to `false`. */
723
715
  this.showTime = false;
724
716
  this.dateOnly = false;
725
717
  this.timeOnly = false;
@@ -880,6 +872,7 @@ class SpiderlyColorPickerComponent extends BaseControl {
880
872
  constructor(translocoService) {
881
873
  super(translocoService);
882
874
  this.translocoService = translocoService;
875
+ /** Whether a hex text input is shown alongside the color swatch. Defaults to `true`. */
883
876
  this.showInputTextField = true;
884
877
  }
885
878
  ngOnInit() {
@@ -937,6 +930,7 @@ class SpiderlyPasswordComponent extends BaseControl {
937
930
  constructor(translocoService) {
938
931
  super(translocoService);
939
932
  this.translocoService = translocoService;
933
+ /** Whether a password-strength meter is shown below the field. Defaults to `false`. */
940
934
  this.showPasswordStrength = false;
941
935
  }
942
936
  ngOnInit() {
@@ -962,6 +956,7 @@ class SpiderlyTextboxComponent extends BaseControl {
962
956
  constructor(translocoService) {
963
957
  super(translocoService);
964
958
  this.translocoService = translocoService;
959
+ /** Whether an icon button is appended to the input. Defaults to `false`. */
965
960
  this.showButton = false;
966
961
  this.onButtonClick = new EventEmitter();
967
962
  }
@@ -1039,6 +1034,7 @@ class SpiderlyNumberComponent extends BaseControl {
1039
1034
  constructor(translocoService) {
1040
1035
  super(translocoService);
1041
1036
  this.translocoService = translocoService;
1037
+ /** Whether increment/decrement spinner buttons are shown. Defaults to `true`. */
1042
1038
  this.showButtons = true;
1043
1039
  this.maxFractionDigits = 0;
1044
1040
  }
@@ -1171,6 +1167,114 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1171
1167
  type: Input
1172
1168
  }] } });
1173
1169
 
1170
+ /**
1171
+ * Markdown form control: a plain textarea (Write) with a rendered live preview (Preview),
1172
+ * arranged as tabs. The stored value is raw Markdown text.
1173
+ *
1174
+ * The preview is rendered with ngx-markdown (marked) and is intentionally APPROXIMATE — a
1175
+ * consuming storefront may render the same Markdown with a different engine/flavor.
1176
+ *
1177
+ * The <textarea> DOM is the source of truth for the text (the form control mirrors it, like
1178
+ * spiderly-editor mirrors Quill). We never splice control.value, because SpiderlyFormControl
1179
+ * defaults to updateOn:'blur' and would be stale relative to the focused textarea.
1180
+ *
1181
+ * When {@link uploadImageMethod} is provided (wired by the generator for properties with an
1182
+ * S3 public-storage attribute), pasting an image uploads it and, on success, inserts a
1183
+ * standard `![](url)` link at the caret via execCommand (which preserves the native undo
1184
+ * stack). Upload progress is shown out-of-band, not as a token in the text.
1185
+ */
1186
+ class SpiderlyMarkdownComponent extends BaseControl {
1187
+ constructor(translocoService) {
1188
+ super(translocoService);
1189
+ this.translocoService = translocoService;
1190
+ this.objectId = 0;
1191
+ this.pendingImageUploads = 0;
1192
+ this.imageUploadFailed = false;
1193
+ }
1194
+ ngOnInit() {
1195
+ super.ngOnInit();
1196
+ }
1197
+ onPaste(event) {
1198
+ // Only intercept when image upload is wired; otherwise let the default paste happen.
1199
+ if (!this.uploadImageMethod || this.control?.disabled)
1200
+ return;
1201
+ const imageFile = this.getPastedImage(event);
1202
+ if (!imageFile)
1203
+ return;
1204
+ event.preventDefault();
1205
+ const formData = new FormData();
1206
+ formData.append('file', imageFile, `${this.objectId}-${imageFile.name || 'pasted-image.png'}`);
1207
+ this.imageUploadFailed = false;
1208
+ this.pendingImageUploads++;
1209
+ this.uploadImageMethod(formData).subscribe({
1210
+ next: (result) => {
1211
+ this.pendingImageUploads--;
1212
+ this.insertImageMarkdown(result.url);
1213
+ },
1214
+ error: () => {
1215
+ this.pendingImageUploads--;
1216
+ this.imageUploadFailed = true;
1217
+ },
1218
+ });
1219
+ }
1220
+ getPastedImage(event) {
1221
+ const items = event.clipboardData?.items;
1222
+ if (!items)
1223
+ return null;
1224
+ for (let i = 0; i < items.length; i++) {
1225
+ if (items[i].type.startsWith('image/')) {
1226
+ return items[i].getAsFile();
1227
+ }
1228
+ }
1229
+ return null;
1230
+ }
1231
+ insertImageMarkdown(url) {
1232
+ const snippet = `![](${url})`;
1233
+ const textarea = this.textareaRef?.nativeElement;
1234
+ // Preferred path: the textarea is focused, so insert at the caret while preserving the
1235
+ // native undo stack. The resulting 'input' event syncs the control on blur, exactly like
1236
+ // the user typing — no manual setValue, no stale-model read.
1237
+ if (textarea && document.activeElement === textarea && document.execCommand('insertText', false, snippet)) {
1238
+ return;
1239
+ }
1240
+ // Fallback (textarea blurred/absent, or execCommand unsupported): read the LIVE textarea
1241
+ // value — never control.value, which is stale while focused. When blurred, the control is
1242
+ // already current, so appending can't drop uncommitted text.
1243
+ if (textarea) {
1244
+ const sep = textarea.value.length ? '\n' : '';
1245
+ textarea.value = `${textarea.value}${sep}${snippet}`;
1246
+ this.control.setValue(textarea.value);
1247
+ }
1248
+ else {
1249
+ const current = this.control.value ?? '';
1250
+ this.control.setValue(current.length ? `${current}\n${snippet}` : snippet);
1251
+ }
1252
+ this.control.markAsDirty();
1253
+ }
1254
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyMarkdownComponent, deps: [{ token: i1.TranslocoService }], target: i0.ɵɵFactoryTarget.Component }); }
1255
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyMarkdownComponent, isStandalone: true, selector: "spiderly-markdown", inputs: { uploadImageMethod: "uploadImageMethod", objectId: "objectId" }, viewQueries: [{ propertyName: "textareaRef", first: true, predicate: ["textarea"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<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 <ng-container *transloco=\"let t\">\n <p-tabs *ngIf=\"control\" value=\"write\">\n <p-tablist>\n <p-tab value=\"write\">{{ t('Write') }}</p-tab>\n <p-tab value=\"preview\">{{ t('Preview') }}</p-tab>\n </p-tablist>\n <p-tabpanels>\n <p-tabpanel value=\"write\">\n <textarea\n #textarea\n pTextarea\n [formControl]=\"control\"\n [id]=\"control.label\"\n (blur)=\"control.markAsDirty()\"\n (paste)=\"onPaste($event)\"\n [autoResize]=\"true\"\n [class]=\"control.disabled ? 'disabled' : ''\"\n [style]=\"{ width: '100%', minHeight: '320px' }\"\n ></textarea>\n <small *ngIf=\"pendingImageUploads > 0\">{{ t('UploadingImage') }}</small>\n <small *ngIf=\"imageUploadFailed\" class=\"spiderly-error-message\">{{ t('ImageUploadFailed') }}</small>\n </p-tabpanel>\n <p-tabpanel value=\"preview\">\n <div class=\"spiderly-markdown-preview\" style=\"min-height: 320px\">\n <markdown [data]=\"control.value\"></markdown>\n </div>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n </ng-container>\n\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\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.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: 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: TextareaModule }, { kind: "directive", type: i4$6.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["autoResize", "variant", "fluid", "pSize"], outputs: ["onResize"] }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5$2.Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: i5$2.TabPanels, selector: "p-tabpanels" }, { kind: "component", type: i5$2.TabPanel, selector: "p-tabpanel", inputs: ["value"], outputs: ["valueChange"] }, { kind: "component", type: i5$2.TabList, selector: "p-tablist" }, { kind: "component", type: i5$2.Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: RequiredComponent, selector: "required" }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
1256
+ }
1257
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyMarkdownComponent, decorators: [{
1258
+ type: Component,
1259
+ args: [{ selector: 'spiderly-markdown', imports: [
1260
+ CommonModule,
1261
+ ReactiveFormsModule,
1262
+ FormsModule,
1263
+ TextareaModule,
1264
+ TabsModule,
1265
+ MarkdownComponent,
1266
+ RequiredComponent,
1267
+ TranslocoDirective,
1268
+ ], template: "<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 <ng-container *transloco=\"let t\">\n <p-tabs *ngIf=\"control\" value=\"write\">\n <p-tablist>\n <p-tab value=\"write\">{{ t('Write') }}</p-tab>\n <p-tab value=\"preview\">{{ t('Preview') }}</p-tab>\n </p-tablist>\n <p-tabpanels>\n <p-tabpanel value=\"write\">\n <textarea\n #textarea\n pTextarea\n [formControl]=\"control\"\n [id]=\"control.label\"\n (blur)=\"control.markAsDirty()\"\n (paste)=\"onPaste($event)\"\n [autoResize]=\"true\"\n [class]=\"control.disabled ? 'disabled' : ''\"\n [style]=\"{ width: '100%', minHeight: '320px' }\"\n ></textarea>\n <small *ngIf=\"pendingImageUploads > 0\">{{ t('UploadingImage') }}</small>\n <small *ngIf=\"imageUploadFailed\" class=\"spiderly-error-message\">{{ t('ImageUploadFailed') }}</small>\n </p-tabpanel>\n <p-tabpanel value=\"preview\">\n <div class=\"spiderly-markdown-preview\" style=\"min-height: 320px\">\n <markdown [data]=\"control.value\"></markdown>\n </div>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n </ng-container>\n\n <small *ngIf=\"control?.errors && control?.dirty\" class=\"spiderly-error-message\">\n {{ getValidationErrrorMessages() }}\n </small>\n</div>\n" }]
1269
+ }], ctorParameters: () => [{ type: i1.TranslocoService }], propDecorators: { textareaRef: [{
1270
+ type: ViewChild,
1271
+ args: ['textarea']
1272
+ }], uploadImageMethod: [{
1273
+ type: Input
1274
+ }], objectId: [{
1275
+ type: Input
1276
+ }] } });
1277
+
1174
1278
  class SpiderlyButtonBaseComponent {
1175
1279
  constructor(router) {
1176
1280
  this.router = router;
@@ -1199,13 +1303,15 @@ class SpiderlyButtonBaseComponent {
1199
1303
  this.subscription.unsubscribe();
1200
1304
  }
1201
1305
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyButtonBaseComponent, deps: [{ token: i3$2.Router }], target: i0.ɵɵFactoryTarget.Component }); }
1202
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyButtonBaseComponent, isStandalone: true, selector: "spiderly-button-base", inputs: { icon: "icon", label: "label", outlined: "outlined", rounded: "rounded", styleClass: "styleClass", routerLink: "routerLink", style: "style", class: "class", severity: "severity", size: "size", disabled: "disabled" }, outputs: { onClick: "onClick" }, ngImport: i0, template: ``, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ButtonModule }, { kind: "ngmodule", type: SplitButtonModule }] }); }
1306
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyButtonBaseComponent, isStandalone: true, selector: "spiderly-button-base", inputs: { icon: "icon", iconUrl: "iconUrl", label: "label", outlined: "outlined", rounded: "rounded", styleClass: "styleClass", routerLink: "routerLink", style: "style", class: "class", severity: "severity", size: "size", disabled: "disabled" }, outputs: { onClick: "onClick" }, ngImport: i0, template: ``, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ButtonModule }, { kind: "ngmodule", type: SplitButtonModule }] }); }
1203
1307
  }
1204
1308
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyButtonBaseComponent, decorators: [{
1205
1309
  type: Component,
1206
1310
  args: [{ selector: 'spiderly-button-base', template: ``, imports: [CommonModule, ButtonModule, SplitButtonModule] }]
1207
1311
  }], ctorParameters: () => [{ type: i3$2.Router }], propDecorators: { icon: [{
1208
1312
  type: Input
1313
+ }], iconUrl: [{
1314
+ type: Input
1209
1315
  }], label: [{
1210
1316
  type: Input
1211
1317
  }], outlined: [{
@@ -1239,11 +1345,11 @@ class SpiderlyButtonComponent extends SpiderlyButtonBaseComponent {
1239
1345
  this.type = 'button';
1240
1346
  }
1241
1347
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyButtonComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
1242
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyButtonComponent, isStandalone: true, selector: "spiderly-button", inputs: { type: "type" }, usesInheritance: true, ngImport: i0, template: "<p-button\n (onClick)=\"handleClick($event)\"\n [label]=\"label\"\n [icon]=\"icon\"\n [outlined]=\"outlined\"\n [styleClass]=\"styleClass\"\n [severity]=\"severity\"\n [rounded]=\"rounded\"\n [style]=\"style\"\n [class]=\"class\"\n [disabled]=\"disabled\"\n [size]=\"size\"\n [type]=\"type\"\n>\n <ng-content></ng-content>\n</p-button>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i12.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "fluid", "buttonProps"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: SplitButtonModule }] }); }
1348
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyButtonComponent, isStandalone: true, selector: "spiderly-button", inputs: { type: "type" }, usesInheritance: true, ngImport: i0, template: "<p-button\n (onClick)=\"handleClick($event)\"\n [label]=\"iconUrl ? undefined : label\"\n [icon]=\"icon\"\n [outlined]=\"outlined\"\n [styleClass]=\"styleClass\"\n [severity]=\"severity\"\n [rounded]=\"rounded\"\n [style]=\"style\"\n [class]=\"class\"\n [disabled]=\"disabled\"\n [size]=\"size\"\n [type]=\"type\"\n>\n <ng-container *ngIf=\"iconUrl\">\n <img [src]=\"iconUrl\" [alt]=\"label\" style=\"height: 16px; width: 16px; margin-right: 8px\" />\n <span>{{ label }}</span>\n </ng-container>\n <ng-content></ng-content>\n</p-button>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i12.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "fluid", "buttonProps"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: SplitButtonModule }] }); }
1243
1349
  }
1244
1350
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyButtonComponent, decorators: [{
1245
1351
  type: Component,
1246
- args: [{ selector: 'spiderly-button', imports: [CommonModule, ButtonModule, SplitButtonModule], template: "<p-button\n (onClick)=\"handleClick($event)\"\n [label]=\"label\"\n [icon]=\"icon\"\n [outlined]=\"outlined\"\n [styleClass]=\"styleClass\"\n [severity]=\"severity\"\n [rounded]=\"rounded\"\n [style]=\"style\"\n [class]=\"class\"\n [disabled]=\"disabled\"\n [size]=\"size\"\n [type]=\"type\"\n>\n <ng-content></ng-content>\n</p-button>\n" }]
1352
+ args: [{ selector: 'spiderly-button', imports: [CommonModule, ButtonModule, SplitButtonModule], template: "<p-button\n (onClick)=\"handleClick($event)\"\n [label]=\"iconUrl ? undefined : label\"\n [icon]=\"icon\"\n [outlined]=\"outlined\"\n [styleClass]=\"styleClass\"\n [severity]=\"severity\"\n [rounded]=\"rounded\"\n [style]=\"style\"\n [class]=\"class\"\n [disabled]=\"disabled\"\n [size]=\"size\"\n [type]=\"type\"\n>\n <ng-container *ngIf=\"iconUrl\">\n <img [src]=\"iconUrl\" [alt]=\"label\" style=\"height: 16px; width: 16px; margin-right: 8px\" />\n <span>{{ label }}</span>\n </ng-container>\n <ng-content></ng-content>\n</p-button>\n" }]
1247
1353
  }], propDecorators: { type: [{
1248
1354
  type: Input
1249
1355
  }] } });
@@ -1415,7 +1521,7 @@ class SpiderlyFileComponent extends BaseControl {
1415
1521
  return isExcelFileType(mimeType);
1416
1522
  }
1417
1523
  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 }); }
1418
- 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", isUrlFileData: "isUrlFileData", imageWidth: "imageWidth", imageHeight: "imageHeight", maxFileSize: "maxFileSize", 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]=\"maxFileSize\"\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-between 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=\"existingFileUrl\">\n <div class=\"flex justify-center p-0 gap-8\">\n <div\n class=\"card m-0 px-4 py-4 flex flex-col items-center gap-4\"\n style=\"justify-content: center; overflow: hidden\"\n >\n <div *ngIf=\"existingFileIsImage\" class=\"image-container\">\n <img [src]=\"existingFileUrl\" />\n </div>\n <div *ngIf=\"!existingFileIsImage\">\n <i class=\"pi pi-file\" style=\"margin-right: 4px\"></i>\n <span>{{ existingFileName }}</span>\n </div>\n <spiderly-button\n [disabled]=\"disabled\"\n icon=\"pi pi-times\"\n (onClick)=\"removeExistingFile()\"\n [outlined]=\"true\"\n [rounded]=\"true\"\n severity=\"danger\"\n />\n </div>\n </div>\n </div>\n <div *ngIf=\"!existingFileUrl && files.length > 0\">\n <div class=\"flex justify-center p-0 gap-8\">\n <div\n *ngFor=\"let file of files; let index = index\"\n class=\"card m-0 px-4 py-4 flex flex-col items-center gap-4\"\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 *ngIf=\"!existingFileUrl\" class=\"flex items-center justify-center flex-col\">\n <i\n class=\"pi pi-cloud-upload border-2 rounded-full p-8 text-8xl text-gray-400 border-gray-400 mt-4\"\n ></i>\n <p class=\"mt-6 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$1.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"] }] }); }
1524
+ 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", isUrlFileData: "isUrlFileData", imageWidth: "imageWidth", imageHeight: "imageHeight", maxFileSize: "maxFileSize", 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]=\"maxFileSize\"\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-between 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=\"existingFileUrl\">\n <div class=\"flex justify-center p-0 gap-8\">\n <div\n class=\"card m-0 px-4 py-4 flex flex-col items-center gap-4\"\n style=\"justify-content: center; overflow: hidden\"\n >\n <div *ngIf=\"existingFileIsImage\" class=\"image-container\">\n <img [src]=\"existingFileUrl\" />\n </div>\n <div *ngIf=\"!existingFileIsImage\">\n <i class=\"pi pi-file\" style=\"margin-right: 4px\"></i>\n <span>{{ existingFileName }}</span>\n </div>\n <spiderly-button\n [disabled]=\"disabled\"\n icon=\"pi pi-times\"\n (onClick)=\"removeExistingFile()\"\n [outlined]=\"true\"\n [rounded]=\"true\"\n severity=\"danger\"\n />\n </div>\n </div>\n </div>\n <div *ngIf=\"!existingFileUrl && files.length > 0\">\n <div class=\"flex justify-center p-0 gap-8\">\n <div\n *ngFor=\"let file of files; let index = index\"\n class=\"card m-0 px-4 py-4 flex flex-col items-center gap-4\"\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 *ngIf=\"!existingFileUrl\" class=\"flex items-center justify-center flex-col\">\n <i\n class=\"pi pi-cloud-upload border-2 rounded-full p-8 text-8xl text-gray-400 border-gray-400 mt-4\"\n ></i>\n <p class=\"mt-6 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$3.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$1.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"] }] }); }
1419
1525
  }
1420
1526
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFileComponent, decorators: [{
1421
1527
  type: Component,
@@ -1508,6 +1614,7 @@ class SpiderlyControlsModule {
1508
1614
  SpiderlyNumberComponent,
1509
1615
  SpiderlyDropdownComponent,
1510
1616
  SpiderlyEditorComponent,
1617
+ SpiderlyMarkdownComponent,
1511
1618
  SpiderlyColorPickerComponent,
1512
1619
  SpiderlyFileComponent], exports: [SpiderlyTextboxComponent,
1513
1620
  SpiderlyTextareaComponent,
@@ -1522,6 +1629,7 @@ class SpiderlyControlsModule {
1522
1629
  SpiderlyNumberComponent,
1523
1630
  SpiderlyDropdownComponent,
1524
1631
  SpiderlyEditorComponent,
1632
+ SpiderlyMarkdownComponent,
1525
1633
  SpiderlyColorPickerComponent,
1526
1634
  SpiderlyFileComponent] }); }
1527
1635
  static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyControlsModule, imports: [SpiderlyTextboxComponent,
@@ -1537,6 +1645,7 @@ class SpiderlyControlsModule {
1537
1645
  SpiderlyNumberComponent,
1538
1646
  SpiderlyDropdownComponent,
1539
1647
  SpiderlyEditorComponent,
1648
+ SpiderlyMarkdownComponent,
1540
1649
  SpiderlyColorPickerComponent,
1541
1650
  SpiderlyFileComponent] }); }
1542
1651
  }
@@ -1557,6 +1666,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1557
1666
  SpiderlyNumberComponent,
1558
1667
  SpiderlyDropdownComponent,
1559
1668
  SpiderlyEditorComponent,
1669
+ SpiderlyMarkdownComponent,
1560
1670
  SpiderlyColorPickerComponent,
1561
1671
  SpiderlyFileComponent,
1562
1672
  ],
@@ -1574,6 +1684,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1574
1684
  SpiderlyNumberComponent,
1575
1685
  SpiderlyDropdownComponent,
1576
1686
  SpiderlyEditorComponent,
1687
+ SpiderlyMarkdownComponent,
1577
1688
  SpiderlyColorPickerComponent,
1578
1689
  SpiderlyFileComponent,
1579
1690
  ],
@@ -1582,95 +1693,494 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1582
1693
  }]
1583
1694
  }] });
1584
1695
 
1585
- class UserBase extends BaseEntity {
1586
- static { this.typeName = 'UserBase'; }
1587
- constructor({ id, email, } = {}) {
1696
+ class InitCompanyAuthDialogDetails extends BaseEntity {
1697
+ constructor({ image, companyName, } = {}) {
1588
1698
  super();
1589
- this.id = id;
1590
- this.email = email;
1699
+ this.image = image;
1700
+ this.companyName = companyName;
1591
1701
  }
1592
- static { this.schema = {
1593
- id: {
1594
- type: 'number',
1595
- },
1596
- email: {
1597
- type: 'string',
1598
- },
1599
- }; }
1702
+ static { this.typeName = 'InitCompanyAuthDialogDetails'; }
1600
1703
  }
1601
- class AuthResult extends BaseEntity {
1602
- static { this.typeName = 'AuthResult'; }
1603
- constructor({ userId, email, accessToken, refreshToken, } = {}) {
1604
- super();
1605
- this.userId = userId;
1606
- this.email = email;
1607
- this.accessToken = accessToken;
1608
- this.refreshToken = refreshToken;
1704
+
1705
+ class ConfigServiceBase {
1706
+ constructor() {
1707
+ this.production = false;
1708
+ this.frontendUrl = 'http://localhost:4200';
1709
+ this.companyName = 'Company Name';
1710
+ this.primaryColor = '#111b2c';
1711
+ /* URLs */
1712
+ this.loginSlug = 'login';
1713
+ /* Local storage */
1714
+ this.accessTokenKey = 'access_token';
1715
+ this.refreshTokenKey = 'refresh_token';
1716
+ this.browserIdKey = 'browser_id';
1717
+ this.httpOptions = {};
1718
+ this.httpSkipSpinnerOptions = {
1719
+ headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
1720
+ params: new HttpParams().set('X-Skip-Spinner', 'true'),
1721
+ };
1722
+ this.logoPath = 'assets/images/logo/logo.svg';
1723
+ /* Pagination */
1724
+ this.defaultPageSize = 10;
1609
1725
  }
1610
- static { this.schema = {
1611
- userId: {
1612
- type: 'number',
1613
- },
1614
- email: {
1615
- type: 'string',
1616
- },
1617
- accessToken: {
1618
- type: 'string',
1619
- },
1620
- refreshToken: {
1621
- type: 'string',
1622
- },
1623
- }; }
1726
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ConfigServiceBase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1727
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ConfigServiceBase, providedIn: 'root' }); }
1624
1728
  }
1625
- class VerificationTokenRequest extends BaseEntity {
1626
- static { this.typeName = 'VerificationTokenRequest'; }
1627
- constructor({ verificationCode, browserId, email, } = {}) {
1628
- super();
1629
- this.verificationCode = verificationCode;
1630
- this.browserId = browserId;
1631
- this.email = email;
1729
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ConfigServiceBase, decorators: [{
1730
+ type: Injectable,
1731
+ args: [{
1732
+ providedIn: 'root',
1733
+ }]
1734
+ }], ctorParameters: () => [] });
1735
+
1736
+ class ApiSecurityService {
1737
+ constructor(http, config) {
1738
+ this.http = http;
1739
+ this.config = config;
1740
+ //#region Authentication
1741
+ this.login = (request) => {
1742
+ return this.http.post(`${this.config.apiUrl}/Security/Login`, request, this.config.httpOptions);
1743
+ };
1744
+ this.sendLoginVerificationEmail = (loginDTO) => {
1745
+ return this.http.post(`${this.config.apiUrl}/Security/SendLoginVerificationEmail`, loginDTO, this.config.httpOptions);
1746
+ };
1747
+ this.loginWithCookies = (request) => {
1748
+ return this.http.post(`${this.config.apiUrl}/Security/LoginWithCookies`, request, this.config.httpOptions);
1749
+ };
1750
+ this.getExternalProviders = () => {
1751
+ return this.http.get(`${this.config.apiUrl}/Security/GetExternalProviders`, this.config.httpSkipSpinnerOptions);
1752
+ };
1753
+ this.logout = (browserId) => {
1754
+ return this.http.get(`${this.config.apiUrl}/Security/Logout?browserId=${browserId}`);
1755
+ };
1756
+ this.logoutWithCookies = (browserId) => {
1757
+ return this.http.get(`${this.config.apiUrl}/Security/LogoutWithCookies?browserId=${browserId}`);
1758
+ };
1759
+ this.refreshTokenWithHeaders = (request) => {
1760
+ return this.http.post(`${this.config.apiUrl}/Security/RefreshTokenWithHeaders`, request, this.config.httpOptions);
1761
+ };
1762
+ this.refreshTokenWithCookies = (browserId) => {
1763
+ // POST, not GET: refresh rotates the single-use token (a state mutation), and a cacheable GET let
1764
+ // browsers replay a stale logged-in response on back/forward navigation (phantom dashboard after logout).
1765
+ return this.http.post(`${this.config.apiUrl}/Security/RefreshTokenWithCookies?browserId=${browserId}`, null);
1766
+ };
1767
+ //#endregion
1768
+ //#region User
1769
+ this.getCurrentUserBase = () => {
1770
+ return this.http.get(`${this.config.apiUrl}/Security/GetCurrentUserBase`, this.config.httpSkipSpinnerOptions);
1771
+ };
1772
+ this.getCurrentUserPermissionCodes = () => {
1773
+ return this.http.get(`${this.config.apiUrl}/Security/GetCurrentUserPermissionCodes`, this.config.httpSkipSpinnerOptions);
1774
+ };
1632
1775
  }
1633
- static { this.schema = {
1634
- verificationCode: {
1635
- type: 'string',
1636
- },
1637
- browserId: {
1638
- type: 'string',
1639
- },
1640
- email: {
1641
- type: 'string',
1642
- },
1643
- }; }
1776
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ApiSecurityService, deps: [{ token: i1$2.HttpClient }, { token: ConfigServiceBase }], target: i0.ɵɵFactoryTarget.Injectable }); }
1777
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ApiSecurityService, providedIn: 'root' }); }
1644
1778
  }
1645
- class ExternalProvider extends BaseEntity {
1646
- static { this.typeName = 'ExternalProvider'; }
1647
- constructor({ idToken, browserId, } = {}) {
1648
- super();
1649
- this.idToken = idToken;
1650
- this.browserId = browserId;
1779
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ApiSecurityService, decorators: [{
1780
+ type: Injectable,
1781
+ args: [{
1782
+ providedIn: 'root',
1783
+ }]
1784
+ }], ctorParameters: () => [{ type: i1$2.HttpClient }, { type: ConfigServiceBase }] });
1785
+
1786
+ /**
1787
+ * Cookie-based session auth. The access/refresh JWTs live in HttpOnly cookies set by the backend
1788
+ * (so JS never holds them — XSS can't exfiltrate them); requests are authenticated via
1789
+ * `withCredentials` (see jwtInterceptor). The readable result only carries userId/email/expiry.
1790
+ */
1791
+ class AuthServiceBase {
1792
+ constructor(router, http, apiService, config, platformId) {
1793
+ this.router = router;
1794
+ this.http = http;
1795
+ this.apiService = apiService;
1796
+ this.config = config;
1797
+ this.platformId = platformId;
1798
+ this.apiUrl = this.config.apiUrl;
1799
+ // External-login error code captured from the bootstrap URL (?externalAuthError=expired|failed) set by the
1800
+ // backend's OAuth callback on failure. Captured before routing can strip it, surfaced once by the login page.
1801
+ this.externalAuthErrorCode = null;
1802
+ this._currentUserPermissionCodes = new BehaviorSubject(undefined);
1803
+ // The subject seeds with `undefined` (not yet loaded) and emits `null` on logout. Consumers only ever care
1804
+ // about a real code list, so filter both out here — subscribers get a clean `string[]`, and `firstValueFrom`
1805
+ // waits for the first loaded value instead of grabbing the `undefined` seed in a load race.
1806
+ this.currentUserPermissionCodes$ = this._currentUserPermissionCodes
1807
+ .asObservable()
1808
+ .pipe(filter((codes) => codes != null));
1809
+ this._user = new BehaviorSubject(undefined);
1810
+ this.user$ = this._user.asObservable();
1811
+ // Cross-tab sync. We store only marker values here (never tokens — those are HttpOnly cookies).
1812
+ this.storageEventListener = (event) => {
1813
+ if (event.storageArea === localStorage) {
1814
+ if (event.key === 'logout-event') {
1815
+ this.stopTokenTimer();
1816
+ this._user.next(null);
1817
+ this._currentUserPermissionCodes.next(null);
1818
+ }
1819
+ if (event.key === 'login-event') {
1820
+ this.refreshToken().subscribe();
1821
+ }
1822
+ }
1823
+ };
1824
+ this.onAfterLogout = () => {
1825
+ this._currentUserPermissionCodes.next(null);
1826
+ this.router.navigate([this.config.loginSlug]);
1827
+ };
1828
+ this.onAfterRefreshToken = () => {
1829
+ this.setCurrentUserPermissionCodes().subscribe(); // after the session is re-established
1830
+ };
1831
+ this.initCompanyAuthDialogDetails = () => {
1832
+ return of(new InitCompanyAuthDialogDetails({
1833
+ image: this.config.logoPath,
1834
+ companyName: this.config.companyName,
1835
+ }));
1836
+ };
1837
+ this.onAfterNgOnDestroy = () => { };
1838
+ if (isPlatformBrowser(platformId)) {
1839
+ window.addEventListener('storage', this.storageEventListener);
1840
+ }
1651
1841
  }
1652
- static { this.schema = {
1653
- idToken: {
1654
- type: 'string',
1655
- },
1656
- browserId: {
1657
- type: 'string',
1658
- },
1659
- }; }
1660
- }
1661
- class UserRole extends BaseEntity {
1662
- static { this.typeName = 'UserRole'; }
1663
- constructor({ roleId, userId, } = {}) {
1664
- super();
1665
- this.roleId = roleId;
1666
- this.userId = userId;
1842
+ sendLoginVerificationEmail(body) {
1843
+ body.browserId = this.getBrowserId();
1844
+ return this.apiService.sendLoginVerificationEmail(body);
1667
1845
  }
1668
- static { this.schema = {
1669
- roleId: {
1670
- type: 'number',
1671
- },
1672
- userId: {
1673
- type: 'number',
1846
+ login(body) {
1847
+ body.browserId = this.getBrowserId();
1848
+ return this.apiService.loginWithCookies(body).pipe(map$1((result) => {
1849
+ this.handleAuthResult(result);
1850
+ return result;
1851
+ }));
1852
+ }
1853
+ // Establishes the in-memory session from a cookie auth result (login or refresh). No tokens are stored
1854
+ // in JS — only the user identity + the access-token expiry the backend reports (to schedule refresh).
1855
+ handleAuthResult(result) {
1856
+ this._user.next({
1857
+ id: result.userId,
1858
+ email: result.email,
1859
+ });
1860
+ this.accessTokenExpiresAt = result.accessTokenExpiresAt
1861
+ ? new Date(result.accessTokenExpiresAt)
1862
+ : undefined;
1863
+ localStorage.setItem('login-event', 'login' + Math.random());
1864
+ this.startTokenTimer();
1865
+ this.setCurrentUserPermissionCodes().subscribe();
1866
+ }
1867
+ logout() {
1868
+ const browserId = this.getBrowserId();
1869
+ this.apiService
1870
+ .logoutWithCookies(browserId)
1871
+ .pipe(finalize(() => {
1872
+ this._user.next(null);
1873
+ localStorage.setItem('logout-event', 'logout' + Math.random());
1874
+ this.onAfterLogout();
1875
+ this.stopTokenTimer();
1876
+ }))
1877
+ .subscribe();
1878
+ }
1879
+ // Clears in-memory session state without calling the backend — used when a request comes back 401
1880
+ // (the backend has already cleared the auth cookies in that case).
1881
+ clearSession() {
1882
+ this.stopTokenTimer();
1883
+ this._user.next(null);
1884
+ this._currentUserPermissionCodes.next(null);
1885
+ localStorage.setItem('logout-event', 'logout' + Math.random());
1886
+ }
1887
+ // Called on app init and by the proactive timer. The refresh token is an HttpOnly cookie; a 401 ("no valid
1888
+ // session" — not logged in / expired) propagates from the interceptor and is handled by catchError below,
1889
+ // resolving the session to anonymous (null). map runs only for a real result, so _user is never partial.
1890
+ refreshToken() {
1891
+ const browserId = this.getBrowserId();
1892
+ return this.apiService.refreshTokenWithCookies(browserId).pipe(map$1((result) => {
1893
+ if (result) {
1894
+ // A re-established session makes any pending external-login error moot — drop it so it can't
1895
+ // surface as a stale toast on a later /login visit (e.g. after a subsequent logout).
1896
+ this.externalAuthErrorCode = null;
1897
+ this._user.next({ id: result.userId, email: result.email });
1898
+ this.accessTokenExpiresAt = result.accessTokenExpiresAt
1899
+ ? new Date(result.accessTokenExpiresAt)
1900
+ : undefined;
1901
+ this.startTokenTimer();
1902
+ this.onAfterRefreshToken();
1903
+ }
1904
+ return result;
1905
+ }), catchError(() => {
1906
+ this._user.next(null);
1907
+ return of(null);
1908
+ }));
1909
+ }
1910
+ // Reads ?externalAuthError= from the bootstrap URL (set by the backend OAuth callback on failure) and
1911
+ // strips it so a manual refresh won't re-trigger the message. Called from the app initializer, before the
1912
+ // router runs — otherwise an unauthenticated landing on "/" redirects to /login and drops the param.
1913
+ captureExternalAuthError() {
1914
+ if (isPlatformBrowser(this.platformId) === false) {
1915
+ return;
1916
+ }
1917
+ const params = new URLSearchParams(window.location.search);
1918
+ const code = params.get('externalAuthError');
1919
+ if (!code) {
1920
+ return;
1921
+ }
1922
+ this.externalAuthErrorCode = code;
1923
+ params.delete('externalAuthError');
1924
+ const query = params.toString();
1925
+ history.replaceState(history.state, '', window.location.pathname + (query ? `?${query}` : '') + window.location.hash);
1926
+ }
1927
+ getBrowserId() {
1928
+ let browserId = localStorage.getItem(this.config.browserIdKey); // not a token — a stable per-browser id
1929
+ if (!browserId) {
1930
+ browserId = crypto.randomUUID();
1931
+ localStorage.setItem(this.config.browserIdKey, browserId);
1932
+ }
1933
+ return browserId;
1934
+ }
1935
+ getTokenRemainingTime() {
1936
+ if (!this.accessTokenExpiresAt) {
1937
+ return 0;
1938
+ }
1939
+ return this.accessTokenExpiresAt.getTime() - Date.now();
1940
+ }
1941
+ startTokenTimer() {
1942
+ const timeout = this.getTokenRemainingTime();
1943
+ if (timeout <= 0) {
1944
+ return;
1945
+ }
1946
+ this.stopTokenTimer();
1947
+ this.timer = of(true)
1948
+ .pipe(delay(timeout), tap({
1949
+ next: () => this.refreshToken().subscribe(),
1950
+ }))
1951
+ .subscribe();
1952
+ }
1953
+ stopTokenTimer() {
1954
+ this.timer?.unsubscribe();
1955
+ }
1956
+ navigateToDashboard() {
1957
+ this.router.navigate(['/']);
1958
+ }
1959
+ setCurrentUserPermissionCodes() {
1960
+ return this.apiService.getCurrentUserPermissionCodes().pipe(map$1((permissionCodes) => {
1961
+ this._currentUserPermissionCodes.next(permissionCodes);
1962
+ return permissionCodes;
1963
+ }));
1964
+ }
1965
+ ngOnDestroy() {
1966
+ if (isPlatformBrowser(this.platformId)) {
1967
+ window.removeEventListener('storage', this.storageEventListener);
1968
+ }
1969
+ this.onAfterNgOnDestroy();
1970
+ }
1971
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthServiceBase, deps: [{ token: i3$2.Router }, { token: i1$2.HttpClient }, { token: ApiSecurityService }, { token: ConfigServiceBase }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); }
1972
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthServiceBase, providedIn: 'root' }); }
1973
+ }
1974
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthServiceBase, decorators: [{
1975
+ type: Injectable,
1976
+ args: [{
1977
+ providedIn: 'root',
1978
+ }]
1979
+ }], ctorParameters: () => [{ type: i3$2.Router }, { type: i1$2.HttpClient }, { type: ApiSecurityService }, { type: ConfigServiceBase }, { type: Object, decorators: [{
1980
+ type: Inject,
1981
+ args: [PLATFORM_ID]
1982
+ }] }] });
1983
+
1984
+ class AuthCardComponent {
1985
+ constructor(authService) {
1986
+ this.authService = authService;
1987
+ this.companyDetailsSubscription = null;
1988
+ }
1989
+ ngOnInit() {
1990
+ this.companyDetailsSubscription = this.authService
1991
+ .initCompanyAuthDialogDetails()
1992
+ .subscribe((details) => {
1993
+ if (details != null) {
1994
+ this.image = details.image;
1995
+ this.companyName = details.companyName;
1996
+ }
1997
+ });
1998
+ }
1999
+ ngOnDestroy() {
2000
+ this.companyDetailsSubscription?.unsubscribe();
2001
+ }
2002
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthCardComponent, deps: [{ token: AuthServiceBase }], target: i0.ɵɵFactoryTarget.Component }); }
2003
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: AuthCardComponent, isStandalone: true, selector: "spiderly-auth-card", ngImport: i0, template: "<div class=\"flex min-h-screen overflow-hidden p-5\">\n <div class=\"flex flex-col w-full\">\n <div\n class=\"w-full sm:w-120\"\n style=\"\n margin: auto;\n border-radius: 50px;\n padding: 0.3rem;\n background: linear-gradient(\n 180deg,\n var(--p-primary-color) 10%,\n rgba(33, 150, 243, 0) 30%\n );\n \"\n >\n <div class=\"surface-card py-12 px-8 sm:px-12\" style=\"border-radius: 45px\">\n <div class=\"flex justify-center\" style=\"margin-bottom: 38px\">\n <ng-content select=\"[auth-logo]\">\n <img\n *ngIf=\"image != null\"\n [src]=\"image\"\n alt=\"{{ companyName }} Logo\"\n title=\"{{ companyName }} Logo\"\n class=\"max-h-15\"\n />\n <i\n *ngIf=\"image == null\"\n class=\"pi pi-spin pi-spinner primary-color\"\n style=\"font-size: 2rem\"\n ></i>\n </ng-content>\n </div>\n\n <ng-content></ng-content>\n\n <ng-content select=\"[auth-footer]\"></ng-content>\n </div>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); }
2004
+ }
2005
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthCardComponent, decorators: [{
2006
+ type: Component,
2007
+ args: [{ selector: 'spiderly-auth-card', imports: [CommonModule], template: "<div class=\"flex min-h-screen overflow-hidden p-5\">\n <div class=\"flex flex-col w-full\">\n <div\n class=\"w-full sm:w-120\"\n style=\"\n margin: auto;\n border-radius: 50px;\n padding: 0.3rem;\n background: linear-gradient(\n 180deg,\n var(--p-primary-color) 10%,\n rgba(33, 150, 243, 0) 30%\n );\n \"\n >\n <div class=\"surface-card py-12 px-8 sm:px-12\" style=\"border-radius: 45px\">\n <div class=\"flex justify-center\" style=\"margin-bottom: 38px\">\n <ng-content select=\"[auth-logo]\">\n <img\n *ngIf=\"image != null\"\n [src]=\"image\"\n alt=\"{{ companyName }} Logo\"\n title=\"{{ companyName }} Logo\"\n class=\"max-h-15\"\n />\n <i\n *ngIf=\"image == null\"\n class=\"pi pi-spin pi-spinner primary-color\"\n style=\"font-size: 2rem\"\n ></i>\n </ng-content>\n </div>\n\n <ng-content></ng-content>\n\n <ng-content select=\"[auth-footer]\"></ng-content>\n </div>\n </div>\n </div>\n</div>\n" }]
2008
+ }], ctorParameters: () => [{ type: AuthServiceBase }] });
2009
+
2010
+ const GOOGLE_G_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="18" height="18"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.55 10.78l7.98-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>`;
2011
+ /**
2012
+ * Built-in default icons for external auth providers, keyed by provider code.
2013
+ * Values are inline data URIs — no network request, CSP entry, or asset wiring,
2014
+ * and they render offline. Consumers override per code via the `providerIcons`
2015
+ * input on ExternalLoginComponent / SpiderlyLoginComponent.
2016
+ */
2017
+ const DEFAULT_EXTERNAL_PROVIDER_ICONS = {
2018
+ google: `data:image/svg+xml,${encodeURIComponent(GOOGLE_G_SVG)}`,
2019
+ };
2020
+
2021
+ class ExternalLoginComponent {
2022
+ constructor(config, authService, apiService) {
2023
+ this.config = config;
2024
+ this.authService = authService;
2025
+ this.apiService = apiService;
2026
+ /** Per-code icon overrides; unset codes fall back to DEFAULT_EXTERNAL_PROVIDER_ICONS. */
2027
+ this.providerIcons = {};
2028
+ // Config-driven: populated from Security/GetExternalProviders (backend is the single source of truth for which providers are enabled).
2029
+ this.externalProviders = [];
2030
+ }
2031
+ ngOnInit() {
2032
+ this.apiService.getExternalProviders().subscribe({
2033
+ next: (providers) => {
2034
+ this.externalProviders = providers ?? [];
2035
+ },
2036
+ // The global unauthorized interceptor already surfaces the HTTP error to the user; here we just
2037
+ // leave the provider buttons hidden instead of letting the error reach the global error handler.
2038
+ error: () => {
2039
+ this.externalProviders = [];
2040
+ },
2041
+ });
2042
+ }
2043
+ iconFor(code) {
2044
+ return this.providerIcons[code] ?? DEFAULT_EXTERNAL_PROVIDER_ICONS[code];
2045
+ }
2046
+ loginWithExternalProvider(code) {
2047
+ // Server-side flow (B2): hand off to the backend challenge endpoint. The backend runs the OAuth
2048
+ // dance, sets the session cookies, and redirects back to returnUrl.
2049
+ const returnUrl = this.config.frontendUrl;
2050
+ const browserId = this.authService.getBrowserId();
2051
+ window.location.href =
2052
+ `${this.config.apiUrl}/Security/ExternalLoginChallenge` +
2053
+ `?provider=${encodeURIComponent(code)}` +
2054
+ `&returnUrl=${encodeURIComponent(returnUrl)}` +
2055
+ `&browserId=${encodeURIComponent(browserId)}`;
2056
+ }
2057
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ExternalLoginComponent, deps: [{ token: ConfigServiceBase }, { token: AuthServiceBase }, { token: ApiSecurityService }], target: i0.ɵɵFactoryTarget.Component }); }
2058
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: ExternalLoginComponent, isStandalone: true, selector: "spiderly-external-login", inputs: { providerIcons: "providerIcons" }, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div *ngIf=\"externalProviders.length > 0\">\n <div\n style=\"display: flex; align-items: center; gap: 7px; justify-content: center; margin-bottom: 16px;\"\n >\n <div class=\"separator\"></div>\n <div>{{ t(\"or\") }}</div>\n <div class=\"separator\"></div>\n </div>\n <div style=\"display: flex; flex-direction: column; gap: 10px\">\n <spiderly-button\n *ngFor=\"let provider of externalProviders\"\n (onClick)=\"loginWithExternalProvider(provider.code)\"\n [label]=\"provider.label || (provider.code | titlecase)\"\n [iconUrl]=\"iconFor(provider.code)\"\n [outlined]=\"true\"\n styleClass=\"w-full\"\n ></spiderly-button>\n </div>\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: "pipe", type: i2.TitleCasePipe, name: "titlecase" }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }] }); }
2059
+ }
2060
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ExternalLoginComponent, decorators: [{
2061
+ type: Component,
2062
+ args: [{ selector: 'spiderly-external-login', imports: [CommonModule, TranslocoDirective, SpiderlyButtonComponent], template: "<ng-container *transloco=\"let t\">\n <div *ngIf=\"externalProviders.length > 0\">\n <div\n style=\"display: flex; align-items: center; gap: 7px; justify-content: center; margin-bottom: 16px;\"\n >\n <div class=\"separator\"></div>\n <div>{{ t(\"or\") }}</div>\n <div class=\"separator\"></div>\n </div>\n <div style=\"display: flex; flex-direction: column; gap: 10px\">\n <spiderly-button\n *ngFor=\"let provider of externalProviders\"\n (onClick)=\"loginWithExternalProvider(provider.code)\"\n [label]=\"provider.label || (provider.code | titlecase)\"\n [iconUrl]=\"iconFor(provider.code)\"\n [outlined]=\"true\"\n styleClass=\"w-full\"\n ></spiderly-button>\n </div>\n </div>\n</ng-container>\n" }]
2063
+ }], ctorParameters: () => [{ type: ConfigServiceBase }, { type: AuthServiceBase }, { type: ApiSecurityService }], propDecorators: { providerIcons: [{
2064
+ type: Input
2065
+ }] } });
2066
+
2067
+ class UserBase extends BaseEntity {
2068
+ static { this.typeName = 'UserBase'; }
2069
+ constructor({ id, email, } = {}) {
2070
+ super();
2071
+ this.id = id;
2072
+ this.email = email;
2073
+ }
2074
+ static { this.schema = {
2075
+ id: {
2076
+ type: 'number',
2077
+ },
2078
+ email: {
2079
+ type: 'string',
2080
+ },
2081
+ }; }
2082
+ }
2083
+ class AuthResult extends BaseEntity {
2084
+ static { this.typeName = 'AuthResult'; }
2085
+ constructor({ userId, email, accessToken, refreshToken, } = {}) {
2086
+ super();
2087
+ this.userId = userId;
2088
+ this.email = email;
2089
+ this.accessToken = accessToken;
2090
+ this.refreshToken = refreshToken;
2091
+ }
2092
+ static { this.schema = {
2093
+ userId: {
2094
+ type: 'number',
2095
+ },
2096
+ email: {
2097
+ type: 'string',
2098
+ },
2099
+ accessToken: {
2100
+ type: 'string',
2101
+ },
2102
+ refreshToken: {
2103
+ type: 'string',
2104
+ },
2105
+ }; }
2106
+ }
2107
+ class VerificationTokenRequest extends BaseEntity {
2108
+ static { this.typeName = 'VerificationTokenRequest'; }
2109
+ constructor({ verificationCode, browserId, email, } = {}) {
2110
+ super();
2111
+ this.verificationCode = verificationCode;
2112
+ this.browserId = browserId;
2113
+ this.email = email;
2114
+ }
2115
+ static { this.schema = {
2116
+ verificationCode: {
2117
+ type: 'string',
2118
+ },
2119
+ browserId: {
2120
+ type: 'string',
2121
+ },
2122
+ email: {
2123
+ type: 'string',
2124
+ },
2125
+ }; }
2126
+ }
2127
+ class ExternalProvider extends BaseEntity {
2128
+ static { this.typeName = 'ExternalProvider'; }
2129
+ constructor({ provider, idToken, browserId, } = {}) {
2130
+ super();
2131
+ this.provider = provider;
2132
+ this.idToken = idToken;
2133
+ this.browserId = browserId;
2134
+ }
2135
+ static { this.schema = {
2136
+ provider: {
2137
+ type: 'string',
2138
+ },
2139
+ idToken: {
2140
+ type: 'string',
2141
+ },
2142
+ browserId: {
2143
+ type: 'string',
2144
+ },
2145
+ }; }
2146
+ }
2147
+ class ExternalProviderPublic extends BaseEntity {
2148
+ static { this.typeName = 'ExternalProviderPublic'; }
2149
+ constructor({ code, authority, clientId, label, } = {}) {
2150
+ super();
2151
+ this.code = code;
2152
+ this.authority = authority;
2153
+ this.clientId = clientId;
2154
+ this.label = label;
2155
+ }
2156
+ static { this.schema = {
2157
+ code: {
2158
+ type: 'string',
2159
+ },
2160
+ authority: {
2161
+ type: 'string',
2162
+ },
2163
+ clientId: {
2164
+ type: 'string',
2165
+ },
2166
+ label: {
2167
+ type: 'string',
2168
+ },
2169
+ }; }
2170
+ }
2171
+ class UserRole extends BaseEntity {
2172
+ static { this.typeName = 'UserRole'; }
2173
+ constructor({ roleId, userId, } = {}) {
2174
+ super();
2175
+ this.roleId = roleId;
2176
+ this.userId = userId;
2177
+ }
2178
+ static { this.schema = {
2179
+ roleId: {
2180
+ type: 'number',
2181
+ },
2182
+ userId: {
2183
+ type: 'number',
1674
2184
  },
1675
2185
  }; }
1676
2186
  }
@@ -2049,501 +2559,142 @@ class BaseFormComponent {
2049
2559
  if (!this.saveBodyClass)
2050
2560
  throw new SpiderlyError('You did not initialize saveBodyClass');
2051
2561
  if (!this.mainUIFormClass)
2052
- throw new SpiderlyError('You did not initialize mainUIFormClass');
2053
- let saveBody = this.parentFormGroup.getRawValue();
2054
- this.onBeforeSave(saveBody);
2055
- const isValid = this.baseFormService.isControlValid(this.parentFormGroup);
2056
- if (isValid) {
2057
- this.parentFormGroup
2058
- .saveObservableMethod(saveBody)
2059
- .subscribe((res) => {
2060
- this.messageService.successMessage(this.successfulSaveToastDescription);
2061
- if (rerouteToParentSlugAfterSave) {
2062
- this.rerouteToSavedObject(undefined);
2063
- }
2064
- else {
2065
- saveBody = this.baseFormService.mapMainUIFormToSaveBody(this.mainUIFormClass, res);
2066
- this.baseFormService.initFormGroup(this.parentFormGroup, this.saveBodyClass, saveBody);
2067
- const saveBodyMainDTOKey = this.baseFormService.getSaveBodyMainDTOKey(this.saveBodyClass);
2068
- const savedObjectId = saveBody[saveBodyMainDTOKey]?.id;
2069
- this.rerouteToSavedObject(savedObjectId); // You always need to have id, because of id == 0 and version change
2070
- }
2071
- this.onAfterSave();
2072
- });
2073
- this.onAfterSaveRequest();
2074
- }
2075
- else {
2076
- this.baseFormService.showInvalidFieldsMessage();
2077
- }
2078
- };
2079
- /**
2080
- * Hook that runs **before** form validation and the save request.
2081
- * Use this to modify the save body or perform any pre-save logic (e.g., transforming data, setting computed fields).
2082
- *
2083
- * @param saveBody - The current save body built from the form's raw value. Mutate it directly to change what gets sent to the server.
2084
- *
2085
- * @example
2086
- * ```ts
2087
- * onBeforeSave = (saveBody?: ProductSaveBody) => {
2088
- * saveBody.productDTO.fullName = saveBody.productDTO.firstName + ' ' + saveBody.productDTO.lastName;
2089
- * };
2090
- * ```
2091
- */
2092
- this.onBeforeSave = (saveBody) => { };
2093
- /**
2094
- * Hook that runs **after** a successful save response is received.
2095
- * Use this for post-save side effects (e.g., refreshing related data, showing additional notifications).
2096
- *
2097
- * @example
2098
- * ```ts
2099
- * onAfterSave = () => {
2100
- * this.loadRelatedProducts();
2101
- * };
2102
- * ```
2103
- */
2104
- this.onAfterSave = () => { };
2105
- /**
2106
- * Hook that runs immediately **after** the save HTTP request is sent, but **before** the response arrives.
2107
- * Use this for side effects that should happen as soon as the request is dispatched (e.g., disabling UI elements, starting a loading indicator).
2108
- *
2109
- * @example
2110
- * ```ts
2111
- * onAfterSaveRequest = () => {
2112
- * this.isSaving = true;
2113
- * };
2114
- * ```
2115
- */
2116
- this.onAfterSaveRequest = () => { };
2117
- }
2118
- ngOnInit() { }
2119
- /**
2120
- * Handles navigation after a successful save.
2121
- * Override this to customize the post-save navigation behavior.
2122
- * By default, navigates to the parent URL when `rerouteId` is not provided, or to the saved object's URL otherwise.
2123
- *
2124
- * @param rerouteId - The ID of the saved object, used to build the target URL. When not provided, navigates to the parent URL.
2125
- *
2126
- * @example
2127
- * ```ts
2128
- * // Override to navigate to a custom route after save
2129
- * override rerouteToSavedObject(rerouteId: number | string): void {
2130
- * this.router.navigateByUrl(`/products/${rerouteId}/details`);
2131
- * }
2132
- * ```
2133
- */
2134
- rerouteToSavedObject(rerouteId) {
2135
- if (rerouteId == null) {
2136
- const currentUrl = this.router.url;
2137
- const parentUrl = getParentUrl(currentUrl);
2138
- this.router.navigateByUrl(parentUrl);
2139
- return;
2140
- }
2141
- const segments = this.router.url.split('/');
2142
- segments[segments.length - 1] = rerouteId.toString();
2143
- const newUrl = segments.join('/');
2144
- this.router.navigateByUrl(newUrl);
2145
- }
2146
- //#endregion
2147
- //#region Model List
2148
- getFormArrayControlByIndex(formControlName, formArray, index, filter) {
2149
- // if(formArray.controlNamesFromHtml.findIndex(x => x === formControlName) === -1)
2150
- // formArray.controlNamesFromHtml.push(formControlName);
2151
- let filteredFormGroups;
2152
- if (filter) {
2153
- filteredFormGroups = filter(formArray.controls);
2154
- }
2155
- else {
2156
- return formArray.controls[index].controls[formControlName];
2157
- }
2158
- return filteredFormGroups[index]?.controls[formControlName]; // FT: Don't change this. It's always possible that change detection occurs before something.
2159
- }
2160
- getFormArrayControls(formControlName, formArray, filter) {
2161
- // if(formArray.controlNamesFromHtml.findIndex(x => x === formControlName) === -1)
2162
- // formArray.controlNamesFromHtml.push(formControlName);
2163
- let filteredFormGroups;
2164
- if (filter) {
2165
- filteredFormGroups = filter(formArray.controls);
2166
- }
2167
- else {
2168
- return formArray.controls.map((x) => x.controls[formControlName]);
2169
- }
2170
- return filteredFormGroups.map((x) => x.controls[formControlName]);
2171
- }
2172
- removeFormControlsFromTheFormArray(formArray, indexes) {
2173
- // Sort indexes in descending order to avoid index shifts when removing controls
2174
- const sortedIndexes = indexes.sort((a, b) => b - a);
2175
- sortedIndexes.forEach((index) => {
2176
- if (index >= 0 && index < formArray.length) {
2177
- formArray.removeAt(index);
2178
- }
2179
- });
2180
- }
2181
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: BaseFormComponent, deps: [{ token: i0.KeyValueDiffers }, { token: i1$2.HttpClient }, { token: SpiderlyMessageService }, { token: i0.ChangeDetectorRef }, { token: i3$2.Router }, { token: i3$2.ActivatedRoute }, { token: i1.TranslocoService }, { token: BaseFormService }], target: i0.ɵɵFactoryTarget.Component }); }
2182
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: BaseFormComponent, isStandalone: false, selector: "base-form", ngImport: i0, template: '', isInline: true }); }
2183
- }
2184
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: BaseFormComponent, decorators: [{
2185
- type: Component,
2186
- args: [{ selector: 'base-form', template: '', standalone: false }]
2187
- }], ctorParameters: () => [{ type: i0.KeyValueDiffers }, { type: i1$2.HttpClient }, { type: SpiderlyMessageService }, { type: i0.ChangeDetectorRef }, { type: i3$2.Router }, { type: i3$2.ActivatedRoute }, { type: i1.TranslocoService }, { type: BaseFormService }] });
2188
-
2189
- class GoogleButtonComponent {
2190
- constructor() {
2191
- this.loginWithGoogle = new EventEmitter();
2192
- }
2193
- handleGoogleLogin() {
2194
- this.loginWithGoogle.emit(createFakeGoogleWrapper());
2195
- }
2196
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: GoogleButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2197
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: GoogleButtonComponent, isStandalone: true, selector: "google-button", inputs: { label: "label" }, outputs: { loginWithGoogle: "loginWithGoogle" }, ngImport: i0, template: "<ng-container>\n <spiderly-button\n (onClick)=\"handleGoogleLogin()\"\n [label]=\"label\"\n [outlined]=\"true\"\n [style]=\"{ width: '100%' }\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"0.98em\"\n height=\"1em\"\n viewBox=\"0 0 256 262\"\n >\n <path\n fill=\"#4285f4\"\n d=\"M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027\"\n />\n <path\n fill=\"#34a853\"\n d=\"M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1\"\n />\n <path\n fill=\"#fbbc05\"\n d=\"M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z\"\n />\n <path\n fill=\"#eb4335\"\n d=\"M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251\"\n />\n </svg>\n </spiderly-button>\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: ButtonModule }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }] }); }
2198
- }
2199
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: GoogleButtonComponent, decorators: [{
2200
- type: Component,
2201
- args: [{ selector: 'google-button', imports: [ButtonModule, SpiderlyButtonComponent], template: "<ng-container>\n <spiderly-button\n (onClick)=\"handleGoogleLogin()\"\n [label]=\"label\"\n [outlined]=\"true\"\n [style]=\"{ width: '100%' }\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"0.98em\"\n height=\"1em\"\n viewBox=\"0 0 256 262\"\n >\n <path\n fill=\"#4285f4\"\n d=\"M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027\"\n />\n <path\n fill=\"#34a853\"\n d=\"M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1\"\n />\n <path\n fill=\"#fbbc05\"\n d=\"M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z\"\n />\n <path\n fill=\"#eb4335\"\n d=\"M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251\"\n />\n </svg>\n </spiderly-button>\n</ng-container>\n" }]
2202
- }], propDecorators: { label: [{
2203
- type: Input
2204
- }], loginWithGoogle: [{
2205
- type: Output
2206
- }] } });
2207
-
2208
- class ConfigServiceBase {
2209
- constructor() {
2210
- this.production = false;
2211
- this.frontendUrl = 'http://localhost:4200';
2212
- this.showGoogleAuth = false;
2213
- this.companyName = 'Company Name';
2214
- this.primaryColor = '#111b2c';
2215
- /* URLs */
2216
- this.loginSlug = 'login';
2217
- /* Local storage */
2218
- this.accessTokenKey = 'access_token';
2219
- this.refreshTokenKey = 'refresh_token';
2220
- this.browserIdKey = 'browser_id';
2221
- this.httpOptions = {};
2222
- this.httpSkipSpinnerOptions = {
2223
- headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
2224
- params: new HttpParams().set('X-Skip-Spinner', 'true'),
2225
- };
2226
- this.logoPath = 'assets/images/logo/logo.svg';
2227
- /* Pagination */
2228
- this.defaultPageSize = 10;
2229
- }
2230
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ConfigServiceBase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2231
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ConfigServiceBase, providedIn: 'root' }); }
2232
- }
2233
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ConfigServiceBase, decorators: [{
2234
- type: Injectable,
2235
- args: [{
2236
- providedIn: 'root',
2237
- }]
2238
- }], ctorParameters: () => [] });
2239
-
2240
- class InitCompanyAuthDialogDetails extends BaseEntity {
2241
- constructor({ image, companyName, } = {}) {
2242
- super();
2243
- this.image = image;
2244
- this.companyName = companyName;
2245
- }
2246
- static { this.typeName = 'InitCompanyAuthDialogDetails'; }
2247
- }
2248
-
2249
- class ApiSecurityService {
2250
- constructor(http, config) {
2251
- this.http = http;
2252
- this.config = config;
2253
- //#region Authentication
2254
- this.login = (request) => {
2255
- return this.http.post(`${this.config.apiUrl}/Security/Login`, request, this.config.httpOptions);
2256
- };
2257
- this.loginExternal = (externalProviderDTO) => {
2258
- return this.http.post(`${this.config.apiUrl}/Security/LoginExternal`, externalProviderDTO, this.config.httpOptions);
2259
- };
2260
- this.sendLoginVerificationEmail = (loginDTO) => {
2261
- return this.http.post(`${this.config.apiUrl}/Security/SendLoginVerificationEmail`, loginDTO, this.config.httpOptions);
2262
- };
2263
- this.loginWithCookies = (request) => {
2264
- return this.http.post(`${this.config.apiUrl}/Security/LoginWithCookies`, request, this.config.httpOptions);
2265
- };
2266
- this.loginExternalWithCookies = (externalProviderDTO) => {
2267
- return this.http.post(`${this.config.apiUrl}/Security/LoginExternalWithCookies`, externalProviderDTO, this.config.httpOptions);
2268
- };
2269
- this.logout = (browserId) => {
2270
- return this.http.get(`${this.config.apiUrl}/Security/Logout?browserId=${browserId}`);
2271
- };
2272
- this.logoutWithCookies = (browserId) => {
2273
- return this.http.get(`${this.config.apiUrl}/Security/LogoutWithCookies?browserId=${browserId}`);
2274
- };
2275
- this.refreshTokenWithHeaders = (request) => {
2276
- return this.http.post(`${this.config.apiUrl}/Security/RefreshTokenWithHeaders`, request, this.config.httpOptions);
2277
- };
2278
- this.refreshTokenWithCookies = (browserId) => {
2279
- return this.http.get(`${this.config.apiUrl}/Security/RefreshTokenWithCookies?browserId=${browserId}`);
2280
- };
2281
- //#endregion
2282
- //#region User
2283
- this.getCurrentUserBase = () => {
2284
- return this.http.get(`${this.config.apiUrl}/Security/GetCurrentUserBase`, this.config.httpSkipSpinnerOptions);
2285
- };
2286
- this.getCurrentUserPermissionCodes = () => {
2287
- return this.http.get(`${this.config.apiUrl}/Security/GetCurrentUserPermissionCodes`, this.config.httpSkipSpinnerOptions);
2288
- };
2289
- }
2290
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ApiSecurityService, deps: [{ token: i1$2.HttpClient }, { token: ConfigServiceBase }], target: i0.ɵɵFactoryTarget.Injectable }); }
2291
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ApiSecurityService, providedIn: 'root' }); }
2292
- }
2293
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ApiSecurityService, decorators: [{
2294
- type: Injectable,
2295
- args: [{
2296
- providedIn: 'root',
2297
- }]
2298
- }], ctorParameters: () => [{ type: i1$2.HttpClient }, { type: ConfigServiceBase }] });
2299
-
2300
- class AuthServiceBase {
2301
- constructor(router, http, externalAuthService, apiService, config, platformId) {
2302
- this.router = router;
2303
- this.http = http;
2304
- this.externalAuthService = externalAuthService;
2305
- this.apiService = apiService;
2306
- this.config = config;
2307
- this.platformId = platformId;
2308
- this.apiUrl = this.config.apiUrl;
2309
- this._currentUserPermissionCodes = new BehaviorSubject(undefined);
2310
- this.currentUserPermissionCodes$ = this._currentUserPermissionCodes.asObservable();
2311
- this._user = new BehaviorSubject(undefined);
2312
- this.user$ = this._user.asObservable();
2313
- // Google auth
2314
- this.authChangeSub = new Subject();
2315
- this.extAuthChangeSub = new Subject();
2316
- this.authChanged = this.authChangeSub.asObservable();
2317
- this.extAuthChanged = this.extAuthChangeSub.asObservable();
2318
- this.storageEventListener = (event) => {
2319
- if (event.storageArea === localStorage) {
2320
- if (event.key === 'logout-event') {
2321
- this.stopTokenTimer();
2322
- this._user.next(null);
2323
- this._currentUserPermissionCodes.next(null);
2324
- }
2325
- if (event.key === 'login-event') {
2326
- this.stopTokenTimer();
2327
- this.apiService.getCurrentUserBase().subscribe((user) => {
2328
- this._user.next({
2329
- id: user.id,
2330
- email: user.email,
2331
- });
2332
- this.setCurrentUserPermissionCodes().subscribe();
2333
- });
2334
- }
2562
+ throw new SpiderlyError('You did not initialize mainUIFormClass');
2563
+ let saveBody = this.parentFormGroup.getRawValue();
2564
+ this.onBeforeSave(saveBody);
2565
+ const isValid = this.baseFormService.isControlValid(this.parentFormGroup);
2566
+ if (isValid) {
2567
+ this.parentFormGroup
2568
+ .saveObservableMethod(saveBody)
2569
+ .subscribe((res) => {
2570
+ this.messageService.successMessage(this.successfulSaveToastDescription);
2571
+ if (rerouteToParentSlugAfterSave) {
2572
+ this.rerouteToSavedObject(undefined);
2573
+ }
2574
+ else {
2575
+ saveBody = this.baseFormService.mapMainUIFormToSaveBody(this.mainUIFormClass, res);
2576
+ this.baseFormService.initFormGroup(this.parentFormGroup, this.saveBodyClass, saveBody);
2577
+ const saveBodyMainDTOKey = this.baseFormService.getSaveBodyMainDTOKey(this.saveBodyClass);
2578
+ const savedObjectId = saveBody[saveBodyMainDTOKey]?.id;
2579
+ this.rerouteToSavedObject(savedObjectId); // You always need to have id, because of id == 0 and version change
2580
+ }
2581
+ this.onAfterSave();
2582
+ });
2583
+ this.onAfterSaveRequest();
2584
+ }
2585
+ else {
2586
+ this.baseFormService.showInvalidFieldsMessage();
2335
2587
  }
2336
2588
  };
2337
- this.onAfterLoginExternal = () => {
2338
- this.navigateToDashboard();
2339
- };
2340
- this.onAfterLogout = () => {
2341
- this._currentUserPermissionCodes.next(null);
2342
- this.router.navigate([this.config.loginSlug]);
2343
- };
2344
- this.onAfterRefreshToken = () => {
2345
- this.setCurrentUserPermissionCodes().subscribe(); // FT: Needs to be after setting local storage
2346
- };
2347
- this.initCompanyAuthDialogDetails = () => {
2348
- return of(new InitCompanyAuthDialogDetails({
2349
- image: this.config.logoPath,
2350
- companyName: this.config.companyName,
2351
- }));
2352
- };
2353
- this.onAfterNgOnDestroy = () => { };
2354
- if (isPlatformBrowser(platformId)) {
2355
- window.addEventListener('storage', this.storageEventListener);
2356
- }
2357
- // Google auth
2358
- this.externalAuthService.authState.subscribe((user) => {
2359
- const externalAuth = {
2360
- // provider: user.provider,
2361
- idToken: user.idToken,
2362
- };
2363
- this.loginExternal(externalAuth).subscribe((authResult) => {
2364
- this.onAfterLoginExternal();
2365
- });
2366
- this.extAuthChangeSub.next(user);
2367
- });
2368
- }
2369
- sendLoginVerificationEmail(body) {
2370
- const browserId = this.getBrowserId();
2371
- body.browserId = browserId;
2372
- return this.apiService.sendLoginVerificationEmail(body);
2373
- }
2374
- login(body) {
2375
- const browserId = this.getBrowserId();
2376
- body.browserId = browserId;
2377
- const loginResultObservable = this.http.post(`${this.apiUrl}/Security/Login`, body);
2378
- return this.handleLoginResult(loginResultObservable);
2379
- }
2380
- loginExternal(body) {
2381
- const browserId = this.getBrowserId();
2382
- body.browserId = browserId;
2383
- const loginResultObservable = this.http.post(`${this.apiUrl}/Security/LoginExternal`, body);
2384
- return this.handleLoginResult(loginResultObservable);
2385
- }
2386
- handleLoginResult(loginResultObservable) {
2387
- return loginResultObservable.pipe(map$1(async (loginResult) => {
2388
- this.setLocalStorage(loginResult);
2389
- this._user.next({
2390
- id: loginResult.userId,
2391
- email: loginResult.email,
2392
- });
2393
- this.startTokenTimer();
2394
- this.setCurrentUserPermissionCodes().subscribe();
2395
- return loginResult;
2396
- }));
2397
- }
2398
- logout() {
2399
- const browserId = this.getBrowserId();
2400
- this.http
2401
- .get(`${this.apiUrl}/Security/Logout?browserId=${browserId}`)
2402
- .pipe(finalize(() => {
2403
- this.clearLocalStorage();
2404
- this._user.next(null);
2405
- this.onAfterLogout();
2406
- this.stopTokenTimer();
2407
- }))
2408
- .subscribe();
2589
+ /**
2590
+ * Hook that runs **before** form validation and the save request.
2591
+ * Use this to modify the save body or perform any pre-save logic (e.g., transforming data, setting computed fields).
2592
+ *
2593
+ * @param saveBody - The current save body built from the form's raw value. Mutate it directly to change what gets sent to the server.
2594
+ *
2595
+ * @example
2596
+ * ```ts
2597
+ * onBeforeSave = (saveBody?: ProductSaveBody) => {
2598
+ * saveBody.productDTO.fullName = saveBody.productDTO.firstName + ' ' + saveBody.productDTO.lastName;
2599
+ * };
2600
+ * ```
2601
+ */
2602
+ this.onBeforeSave = (saveBody) => { };
2603
+ /**
2604
+ * Hook that runs **after** a successful save response is received.
2605
+ * Use this for post-save side effects (e.g., refreshing related data, showing additional notifications).
2606
+ *
2607
+ * @example
2608
+ * ```ts
2609
+ * onAfterSave = () => {
2610
+ * this.loadRelatedProducts();
2611
+ * };
2612
+ * ```
2613
+ */
2614
+ this.onAfterSave = () => { };
2615
+ /**
2616
+ * Hook that runs immediately **after** the save HTTP request is sent, but **before** the response arrives.
2617
+ * Use this for side effects that should happen as soon as the request is dispatched (e.g., disabling UI elements, starting a loading indicator).
2618
+ *
2619
+ * @example
2620
+ * ```ts
2621
+ * onAfterSaveRequest = () => {
2622
+ * this.isSaving = true;
2623
+ * };
2624
+ * ```
2625
+ */
2626
+ this.onAfterSaveRequest = () => { };
2409
2627
  }
2410
- refreshToken() {
2411
- const refreshToken = localStorage.getItem(this.config.refreshTokenKey);
2412
- if (!refreshToken) {
2413
- this.clearLocalStorage();
2414
- return of(null);
2628
+ ngOnInit() { }
2629
+ /**
2630
+ * Handles navigation after a successful save.
2631
+ * Override this to customize the post-save navigation behavior.
2632
+ * By default, navigates to the parent URL when `rerouteId` is not provided, or to the saved object's URL otherwise.
2633
+ *
2634
+ * @param rerouteId - The ID of the saved object, used to build the target URL. When not provided, navigates to the parent URL.
2635
+ *
2636
+ * @example
2637
+ * ```ts
2638
+ * // Override to navigate to a custom route after save
2639
+ * override rerouteToSavedObject(rerouteId: number | string): void {
2640
+ * this.router.navigateByUrl(`/products/${rerouteId}/details`);
2641
+ * }
2642
+ * ```
2643
+ */
2644
+ rerouteToSavedObject(rerouteId) {
2645
+ if (rerouteId == null) {
2646
+ const currentUrl = this.router.url;
2647
+ const parentUrl = getParentUrl(currentUrl);
2648
+ this.router.navigateByUrl(parentUrl);
2649
+ return;
2415
2650
  }
2416
- const browserId = this.getBrowserId();
2417
- const body = new RefreshTokenRequest();
2418
- body.browserId = browserId;
2419
- body.refreshToken = refreshToken;
2420
- return this.http
2421
- .post(`${this.apiUrl}/Security/RefreshTokenWithHeaders`, body, this.config.httpSkipSpinnerOptions)
2422
- .pipe(map$1((loginResult) => {
2423
- this._user.next({
2424
- id: loginResult.userId,
2425
- email: loginResult.email,
2426
- });
2427
- this.setLocalStorage(loginResult);
2428
- this.startTokenTimer();
2429
- this.onAfterRefreshToken();
2430
- return loginResult;
2431
- }));
2432
- }
2433
- setLocalStorage(loginResult) {
2434
- localStorage.setItem(this.config.accessTokenKey, loginResult.accessToken);
2435
- localStorage.setItem(this.config.refreshTokenKey, loginResult.refreshToken);
2436
- localStorage.setItem('login-event', 'login' + Math.random());
2437
- }
2438
- clearLocalStorage() {
2439
- localStorage.removeItem(this.config.accessTokenKey);
2440
- localStorage.removeItem(this.config.refreshTokenKey);
2441
- localStorage.setItem('logout-event', 'logout' + Math.random());
2651
+ const segments = this.router.url.split('/');
2652
+ segments[segments.length - 1] = rerouteId.toString();
2653
+ const newUrl = segments.join('/');
2654
+ this.router.navigateByUrl(newUrl);
2442
2655
  }
2443
- getBrowserId() {
2444
- let browserId = localStorage.getItem(this.config.browserIdKey); // FT: We don't need to remove this from the local storage ever, only if the user manuely deletes it, we will handle it
2445
- if (!browserId) {
2446
- browserId = crypto.randomUUID();
2447
- localStorage.setItem(this.config.browserIdKey, browserId);
2656
+ //#endregion
2657
+ //#region Model List
2658
+ getFormArrayControlByIndex(formControlName, formArray, index, filter) {
2659
+ // if(formArray.controlNamesFromHtml.findIndex(x => x === formControlName) === -1)
2660
+ // formArray.controlNamesFromHtml.push(formControlName);
2661
+ let filteredFormGroups;
2662
+ if (filter) {
2663
+ filteredFormGroups = filter(formArray.controls);
2448
2664
  }
2449
- return browserId;
2450
- }
2451
- isAccessTokenExpired() {
2452
- const expired = this.getTokenRemainingTime() < 5000;
2453
- return expired;
2454
- }
2455
- getTokenRemainingTime() {
2456
- const accessToken = this.getAccessToken();
2457
- if (!accessToken) {
2458
- return 0;
2665
+ else {
2666
+ return formArray.controls[index].controls[formControlName];
2459
2667
  }
2460
- const jwtToken = JSON.parse(atob(accessToken.split('.')[1]));
2461
- const expires = new Date(jwtToken.exp * 1000);
2462
- return expires.getTime() - Date.now();
2668
+ return filteredFormGroups[index]?.controls[formControlName]; // FT: Don't change this. It's always possible that change detection occurs before something.
2463
2669
  }
2464
- getAccessToken() {
2465
- if (isPlatformBrowser(this.platformId)) {
2466
- return localStorage.getItem(this.config.accessTokenKey);
2670
+ getFormArrayControls(formControlName, formArray, filter) {
2671
+ // if(formArray.controlNamesFromHtml.findIndex(x => x === formControlName) === -1)
2672
+ // formArray.controlNamesFromHtml.push(formControlName);
2673
+ let filteredFormGroups;
2674
+ if (filter) {
2675
+ filteredFormGroups = filter(formArray.controls);
2467
2676
  }
2468
- return null;
2469
- }
2470
- startTokenTimer() {
2471
- const timeout = this.getTokenRemainingTime();
2472
- this.timer = of(true)
2473
- .pipe(delay(timeout), tap({
2474
- next: () => this.refreshToken().subscribe(),
2475
- }))
2476
- .subscribe();
2477
- }
2478
- stopTokenTimer() {
2479
- this.timer?.unsubscribe();
2480
- }
2481
- navigateToDashboard() {
2482
- this.router.navigate(['/']);
2483
- }
2484
- setCurrentUserPermissionCodes() {
2485
- return this.apiService.getCurrentUserPermissionCodes().pipe(map$1((permissionCodes) => {
2486
- this._currentUserPermissionCodes.next(permissionCodes);
2487
- return permissionCodes;
2488
- }));
2489
- }
2490
- ngOnDestroy() {
2491
- if (isPlatformBrowser(this.platformId)) {
2492
- window.removeEventListener('storage', this.storageEventListener);
2677
+ else {
2678
+ return formArray.controls.map((x) => x.controls[formControlName]);
2493
2679
  }
2494
- this.onAfterNgOnDestroy();
2495
- }
2496
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthServiceBase, deps: [{ token: i3$2.Router }, { token: i1$2.HttpClient }, { token: i3$3.SocialAuthService }, { token: ApiSecurityService }, { token: ConfigServiceBase }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); }
2497
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthServiceBase, providedIn: 'root' }); }
2498
- }
2499
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthServiceBase, decorators: [{
2500
- type: Injectable,
2501
- args: [{
2502
- providedIn: 'root',
2503
- }]
2504
- }], ctorParameters: () => [{ type: i3$2.Router }, { type: i1$2.HttpClient }, { type: i3$3.SocialAuthService }, { type: ApiSecurityService }, { type: ConfigServiceBase }, { type: Object, decorators: [{
2505
- type: Inject,
2506
- args: [PLATFORM_ID]
2507
- }] }] });
2508
-
2509
- class AuthComponent {
2510
- constructor(config, authService) {
2511
- this.config = config;
2512
- this.authService = authService;
2513
- this.initCompanyAuthDialogDetailsSubscription = null;
2514
- this.onCompanyNameChange = new EventEmitter();
2515
- }
2516
- ngOnInit() {
2517
- this.initCompanyDetails();
2680
+ return filteredFormGroups.map((x) => x.controls[formControlName]);
2518
2681
  }
2519
- initCompanyDetails() {
2520
- this.initCompanyAuthDialogDetailsSubscription = this.authService
2521
- .initCompanyAuthDialogDetails()
2522
- .subscribe((initCompanyAuthDialogDetails) => {
2523
- if (initCompanyAuthDialogDetails != null) {
2524
- this.image = initCompanyAuthDialogDetails.image;
2525
- this.companyName = initCompanyAuthDialogDetails.companyName;
2526
- this.onCompanyNameChange.next(this.companyName);
2682
+ removeFormControlsFromTheFormArray(formArray, indexes) {
2683
+ // Sort indexes in descending order to avoid index shifts when removing controls
2684
+ const sortedIndexes = indexes.sort((a, b) => b - a);
2685
+ sortedIndexes.forEach((index) => {
2686
+ if (index >= 0 && index < formArray.length) {
2687
+ formArray.removeAt(index);
2527
2688
  }
2528
2689
  });
2529
2690
  }
2530
- onGoogleSignIn(googleWrapper) {
2531
- googleWrapper.click();
2532
- }
2533
- ngOnDestroy() {
2534
- if (this.initCompanyAuthDialogDetailsSubscription) {
2535
- this.initCompanyAuthDialogDetailsSubscription.unsubscribe();
2536
- }
2537
- }
2538
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthComponent, deps: [{ token: ConfigServiceBase }, { token: AuthServiceBase }], target: i0.ɵɵFactoryTarget.Component }); }
2539
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: AuthComponent, isStandalone: true, selector: "auth", outputs: { onCompanyNameChange: "onCompanyNameChange" }, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div class=\"flex min-h-screen overflow-hidden p-5\">\n <div class=\"flex flex-col w-full\">\n <div\n class=\"w-full sm:w-120\"\n style=\"\n margin: auto;\n border-radius: 50px;\n padding: 0.3rem;\n background: linear-gradient(\n 180deg,\n var(--p-primary-color) 10%,\n rgba(33, 150, 243, 0) 30%\n );\n \"\n >\n <div\n class=\"surface-card py-12 px-8 sm:px-12\"\n style=\"border-radius: 45px\"\n >\n <div class=\"flex justify-center\" style=\"margin-bottom: 38px\">\n <img\n *ngIf=\"image != null\"\n [src]=\"image\"\n alt=\"{{ companyName }} Logo\"\n title=\"{{ companyName }} Logo\"\n class=\"max-h-15\"\n />\n <i\n *ngIf=\"image == null\"\n class=\"pi pi-spin pi-spinner primary-color\"\n style=\"font-size: 2rem\"\n ></i>\n </div>\n\n <ng-content></ng-content>\n\n <div *ngIf=\"config.showGoogleAuth\">\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 7px;\n justify-content: center;\n margin-bottom: 16px;\n \"\n >\n <div class=\"separator\"></div>\n <div>{{ t(\"or\") }}</div>\n <div class=\"separator\"></div>\n </div>\n <div>\n <!-- https://code-maze.com/how-to-sign-in-with-google-angular-aspnet-webapi/ -->\n <google-button\n (loginWithGoogle)=\"onGoogleSignIn($event)\"\n [label]=\"t('ContinueWithGoogle')\"\n ></google-button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: GoogleButtonComponent, selector: "google-button", inputs: ["label"], outputs: ["loginWithGoogle"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
2691
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: BaseFormComponent, deps: [{ token: i0.KeyValueDiffers }, { token: i1$2.HttpClient }, { token: SpiderlyMessageService }, { token: i0.ChangeDetectorRef }, { token: i3$2.Router }, { token: i3$2.ActivatedRoute }, { token: i1.TranslocoService }, { token: BaseFormService }], target: i0.ɵɵFactoryTarget.Component }); }
2692
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: BaseFormComponent, isStandalone: false, selector: "base-form", ngImport: i0, template: '', isInline: true }); }
2540
2693
  }
2541
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthComponent, decorators: [{
2694
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: BaseFormComponent, decorators: [{
2542
2695
  type: Component,
2543
- args: [{ selector: 'auth', imports: [CommonModule, GoogleButtonComponent, TranslocoDirective], template: "<ng-container *transloco=\"let t\">\n <div class=\"flex min-h-screen overflow-hidden p-5\">\n <div class=\"flex flex-col w-full\">\n <div\n class=\"w-full sm:w-120\"\n style=\"\n margin: auto;\n border-radius: 50px;\n padding: 0.3rem;\n background: linear-gradient(\n 180deg,\n var(--p-primary-color) 10%,\n rgba(33, 150, 243, 0) 30%\n );\n \"\n >\n <div\n class=\"surface-card py-12 px-8 sm:px-12\"\n style=\"border-radius: 45px\"\n >\n <div class=\"flex justify-center\" style=\"margin-bottom: 38px\">\n <img\n *ngIf=\"image != null\"\n [src]=\"image\"\n alt=\"{{ companyName }} Logo\"\n title=\"{{ companyName }} Logo\"\n class=\"max-h-15\"\n />\n <i\n *ngIf=\"image == null\"\n class=\"pi pi-spin pi-spinner primary-color\"\n style=\"font-size: 2rem\"\n ></i>\n </div>\n\n <ng-content></ng-content>\n\n <div *ngIf=\"config.showGoogleAuth\">\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 7px;\n justify-content: center;\n margin-bottom: 16px;\n \"\n >\n <div class=\"separator\"></div>\n <div>{{ t(\"or\") }}</div>\n <div class=\"separator\"></div>\n </div>\n <div>\n <!-- https://code-maze.com/how-to-sign-in-with-google-angular-aspnet-webapi/ -->\n <google-button\n (loginWithGoogle)=\"onGoogleSignIn($event)\"\n [label]=\"t('ContinueWithGoogle')\"\n ></google-button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n</ng-container>\n" }]
2544
- }], ctorParameters: () => [{ type: ConfigServiceBase }, { type: AuthServiceBase }], propDecorators: { onCompanyNameChange: [{
2545
- type: Output
2546
- }] } });
2696
+ args: [{ selector: 'base-form', template: '', standalone: false }]
2697
+ }], ctorParameters: () => [{ type: i0.KeyValueDiffers }, { type: i1$2.HttpClient }, { type: SpiderlyMessageService }, { type: i0.ChangeDetectorRef }, { type: i3$2.Router }, { type: i3$2.ActivatedRoute }, { type: i1.TranslocoService }, { type: BaseFormService }] });
2547
2698
 
2548
2699
  class PanelBodyComponent {
2549
2700
  constructor() {
@@ -2574,6 +2725,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
2574
2725
  class PanelHeaderComponent {
2575
2726
  constructor(translocoService) {
2576
2727
  this.translocoService = translocoService;
2728
+ /** Whether the header icon is shown. Defaults to `true`. */
2577
2729
  this.showIcon = true;
2578
2730
  }
2579
2731
  ngOnInit() {
@@ -2624,8 +2776,11 @@ class SpiderlyPanelComponent {
2624
2776
  this.toggleable = false;
2625
2777
  this.toggler = 'icon';
2626
2778
  this.collapsed = false;
2779
+ /** Whether the CRUD context-menu icon is shown. Defaults to `true`. */
2627
2780
  this.showCrudMenu = true;
2781
+ /** Whether a remove/delete icon is shown. Defaults to `false`. */
2628
2782
  this.showRemoveIcon = false;
2783
+ /** Whether the panel header is rendered. Defaults to `true`. */
2629
2784
  this.showPanelHeader = true;
2630
2785
  this.onMenuIconClick = new EventEmitter();
2631
2786
  this.onRemoveIconClick = new EventEmitter();
@@ -2793,8 +2948,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
2793
2948
  type: Input
2794
2949
  }] } });
2795
2950
 
2796
- class LoginComponent extends BaseFormComponent {
2797
- constructor(differs, http, messageService, changeDetectorRef, router, route, translocoService, baseFormService, authService, config) {
2951
+ class SpiderlyLoginComponent extends BaseFormComponent {
2952
+ constructor(differs, http, messageService, changeDetectorRef, router, route, translocoService, baseFormService, authService) {
2798
2953
  super(differs, http, messageService, changeDetectorRef, router, route, translocoService, baseFormService);
2799
2954
  this.differs = differs;
2800
2955
  this.http = http;
@@ -2805,21 +2960,31 @@ class LoginComponent extends BaseFormComponent {
2805
2960
  this.translocoService = translocoService;
2806
2961
  this.baseFormService = baseFormService;
2807
2962
  this.authService = authService;
2808
- this.config = config;
2809
2963
  this.loginFormGroup = new SpiderlyFormGroup({});
2810
2964
  this.showEmailSentDialog = false;
2965
+ /** Per-code provider icon overrides, forwarded to <spiderly-external-login>. */
2966
+ this.providerIcons = {};
2811
2967
  }
2812
2968
  ngOnInit() {
2813
2969
  this.initLoginFormGroup(new Login({}));
2970
+ this.showExternalAuthErrorIfPresent();
2971
+ }
2972
+ // Surface a friendly message when the server-side external login bounced back with an error (captured from
2973
+ // the URL at bootstrap by AuthServiceBase). "expired" = the user lingered on the provider's account picker.
2974
+ showExternalAuthErrorIfPresent() {
2975
+ const code = this.authService.externalAuthErrorCode;
2976
+ if (!code) {
2977
+ return;
2978
+ }
2979
+ this.authService.externalAuthErrorCode = null; // show once
2980
+ const messageKey = code === 'expired' ? 'ExternalLoginExpiredDetails' : 'ExternalLoginFailedDetails';
2981
+ this.messageService.warningMessage(this.translocoService.translate(messageKey));
2814
2982
  }
2815
2983
  initLoginFormGroup(model) {
2816
2984
  this.baseFormService.initFormGroup(this.loginFormGroup, Login, model, [
2817
2985
  'email',
2818
2986
  ]);
2819
2987
  }
2820
- companyNameChange(companyName) {
2821
- this.companyName = companyName;
2822
- }
2823
2988
  sendLoginVerificationEmail() {
2824
2989
  let isFormGroupValid = this.baseFormService.isControlValid(this.loginFormGroup);
2825
2990
  if (isFormGroupValid == false) {
@@ -2835,20 +3000,23 @@ class LoginComponent extends BaseFormComponent {
2835
3000
  this.showEmailSentDialog = true;
2836
3001
  });
2837
3002
  }
2838
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: LoginComponent, deps: [{ token: i0.KeyValueDiffers }, { token: i1$2.HttpClient }, { token: SpiderlyMessageService }, { token: i0.ChangeDetectorRef }, { token: i3$2.Router }, { token: i3$2.ActivatedRoute }, { token: i1.TranslocoService }, { token: BaseFormService }, { token: AuthServiceBase }, { token: ConfigServiceBase }], target: i0.ɵɵFactoryTarget.Component }); }
2839
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.13", type: LoginComponent, isStandalone: true, selector: "app-login", usesInheritance: true, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n @if (loginFormGroup != null) {\n @if (showEmailSentDialog == false) {\n <auth (onCompanyNameChange)=\"companyNameChange($event)\">\n <!-- We are not loading anything from the server here so we don't need defer block -->\n <form [formGroup]=\"loginFormGroup\" style=\"margin-bottom: 16px\">\n <div>\n <spiderly-textbox\n [control]=\"loginFormGroup.getControl('email')\"\n ></spiderly-textbox>\n </div>\n\n <div class=\"mt-4 mb-6\">\n <div class=\"text-center\" style=\"font-size: smaller\">\n {{ t(\"AgreementsOnRegister\") }}\n <b\n routerLink=\"/user-agreement\"\n class=\"primary-color cursor-pointer\"\n >{{ t(\"UserAgreement\") }}</b\n >\n {{ t(\"and\") }}\n <b\n routerLink=\"/privacy-policy\"\n class=\"primary-color cursor-pointer\"\n >{{ t(\"PrivacyPolicy\") }}</b\n >.\n </div>\n </div>\n\n <div style=\"display: flex; flex-direction: column; gap: 16px\">\n <spiderly-button\n [label]=\"t('Login')\"\n (onClick)=\"sendLoginVerificationEmail()\"\n [outlined]=\"true\"\n [style]=\"{ width: '100%' }\"\n type=\"submit\"\n ></spiderly-button>\n </div>\n </form>\n </auth>\n } @else {\n <login-verification\n [email]=\"loginFormGroup.controls.email.getRawValue()\"\n ></login-verification>\n }\n } @else {\n <!-- TODO: Add skeleton -->\n }\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: AuthComponent, selector: "auth", outputs: ["onCompanyNameChange"] }, { kind: "ngmodule", type: SpiderlyControlsModule }, { kind: "component", type: SpiderlyTextboxComponent, selector: "spiderly-textbox", inputs: ["showButton", "buttonIcon"], outputs: ["onButtonClick"] }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "component", type: LoginVerificationComponent, selector: "login-verification", inputs: ["email", "userId"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
3003
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyLoginComponent, deps: [{ token: i0.KeyValueDiffers }, { token: i1$2.HttpClient }, { token: SpiderlyMessageService }, { token: i0.ChangeDetectorRef }, { token: i3$2.Router }, { token: i3$2.ActivatedRoute }, { token: i1.TranslocoService }, { token: BaseFormService }, { token: AuthServiceBase }], target: i0.ɵɵFactoryTarget.Component }); }
3004
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.13", type: SpiderlyLoginComponent, isStandalone: true, selector: "spiderly-login", inputs: { providerIcons: "providerIcons" }, usesInheritance: true, ngImport: i0, template: "<ng-container *transloco=\"let t\">\n @if (loginFormGroup != null) {\n @if (showEmailSentDialog == false) {\n <spiderly-auth-card>\n <form [formGroup]=\"loginFormGroup\" style=\"margin-bottom: 16px\">\n <div>\n <spiderly-textbox\n [control]=\"loginFormGroup.getControl('email')\"\n ></spiderly-textbox>\n </div>\n\n <div class=\"mt-4 mb-6\">\n <div class=\"text-center\" style=\"font-size: smaller\">\n {{ t(\"AgreementsOnRegister\") }}\n <b\n routerLink=\"/user-agreement\"\n class=\"primary-color cursor-pointer\"\n >{{ t(\"UserAgreement\") }}</b\n >\n {{ t(\"and\") }}\n <b\n routerLink=\"/privacy-policy\"\n class=\"primary-color cursor-pointer\"\n >{{ t(\"PrivacyPolicy\") }}</b\n >.\n </div>\n </div>\n\n <div style=\"display: flex; flex-direction: column; gap: 16px\">\n <spiderly-button\n [label]=\"t('Login')\"\n (onClick)=\"sendLoginVerificationEmail()\"\n [outlined]=\"true\"\n [style]=\"{ width: '100%' }\"\n type=\"submit\"\n ></spiderly-button>\n </div>\n </form>\n\n <spiderly-external-login\n [providerIcons]=\"providerIcons\"\n ></spiderly-external-login>\n </spiderly-auth-card>\n } @else {\n <login-verification\n [email]=\"loginFormGroup.controls.email.getRawValue()\"\n ></login-verification>\n }\n }\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: AuthCardComponent, selector: "spiderly-auth-card" }, { kind: "component", type: ExternalLoginComponent, selector: "spiderly-external-login", inputs: ["providerIcons"] }, { kind: "ngmodule", type: SpiderlyControlsModule }, { kind: "component", type: SpiderlyTextboxComponent, selector: "spiderly-textbox", inputs: ["showButton", "buttonIcon"], outputs: ["onButtonClick"] }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "component", type: LoginVerificationComponent, selector: "login-verification", inputs: ["email", "userId"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] }); }
2840
3005
  }
2841
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: LoginComponent, decorators: [{
3006
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyLoginComponent, decorators: [{
2842
3007
  type: Component,
2843
- args: [{ selector: 'app-login', imports: [
3008
+ args: [{ selector: 'spiderly-login', imports: [
2844
3009
  CommonModule,
2845
3010
  ReactiveFormsModule,
2846
- AuthComponent,
3011
+ AuthCardComponent,
3012
+ ExternalLoginComponent,
2847
3013
  SpiderlyControlsModule,
2848
3014
  LoginVerificationComponent,
2849
3015
  TranslocoDirective,
2850
- ], template: "<ng-container *transloco=\"let t\">\n @if (loginFormGroup != null) {\n @if (showEmailSentDialog == false) {\n <auth (onCompanyNameChange)=\"companyNameChange($event)\">\n <!-- We are not loading anything from the server here so we don't need defer block -->\n <form [formGroup]=\"loginFormGroup\" style=\"margin-bottom: 16px\">\n <div>\n <spiderly-textbox\n [control]=\"loginFormGroup.getControl('email')\"\n ></spiderly-textbox>\n </div>\n\n <div class=\"mt-4 mb-6\">\n <div class=\"text-center\" style=\"font-size: smaller\">\n {{ t(\"AgreementsOnRegister\") }}\n <b\n routerLink=\"/user-agreement\"\n class=\"primary-color cursor-pointer\"\n >{{ t(\"UserAgreement\") }}</b\n >\n {{ t(\"and\") }}\n <b\n routerLink=\"/privacy-policy\"\n class=\"primary-color cursor-pointer\"\n >{{ t(\"PrivacyPolicy\") }}</b\n >.\n </div>\n </div>\n\n <div style=\"display: flex; flex-direction: column; gap: 16px\">\n <spiderly-button\n [label]=\"t('Login')\"\n (onClick)=\"sendLoginVerificationEmail()\"\n [outlined]=\"true\"\n [style]=\"{ width: '100%' }\"\n type=\"submit\"\n ></spiderly-button>\n </div>\n </form>\n </auth>\n } @else {\n <login-verification\n [email]=\"loginFormGroup.controls.email.getRawValue()\"\n ></login-verification>\n }\n } @else {\n <!-- TODO: Add skeleton -->\n }\n</ng-container>\n" }]
2851
- }], ctorParameters: () => [{ type: i0.KeyValueDiffers }, { type: i1$2.HttpClient }, { type: SpiderlyMessageService }, { type: i0.ChangeDetectorRef }, { type: i3$2.Router }, { type: i3$2.ActivatedRoute }, { type: i1.TranslocoService }, { type: BaseFormService }, { type: AuthServiceBase }, { type: ConfigServiceBase }] });
3016
+ ], template: "<ng-container *transloco=\"let t\">\n @if (loginFormGroup != null) {\n @if (showEmailSentDialog == false) {\n <spiderly-auth-card>\n <form [formGroup]=\"loginFormGroup\" style=\"margin-bottom: 16px\">\n <div>\n <spiderly-textbox\n [control]=\"loginFormGroup.getControl('email')\"\n ></spiderly-textbox>\n </div>\n\n <div class=\"mt-4 mb-6\">\n <div class=\"text-center\" style=\"font-size: smaller\">\n {{ t(\"AgreementsOnRegister\") }}\n <b\n routerLink=\"/user-agreement\"\n class=\"primary-color cursor-pointer\"\n >{{ t(\"UserAgreement\") }}</b\n >\n {{ t(\"and\") }}\n <b\n routerLink=\"/privacy-policy\"\n class=\"primary-color cursor-pointer\"\n >{{ t(\"PrivacyPolicy\") }}</b\n >.\n </div>\n </div>\n\n <div style=\"display: flex; flex-direction: column; gap: 16px\">\n <spiderly-button\n [label]=\"t('Login')\"\n (onClick)=\"sendLoginVerificationEmail()\"\n [outlined]=\"true\"\n [style]=\"{ width: '100%' }\"\n type=\"submit\"\n ></spiderly-button>\n </div>\n </form>\n\n <spiderly-external-login\n [providerIcons]=\"providerIcons\"\n ></spiderly-external-login>\n </spiderly-auth-card>\n } @else {\n <login-verification\n [email]=\"loginFormGroup.controls.email.getRawValue()\"\n ></login-verification>\n }\n }\n</ng-container>\n" }]
3017
+ }], ctorParameters: () => [{ type: i0.KeyValueDiffers }, { type: i1$2.HttpClient }, { type: SpiderlyMessageService }, { type: i0.ChangeDetectorRef }, { type: i3$2.Router }, { type: i3$2.ActivatedRoute }, { type: i1.TranslocoService }, { type: BaseFormService }, { type: AuthServiceBase }], propDecorators: { providerIcons: [{
3018
+ type: Input
3019
+ }] } });
2852
3020
 
2853
3021
  class CardSkeletonComponent {
2854
3022
  constructor() {
@@ -2893,6 +3061,7 @@ class IndexCardComponent {
2893
3061
  constructor(formBuilder) {
2894
3062
  this.formBuilder = formBuilder;
2895
3063
  this.header = '';
3064
+ /** Whether the CRUD context-menu icon is shown. Defaults to `true`. */
2896
3065
  this.showCrudMenu = true;
2897
3066
  this.onMenuIconClick = new EventEmitter();
2898
3067
  this.onRemoveIconClick = new EventEmitter();
@@ -2934,6 +3103,7 @@ class InfoCardComponent {
2934
3103
  constructor(formBuilder) {
2935
3104
  this.formBuilder = formBuilder;
2936
3105
  this.header = '';
3106
+ /** Whether the small icon variant is shown. Defaults to `true`. */
2937
3107
  this.showSmallIcon = true;
2938
3108
  this.icon = 'pi pi-info-circle';
2939
3109
  this.textColor = '';
@@ -2976,22 +3146,16 @@ class LayoutServiceBase {
2976
3146
  inputStyle: 'outlined',
2977
3147
  menuMode: 'static',
2978
3148
  colorScheme: 'light',
2979
- theme: 'lara-light-indigo',
2980
- scale: 14,
2981
- color: `var(--p-primary-color)`,
2982
3149
  };
2983
3150
  this.state = {
2984
3151
  staticMenuDesktopInactive: false,
2985
3152
  overlayMenuActive: false,
2986
3153
  profileSidebarVisible: false,
2987
3154
  profileDropdownSidebarVisible: false,
2988
- configSidebarVisible: false,
2989
3155
  staticMenuMobileActive: false,
2990
3156
  menuHoverActive: false,
2991
3157
  };
2992
- this.configUpdate = new Subject();
2993
3158
  this.overlayOpen = new Subject();
2994
- this.configUpdate$ = this.configUpdate.asObservable();
2995
3159
  this.overlayOpen$ = this.overlayOpen.asObservable();
2996
3160
  //#region Top Bar
2997
3161
  this.initTopBarData = () => {
@@ -3042,9 +3206,6 @@ class LayoutServiceBase {
3042
3206
  this.overlayOpen.next(null);
3043
3207
  }
3044
3208
  }
3045
- showConfigSidebar() {
3046
- this.state.configSidebarVisible = true;
3047
- }
3048
3209
  isOverlay() {
3049
3210
  return this.layoutConfig.menuMode === 'overlay';
3050
3211
  }
@@ -3054,9 +3215,6 @@ class LayoutServiceBase {
3054
3215
  isMobile() {
3055
3216
  return !this.isDesktop();
3056
3217
  }
3057
- onConfigUpdate() {
3058
- this.configUpdate.next(this.layoutConfig);
3059
- }
3060
3218
  //#endregion
3061
3219
  ngOnDestroy() {
3062
3220
  if (this.userSubscription) {
@@ -3297,6 +3455,7 @@ class ProfileAvatarComponent {
3297
3455
  this.config = config;
3298
3456
  this.isSideMenuLayout = true;
3299
3457
  this.routeOnLargeProfileAvatarClick = true;
3458
+ /** Whether the login button is shown when no user is signed in. Defaults to `true`. */
3300
3459
  this.showLoginButton = true;
3301
3460
  this.routeToLoginPage = true;
3302
3461
  this.loginButtonOutlined = false;
@@ -3719,7 +3878,9 @@ class SpiderlyDataTableComponent {
3719
3878
  this.locale = locale;
3720
3879
  this.destroy$ = new Subject();
3721
3880
  this.tableIcon = 'pi pi-list';
3722
- this.showPaginator = true; // Pass only when hasLazyLoad === false
3881
+ /** Whether the paginator is shown. Pass only when `hasLazyLoad === false`. Defaults to `true`. */
3882
+ this.showPaginator = true;
3883
+ /** Whether the table is wrapped in a card container. Defaults to `false`. */
3723
3884
  this.showCardWrapper = false;
3724
3885
  this.readonly = false;
3725
3886
  this.idField = 'id';
@@ -3737,8 +3898,11 @@ class SpiderlyDataTableComponent {
3737
3898
  this.onIsAllSelectedChange = new EventEmitter();
3738
3899
  this.matchModeDateOptions = [];
3739
3900
  this.matchModeNumberOptions = [];
3901
+ /** Whether the "Add" button is shown. Defaults to `true`. */
3740
3902
  this.showAddButton = true;
3903
+ /** Whether the "Export to Excel" button is shown. Defaults to `true`. */
3741
3904
  this.showExportToExcelButton = true;
3905
+ /** Whether the reload-table button is shown. Defaults to `false`. */
3742
3906
  this.showReloadTableButton = false;
3743
3907
  this.hasLazyLoad = true;
3744
3908
  /** 'session' persists across refresh only; 'local' persists indefinitely. */
@@ -4388,12 +4552,14 @@ class SpiderlyDataViewComponent {
4388
4552
  this.rows = 10;
4389
4553
  this.filters = [];
4390
4554
  this.onLazyLoad = new EventEmitter();
4555
+ /** Whether the data view is wrapped in a card container. Defaults to `true`. */
4391
4556
  this.showCardWrapper = true;
4392
4557
  /**
4393
4558
  * Whether to display additional data on the right side of the paginator.
4394
4559
  * Defaults to `false`.
4395
4560
  */
4396
4561
  this.showPaginatorRightData = false;
4562
+ /** Whether the total records count is shown. Defaults to `false`. */
4397
4563
  this.showTotalRecordsNumber = false;
4398
4564
  this.applyFiltersIcon = 'pi pi-filter';
4399
4565
  this.clearFiltersIcon = 'pi pi-filter-slash';
@@ -4762,18 +4928,13 @@ class AuthGuard {
4762
4928
  return this.checkAuth();
4763
4929
  }
4764
4930
  checkAuth() {
4765
- return this.authService.user$.pipe(map((user) => {
4931
+ return this.authService.user$.pipe(filter$1((user) => user !== undefined), // wait until the session is resolved (undefined = still loading)
4932
+ take(1), map((user) => {
4766
4933
  if (user) {
4767
4934
  return true;
4768
4935
  }
4769
- else {
4770
- // const returnUrl = this.router.getCurrentNavigation()?.extractedUrl.toString() || '/';
4771
- // this.router.navigate(['login'], {
4772
- // queryParams: { returnUrl },
4773
- // });
4774
- this.router.navigate([this.config.loginSlug]);
4775
- return false;
4776
- }
4936
+ this.router.navigate([this.config.loginSlug]);
4937
+ return false;
4777
4938
  }));
4778
4939
  }
4779
4940
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthGuard, deps: [{ token: AuthServiceBase }, { token: i3$2.Router }, { token: ConfigServiceBase }], target: i0.ɵɵFactoryTarget.Injectable }); }
@@ -4794,14 +4955,13 @@ class NotAuthGuard {
4794
4955
  return this.checkAuth();
4795
4956
  }
4796
4957
  checkAuth() {
4797
- return this.authService.user$.pipe(map((user) => {
4958
+ return this.authService.user$.pipe(filter$1((user) => user !== undefined), // wait until the session is resolved (undefined = still loading)
4959
+ take(1), map((user) => {
4798
4960
  if (user) {
4799
4961
  this.authService.navigateToDashboard();
4800
4962
  return false;
4801
4963
  }
4802
- else {
4803
- return true;
4804
- }
4964
+ return true;
4805
4965
  }));
4806
4966
  }
4807
4967
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: NotAuthGuard, deps: [{ token: AuthServiceBase }], target: i0.ɵɵFactoryTarget.Injectable }); }
@@ -4909,75 +5069,90 @@ const convertToDate = (object, parent, key) => {
4909
5069
  }
4910
5070
  };
4911
5071
 
5072
+ // Cookie-based auth: the session JWTs are HttpOnly cookies, so we just send credentials on API calls.
5073
+ // The browser attaches/refreshes the cookies; JS never holds the tokens (XSS-safe).
5074
+ //
5075
+ // CSRF: state-changing requests (POST/PUT/DELETE/PATCH) authenticated via cookie must include the
5076
+ // X-CSRF header, otherwise Spiderly.Shared/Attributes/AuthGuardAttribute.cs returns 403 Forbidden
5077
+ // (the server-side check was added in commit 92f238d but the matching client-side header was never
5078
+ // emitted, so every cookie-authed write was failing in the admin). The check is presence-only —
5079
+ // any non-empty value works — and the protection comes from the fact that a cross-origin form
5080
+ // submission cannot set custom request headers without a CORS preflight.
5081
+ const SAFE_HTTP_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
4912
5082
  const jwtInterceptor = (req, next) => {
4913
5083
  const config = inject(ConfigServiceBase);
4914
- const platformId = inject(PLATFORM_ID);
4915
- let accessToken = null;
4916
- if (isPlatformBrowser(platformId)) {
4917
- accessToken = localStorage.getItem(config.accessTokenKey);
4918
- }
4919
5084
  const isApiUrl = req.url.startsWith(config.apiUrl);
4920
- if (accessToken && isApiUrl) {
5085
+ if (isApiUrl) {
5086
+ const isStateChanging = !SAFE_HTTP_METHODS.has(req.method.toUpperCase());
4921
5087
  req = req.clone({
4922
- setHeaders: { Authorization: `Bearer ${accessToken}` },
5088
+ withCredentials: true,
5089
+ ...(isStateChanging && { setHeaders: { 'X-CSRF': '1' } }),
4923
5090
  });
4924
5091
  }
4925
5092
  return next(req);
4926
5093
  };
4927
5094
 
5095
+ /**
5096
+ * Owns cross-cutting HTTP-error UX: shows the right message and, on an expired session, clears auth — then
5097
+ * RETHROWS. Errors stay errors: callers run only their success path, and an unhandled HttpErrorResponse that
5098
+ * reaches the global ErrorHandler is intentionally ignored there (HTTP-error UX lives here). This interceptor
5099
+ * must never convert an error into a value — doing so makes callers treat failures as data.
5100
+ */
4928
5101
  const unauthorizedInterceptor = (req, next) => {
4929
5102
  const messageService = inject(SpiderlyMessageService);
4930
5103
  const translocoService = inject(TranslocoService);
4931
5104
  const config = inject(ConfigServiceBase);
4932
5105
  const authService = inject(AuthServiceBase);
4933
- const handleAuthError = (err, request) => {
5106
+ const reactToError = (err, request) => {
4934
5107
  if (!config.production) {
4935
5108
  console.error(err);
4936
5109
  }
4937
5110
  let errorResponse = err.error;
4938
- if (request.responseType != 'json')
4939
- errorResponse = JSON.parse(err.error);
4940
- if (err.status == 0) {
5111
+ if (request.responseType !== 'json' && typeof err.error === 'string') {
5112
+ try {
5113
+ errorResponse = JSON.parse(err.error);
5114
+ }
5115
+ catch {
5116
+ errorResponse = null;
5117
+ }
5118
+ }
5119
+ if (err.status === 0) {
5120
+ // Server unreachable; defer so the message isn't lost during a shutdown/refresh race.
4941
5121
  setTimeout(() => {
4942
- // Had problem when the server is shut down, and try to refresh token, warning message didn't appear
4943
5122
  messageService.warningMessage(translocoService.translate('ServerLostConnectionDetails'), translocoService.translate('ServerLostConnectionTitle'));
4944
5123
  }, 100);
4945
- return of(err.message);
4946
5124
  }
4947
- else if (err.status == 400) {
4948
- messageService.warningMessage(errorResponse.message ??
4949
- translocoService.translate('BadRequestDetails'), translocoService.translate('Warning'));
4950
- return of(err.message);
5125
+ else if (err.status === 400) {
5126
+ messageService.warningMessage(errorResponse?.message ?? translocoService.translate('BadRequestDetails'), translocoService.translate('Warning'));
4951
5127
  }
4952
- else if (err.status == 401) {
5128
+ else if (err.status === 401) {
4953
5129
  if (errorResponse?.errorCode === ApiErrorCodes.InvalidToken) {
4954
- authService.clearLocalStorage();
4955
- return of(err.message);
5130
+ authService.clearSession(); // expired/invalid session — drop it; guards send the user to login
5131
+ }
5132
+ else {
5133
+ messageService.warningMessage(errorResponse?.message ?? translocoService.translate('LoginRequired'), translocoService.translate('Warning'));
4956
5134
  }
4957
- messageService.warningMessage(errorResponse?.message ?? translocoService.translate('LoginRequired'), translocoService.translate('Warning'));
4958
- return of(err.message);
4959
5135
  }
4960
- else if (err.status == 403) {
5136
+ else if (err.status === 403) {
4961
5137
  messageService.warningMessage(translocoService.translate('PermissionErrorDetails'), translocoService.translate('PermissionErrorTitle'));
4962
- return of(err.message);
4963
5138
  }
4964
- else if (err.status == 404) {
5139
+ else if (err.status === 404) {
4965
5140
  messageService.warningMessage(translocoService.translate('NotFoundDetails'), translocoService.translate('NotFoundTitle'));
4966
- return of(err.message);
4967
5141
  }
4968
5142
  else {
4969
5143
  messageService.errorMessage(translocoService.translate('UnexpectedErrorDetails'), translocoService.translate('UnexpectedErrorTitle'));
4970
- return of(err.message);
4971
5144
  }
4972
5145
  };
4973
5146
  return next(req).pipe(catchError((err) => {
4974
- return handleAuthError(err, req);
5147
+ reactToError(err, req);
5148
+ return throwError(() => err);
4975
5149
  }));
4976
5150
  };
4977
5151
 
4978
5152
  function authInitializer(authService, platformId) {
4979
5153
  if (isPlatformBrowser(platformId)) {
4980
5154
  return () => {
5155
+ authService.captureExternalAuthError(); // before the router can strip ?externalAuthError on the /login redirect
4981
5156
  return authService.refreshToken();
4982
5157
  };
4983
5158
  }
@@ -5028,5 +5203,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
5028
5203
  * Generated bundle index. Do not edit.
5029
5204
  */
5030
5205
 
5031
- export { Action, AllClickEvent, ApiErrorCodes, ApiSecurityService, AppSidebarComponent, AuthGuard, AuthResult, AuthResultWithCookies, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormComponent, 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, parseDateOnlyLocal, primitiveArrayTypes, pushAction, selectedTab, singleOrDefault, splitPascalCase, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
5206
+ export { Action, AllClickEvent, ApiErrorCodes, ApiSecurityService, AppSidebarComponent, AuthCardComponent, AuthGuard, AuthResult, AuthResultWithCookies, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormComponent, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, DEFAULT_EXTERNAL_PROVIDER_ICONS, ExternalLoginComponent, ExternalProvider, ExternalProviderPublic, Filter, FilterRule, FilterSortMeta, FooterComponent, IndexCardComponent, InfoCardComponent, InitCompanyAuthDialogDetails, InitTopBarData, IsAuthorizedForSaveEvent, LastMenuIconIndexClicked, LayoutServiceBase, LazyLoadSelectedIdsResult, Login, 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, SpiderlyLoginComponent, SpiderlyMarkdownComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, capitalizeFirstChar, deleteAction, exportListToExcel, firstCharToUpper, getFileNameFromContentDisposition, getHtmlImgDisplayString64, getImageDimensions, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, getPrimengNamebookOptions, httpLoadingInterceptor, isExcelFileType, isFileImageType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, parseDateOnlyLocal, primitiveArrayTypes, pushAction, selectedTab, singleOrDefault, splitPascalCase, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
5032
5207
  //# sourceMappingURL=spiderly.mjs.map