spiderly 19.8.2 → 19.8.4

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 +833 -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 +3 -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,23 @@ 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',
87
+ ExternalEmailMissing: 'external_email_missing',
83
88
  };
84
89
 
85
90
  class BaseControl {
86
91
  constructor(translocoService) {
87
92
  this.translocoService = translocoService;
88
93
  this.disabled = false;
94
+ /** Whether the field's label is rendered. Defaults to `true`. */
89
95
  this.showLabel = true;
96
+ /** Whether the required (*) indicator is shown next to the label. Defaults to `true`. */
90
97
  this.showRequired = true;
91
98
  this.label = null; // NgModel/Want custom translation
92
99
  this.controlValid = true; // NgModel
93
100
  this.placeholder = '';
101
+ /** Whether the info tooltip icon is shown next to the label. Defaults to `false`. */
94
102
  this.showTooltip = false;
95
103
  this.tooltipText = null;
96
104
  this.tooltipIcon = 'pi pi-info-circle';
@@ -143,6 +151,7 @@ class BaseDropdownControl extends BaseControl {
143
151
  constructor(translocoService) {
144
152
  super(translocoService);
145
153
  this.translocoService = translocoService;
154
+ /** Whether an addon button is shown next to the dropdown. Defaults to `false`. */
146
155
  this.showAddon = false;
147
156
  this.addonIcon = 'pi pi-ellipsis-h';
148
157
  this.placeholder = this.translocoService.translate('SelectFromTheList');
@@ -390,6 +399,7 @@ class SpiderlyAutocompleteComponent extends BaseAutocompleteControl {
390
399
  this.translocoService = translocoService;
391
400
  this.validatorService = validatorService;
392
401
  this.appendTo = 'body';
402
+ /** Whether a clear button is shown. Defaults to `true`. */
393
403
  this.showClear = true;
394
404
  this.helperFormControl = new SpiderlyFormControl(null, {
395
405
  updateOn: 'change',
@@ -622,10 +632,11 @@ function exportListToExcel(exportListToExcelObservableMethod, filter) {
622
632
  FileSaver.saveAs(res.body, decodeURIComponent(fileName));
623
633
  });
624
634
  }
635
+ function getPrimengNamebookOptions(namebookList) {
636
+ return namebookList.map((x) => ({ label: x.displayName, code: x.id }));
637
+ }
625
638
  function getPrimengDropdownNamebookOptions(getDropdownListObservable, parentEntityId) {
626
- return getDropdownListObservable(parentEntityId ?? 0).pipe(map((res) => {
627
- return res.map((x) => ({ label: x.displayName, code: x.id }));
628
- }));
639
+ return getDropdownListObservable(parentEntityId ?? 0).pipe(map((res) => getPrimengNamebookOptions(res)));
629
640
  }
630
641
  function getPrimengDropdownCodebookOptions(getDropdownListObservable) {
631
642
  return getDropdownListObservable().pipe(map((res) => {
@@ -675,25 +686,6 @@ function kebabToTitleCase(input) {
675
686
  .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
676
687
  .join(' ');
677
688
  }
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
689
  const PROPS_KEY = Symbol('props');
698
690
  function ReflectProp(target, propertyKey) {
699
691
  const props = Reflect.getMetadata(PROPS_KEY, target) || [];
@@ -720,6 +712,7 @@ class SpiderlyCalendarComponent extends BaseControl {
720
712
  constructor(translocoService) {
721
713
  super(translocoService);
722
714
  this.translocoService = translocoService;
715
+ /** Whether the time picker is shown in addition to the date. Defaults to `false`. */
723
716
  this.showTime = false;
724
717
  this.dateOnly = false;
725
718
  this.timeOnly = false;
@@ -880,6 +873,7 @@ class SpiderlyColorPickerComponent extends BaseControl {
880
873
  constructor(translocoService) {
881
874
  super(translocoService);
882
875
  this.translocoService = translocoService;
876
+ /** Whether a hex text input is shown alongside the color swatch. Defaults to `true`. */
883
877
  this.showInputTextField = true;
884
878
  }
885
879
  ngOnInit() {
@@ -937,6 +931,7 @@ class SpiderlyPasswordComponent extends BaseControl {
937
931
  constructor(translocoService) {
938
932
  super(translocoService);
939
933
  this.translocoService = translocoService;
934
+ /** Whether a password-strength meter is shown below the field. Defaults to `false`. */
940
935
  this.showPasswordStrength = false;
941
936
  }
942
937
  ngOnInit() {
@@ -962,6 +957,7 @@ class SpiderlyTextboxComponent extends BaseControl {
962
957
  constructor(translocoService) {
963
958
  super(translocoService);
964
959
  this.translocoService = translocoService;
960
+ /** Whether an icon button is appended to the input. Defaults to `false`. */
965
961
  this.showButton = false;
966
962
  this.onButtonClick = new EventEmitter();
967
963
  }
@@ -1039,6 +1035,7 @@ class SpiderlyNumberComponent extends BaseControl {
1039
1035
  constructor(translocoService) {
1040
1036
  super(translocoService);
1041
1037
  this.translocoService = translocoService;
1038
+ /** Whether increment/decrement spinner buttons are shown. Defaults to `true`. */
1042
1039
  this.showButtons = true;
1043
1040
  this.maxFractionDigits = 0;
1044
1041
  }
@@ -1171,6 +1168,114 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1171
1168
  type: Input
1172
1169
  }] } });
1173
1170
 
1171
+ /**
1172
+ * Markdown form control: a plain textarea (Write) with a rendered live preview (Preview),
1173
+ * arranged as tabs. The stored value is raw Markdown text.
1174
+ *
1175
+ * The preview is rendered with ngx-markdown (marked) and is intentionally APPROXIMATE — a
1176
+ * consuming storefront may render the same Markdown with a different engine/flavor.
1177
+ *
1178
+ * The <textarea> DOM is the source of truth for the text (the form control mirrors it, like
1179
+ * spiderly-editor mirrors Quill). We never splice control.value, because SpiderlyFormControl
1180
+ * defaults to updateOn:'blur' and would be stale relative to the focused textarea.
1181
+ *
1182
+ * When {@link uploadImageMethod} is provided (wired by the generator for properties with an
1183
+ * S3 public-storage attribute), pasting an image uploads it and, on success, inserts a
1184
+ * standard `![](url)` link at the caret via execCommand (which preserves the native undo
1185
+ * stack). Upload progress is shown out-of-band, not as a token in the text.
1186
+ */
1187
+ class SpiderlyMarkdownComponent extends BaseControl {
1188
+ constructor(translocoService) {
1189
+ super(translocoService);
1190
+ this.translocoService = translocoService;
1191
+ this.objectId = 0;
1192
+ this.pendingImageUploads = 0;
1193
+ this.imageUploadFailed = false;
1194
+ }
1195
+ ngOnInit() {
1196
+ super.ngOnInit();
1197
+ }
1198
+ onPaste(event) {
1199
+ // Only intercept when image upload is wired; otherwise let the default paste happen.
1200
+ if (!this.uploadImageMethod || this.control?.disabled)
1201
+ return;
1202
+ const imageFile = this.getPastedImage(event);
1203
+ if (!imageFile)
1204
+ return;
1205
+ event.preventDefault();
1206
+ const formData = new FormData();
1207
+ formData.append('file', imageFile, `${this.objectId}-${imageFile.name || 'pasted-image.png'}`);
1208
+ this.imageUploadFailed = false;
1209
+ this.pendingImageUploads++;
1210
+ this.uploadImageMethod(formData).subscribe({
1211
+ next: (result) => {
1212
+ this.pendingImageUploads--;
1213
+ this.insertImageMarkdown(result.url);
1214
+ },
1215
+ error: () => {
1216
+ this.pendingImageUploads--;
1217
+ this.imageUploadFailed = true;
1218
+ },
1219
+ });
1220
+ }
1221
+ getPastedImage(event) {
1222
+ const items = event.clipboardData?.items;
1223
+ if (!items)
1224
+ return null;
1225
+ for (let i = 0; i < items.length; i++) {
1226
+ if (items[i].type.startsWith('image/')) {
1227
+ return items[i].getAsFile();
1228
+ }
1229
+ }
1230
+ return null;
1231
+ }
1232
+ insertImageMarkdown(url) {
1233
+ const snippet = `![](${url})`;
1234
+ const textarea = this.textareaRef?.nativeElement;
1235
+ // Preferred path: the textarea is focused, so insert at the caret while preserving the
1236
+ // native undo stack. The resulting 'input' event syncs the control on blur, exactly like
1237
+ // the user typing — no manual setValue, no stale-model read.
1238
+ if (textarea && document.activeElement === textarea && document.execCommand('insertText', false, snippet)) {
1239
+ return;
1240
+ }
1241
+ // Fallback (textarea blurred/absent, or execCommand unsupported): read the LIVE textarea
1242
+ // value — never control.value, which is stale while focused. When blurred, the control is
1243
+ // already current, so appending can't drop uncommitted text.
1244
+ if (textarea) {
1245
+ const sep = textarea.value.length ? '\n' : '';
1246
+ textarea.value = `${textarea.value}${sep}${snippet}`;
1247
+ this.control.setValue(textarea.value);
1248
+ }
1249
+ else {
1250
+ const current = this.control.value ?? '';
1251
+ this.control.setValue(current.length ? `${current}\n${snippet}` : snippet);
1252
+ }
1253
+ this.control.markAsDirty();
1254
+ }
1255
+ 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 }); }
1256
+ 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"] }] }); }
1257
+ }
1258
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyMarkdownComponent, decorators: [{
1259
+ type: Component,
1260
+ args: [{ selector: 'spiderly-markdown', imports: [
1261
+ CommonModule,
1262
+ ReactiveFormsModule,
1263
+ FormsModule,
1264
+ TextareaModule,
1265
+ TabsModule,
1266
+ MarkdownComponent,
1267
+ RequiredComponent,
1268
+ TranslocoDirective,
1269
+ ], 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" }]
1270
+ }], ctorParameters: () => [{ type: i1.TranslocoService }], propDecorators: { textareaRef: [{
1271
+ type: ViewChild,
1272
+ args: ['textarea']
1273
+ }], uploadImageMethod: [{
1274
+ type: Input
1275
+ }], objectId: [{
1276
+ type: Input
1277
+ }] } });
1278
+
1174
1279
  class SpiderlyButtonBaseComponent {
1175
1280
  constructor(router) {
1176
1281
  this.router = router;
@@ -1199,13 +1304,15 @@ class SpiderlyButtonBaseComponent {
1199
1304
  this.subscription.unsubscribe();
1200
1305
  }
1201
1306
  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 }] }); }
1307
+ 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
1308
  }
1204
1309
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyButtonBaseComponent, decorators: [{
1205
1310
  type: Component,
1206
1311
  args: [{ selector: 'spiderly-button-base', template: ``, imports: [CommonModule, ButtonModule, SplitButtonModule] }]
1207
1312
  }], ctorParameters: () => [{ type: i3$2.Router }], propDecorators: { icon: [{
1208
1313
  type: Input
1314
+ }], iconUrl: [{
1315
+ type: Input
1209
1316
  }], label: [{
1210
1317
  type: Input
1211
1318
  }], outlined: [{
@@ -1239,11 +1346,11 @@ class SpiderlyButtonComponent extends SpiderlyButtonBaseComponent {
1239
1346
  this.type = 'button';
1240
1347
  }
1241
1348
  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 }] }); }
1349
+ 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
1350
  }
1244
1351
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyButtonComponent, decorators: [{
1245
1352
  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" }]
1353
+ 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
1354
  }], propDecorators: { type: [{
1248
1355
  type: Input
1249
1356
  }] } });
@@ -1415,7 +1522,7 @@ class SpiderlyFileComponent extends BaseControl {
1415
1522
  return isExcelFileType(mimeType);
1416
1523
  }
1417
1524
  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"] }] }); }
1525
+ 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
1526
  }
1420
1527
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyFileComponent, decorators: [{
1421
1528
  type: Component,
@@ -1508,6 +1615,7 @@ class SpiderlyControlsModule {
1508
1615
  SpiderlyNumberComponent,
1509
1616
  SpiderlyDropdownComponent,
1510
1617
  SpiderlyEditorComponent,
1618
+ SpiderlyMarkdownComponent,
1511
1619
  SpiderlyColorPickerComponent,
1512
1620
  SpiderlyFileComponent], exports: [SpiderlyTextboxComponent,
1513
1621
  SpiderlyTextareaComponent,
@@ -1522,6 +1630,7 @@ class SpiderlyControlsModule {
1522
1630
  SpiderlyNumberComponent,
1523
1631
  SpiderlyDropdownComponent,
1524
1632
  SpiderlyEditorComponent,
1633
+ SpiderlyMarkdownComponent,
1525
1634
  SpiderlyColorPickerComponent,
1526
1635
  SpiderlyFileComponent] }); }
1527
1636
  static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyControlsModule, imports: [SpiderlyTextboxComponent,
@@ -1537,6 +1646,7 @@ class SpiderlyControlsModule {
1537
1646
  SpiderlyNumberComponent,
1538
1647
  SpiderlyDropdownComponent,
1539
1648
  SpiderlyEditorComponent,
1649
+ SpiderlyMarkdownComponent,
1540
1650
  SpiderlyColorPickerComponent,
1541
1651
  SpiderlyFileComponent] }); }
1542
1652
  }
@@ -1557,6 +1667,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1557
1667
  SpiderlyNumberComponent,
1558
1668
  SpiderlyDropdownComponent,
1559
1669
  SpiderlyEditorComponent,
1670
+ SpiderlyMarkdownComponent,
1560
1671
  SpiderlyColorPickerComponent,
1561
1672
  SpiderlyFileComponent,
1562
1673
  ],
@@ -1574,6 +1685,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1574
1685
  SpiderlyNumberComponent,
1575
1686
  SpiderlyDropdownComponent,
1576
1687
  SpiderlyEditorComponent,
1688
+ SpiderlyMarkdownComponent,
1577
1689
  SpiderlyColorPickerComponent,
1578
1690
  SpiderlyFileComponent,
1579
1691
  ],
@@ -1582,95 +1694,494 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
1582
1694
  }]
1583
1695
  }] });
1584
1696
 
1585
- class UserBase extends BaseEntity {
1586
- static { this.typeName = 'UserBase'; }
1587
- constructor({ id, email, } = {}) {
1697
+ class InitCompanyAuthDialogDetails extends BaseEntity {
1698
+ constructor({ image, companyName, } = {}) {
1588
1699
  super();
1589
- this.id = id;
1590
- this.email = email;
1700
+ this.image = image;
1701
+ this.companyName = companyName;
1591
1702
  }
1592
- static { this.schema = {
1593
- id: {
1594
- type: 'number',
1595
- },
1596
- email: {
1597
- type: 'string',
1598
- },
1599
- }; }
1703
+ static { this.typeName = 'InitCompanyAuthDialogDetails'; }
1600
1704
  }
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;
1705
+
1706
+ class ConfigServiceBase {
1707
+ constructor() {
1708
+ this.production = false;
1709
+ this.frontendUrl = 'http://localhost:4200';
1710
+ this.companyName = 'Company Name';
1711
+ this.primaryColor = '#111b2c';
1712
+ /* URLs */
1713
+ this.loginSlug = 'login';
1714
+ /* Local storage */
1715
+ this.accessTokenKey = 'access_token';
1716
+ this.refreshTokenKey = 'refresh_token';
1717
+ this.browserIdKey = 'browser_id';
1718
+ this.httpOptions = {};
1719
+ this.httpSkipSpinnerOptions = {
1720
+ headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
1721
+ params: new HttpParams().set('X-Skip-Spinner', 'true'),
1722
+ };
1723
+ this.logoPath = 'assets/images/logo/logo.svg';
1724
+ /* Pagination */
1725
+ this.defaultPageSize = 10;
1609
1726
  }
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
- }; }
1727
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ConfigServiceBase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1728
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ConfigServiceBase, providedIn: 'root' }); }
1624
1729
  }
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;
1730
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ConfigServiceBase, decorators: [{
1731
+ type: Injectable,
1732
+ args: [{
1733
+ providedIn: 'root',
1734
+ }]
1735
+ }], ctorParameters: () => [] });
1736
+
1737
+ class ApiSecurityService {
1738
+ constructor(http, config) {
1739
+ this.http = http;
1740
+ this.config = config;
1741
+ //#region Authentication
1742
+ this.login = (request) => {
1743
+ return this.http.post(`${this.config.apiUrl}/Security/Login`, request, this.config.httpOptions);
1744
+ };
1745
+ this.sendLoginVerificationEmail = (loginDTO) => {
1746
+ return this.http.post(`${this.config.apiUrl}/Security/SendLoginVerificationEmail`, loginDTO, this.config.httpOptions);
1747
+ };
1748
+ this.loginWithCookies = (request) => {
1749
+ return this.http.post(`${this.config.apiUrl}/Security/LoginWithCookies`, request, this.config.httpOptions);
1750
+ };
1751
+ this.getExternalProviders = () => {
1752
+ return this.http.get(`${this.config.apiUrl}/Security/GetExternalProviders`, this.config.httpSkipSpinnerOptions);
1753
+ };
1754
+ this.logout = (browserId) => {
1755
+ return this.http.get(`${this.config.apiUrl}/Security/Logout?browserId=${browserId}`);
1756
+ };
1757
+ this.logoutWithCookies = (browserId) => {
1758
+ return this.http.get(`${this.config.apiUrl}/Security/LogoutWithCookies?browserId=${browserId}`);
1759
+ };
1760
+ this.refreshTokenWithHeaders = (request) => {
1761
+ return this.http.post(`${this.config.apiUrl}/Security/RefreshTokenWithHeaders`, request, this.config.httpOptions);
1762
+ };
1763
+ this.refreshTokenWithCookies = (browserId) => {
1764
+ // POST, not GET: refresh rotates the single-use token (a state mutation), and a cacheable GET let
1765
+ // browsers replay a stale logged-in response on back/forward navigation (phantom dashboard after logout).
1766
+ return this.http.post(`${this.config.apiUrl}/Security/RefreshTokenWithCookies?browserId=${browserId}`, null);
1767
+ };
1768
+ //#endregion
1769
+ //#region User
1770
+ this.getCurrentUserBase = () => {
1771
+ return this.http.get(`${this.config.apiUrl}/Security/GetCurrentUserBase`, this.config.httpSkipSpinnerOptions);
1772
+ };
1773
+ this.getCurrentUserPermissionCodes = () => {
1774
+ return this.http.get(`${this.config.apiUrl}/Security/GetCurrentUserPermissionCodes`, this.config.httpSkipSpinnerOptions);
1775
+ };
1632
1776
  }
1633
- static { this.schema = {
1634
- verificationCode: {
1635
- type: 'string',
1636
- },
1637
- browserId: {
1638
- type: 'string',
1639
- },
1640
- email: {
1641
- type: 'string',
1642
- },
1643
- }; }
1777
+ 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 }); }
1778
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ApiSecurityService, providedIn: 'root' }); }
1644
1779
  }
1645
- class ExternalProvider extends BaseEntity {
1646
- static { this.typeName = 'ExternalProvider'; }
1647
- constructor({ idToken, browserId, } = {}) {
1648
- super();
1649
- this.idToken = idToken;
1650
- this.browserId = browserId;
1780
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ApiSecurityService, decorators: [{
1781
+ type: Injectable,
1782
+ args: [{
1783
+ providedIn: 'root',
1784
+ }]
1785
+ }], ctorParameters: () => [{ type: i1$2.HttpClient }, { type: ConfigServiceBase }] });
1786
+
1787
+ /**
1788
+ * Cookie-based session auth. The access/refresh JWTs live in HttpOnly cookies set by the backend
1789
+ * (so JS never holds them — XSS can't exfiltrate them); requests are authenticated via
1790
+ * `withCredentials` (see jwtInterceptor). The readable result only carries userId/email/expiry.
1791
+ */
1792
+ class AuthServiceBase {
1793
+ constructor(router, http, apiService, config, platformId) {
1794
+ this.router = router;
1795
+ this.http = http;
1796
+ this.apiService = apiService;
1797
+ this.config = config;
1798
+ this.platformId = platformId;
1799
+ this.apiUrl = this.config.apiUrl;
1800
+ // External-login error code captured from the bootstrap URL (?externalAuthError=expired|failed) set by the
1801
+ // backend's OAuth callback on failure. Captured before routing can strip it, surfaced once by the login page.
1802
+ this.externalAuthErrorCode = null;
1803
+ this._currentUserPermissionCodes = new BehaviorSubject(undefined);
1804
+ // The subject seeds with `undefined` (not yet loaded) and emits `null` on logout. Consumers only ever care
1805
+ // about a real code list, so filter both out here — subscribers get a clean `string[]`, and `firstValueFrom`
1806
+ // waits for the first loaded value instead of grabbing the `undefined` seed in a load race.
1807
+ this.currentUserPermissionCodes$ = this._currentUserPermissionCodes
1808
+ .asObservable()
1809
+ .pipe(filter((codes) => codes != null));
1810
+ this._user = new BehaviorSubject(undefined);
1811
+ this.user$ = this._user.asObservable();
1812
+ // Cross-tab sync. We store only marker values here (never tokens — those are HttpOnly cookies).
1813
+ this.storageEventListener = (event) => {
1814
+ if (event.storageArea === localStorage) {
1815
+ if (event.key === 'logout-event') {
1816
+ this.stopTokenTimer();
1817
+ this._user.next(null);
1818
+ this._currentUserPermissionCodes.next(null);
1819
+ }
1820
+ if (event.key === 'login-event') {
1821
+ this.refreshToken().subscribe();
1822
+ }
1823
+ }
1824
+ };
1825
+ this.onAfterLogout = () => {
1826
+ this._currentUserPermissionCodes.next(null);
1827
+ this.router.navigate([this.config.loginSlug]);
1828
+ };
1829
+ this.onAfterRefreshToken = () => {
1830
+ this.setCurrentUserPermissionCodes().subscribe(); // after the session is re-established
1831
+ };
1832
+ this.initCompanyAuthDialogDetails = () => {
1833
+ return of(new InitCompanyAuthDialogDetails({
1834
+ image: this.config.logoPath,
1835
+ companyName: this.config.companyName,
1836
+ }));
1837
+ };
1838
+ this.onAfterNgOnDestroy = () => { };
1839
+ if (isPlatformBrowser(platformId)) {
1840
+ window.addEventListener('storage', this.storageEventListener);
1841
+ }
1651
1842
  }
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;
1843
+ sendLoginVerificationEmail(body) {
1844
+ body.browserId = this.getBrowserId();
1845
+ return this.apiService.sendLoginVerificationEmail(body);
1667
1846
  }
1668
- static { this.schema = {
1669
- roleId: {
1670
- type: 'number',
1671
- },
1672
- userId: {
1673
- type: 'number',
1847
+ login(body) {
1848
+ body.browserId = this.getBrowserId();
1849
+ return this.apiService.loginWithCookies(body).pipe(map$1((result) => {
1850
+ this.handleAuthResult(result);
1851
+ return result;
1852
+ }));
1853
+ }
1854
+ // Establishes the in-memory session from a cookie auth result (login or refresh). No tokens are stored
1855
+ // in JS — only the user identity + the access-token expiry the backend reports (to schedule refresh).
1856
+ handleAuthResult(result) {
1857
+ this._user.next({
1858
+ id: result.userId,
1859
+ email: result.email,
1860
+ });
1861
+ this.accessTokenExpiresAt = result.accessTokenExpiresAt
1862
+ ? new Date(result.accessTokenExpiresAt)
1863
+ : undefined;
1864
+ localStorage.setItem('login-event', 'login' + Math.random());
1865
+ this.startTokenTimer();
1866
+ this.setCurrentUserPermissionCodes().subscribe();
1867
+ }
1868
+ logout() {
1869
+ const browserId = this.getBrowserId();
1870
+ this.apiService
1871
+ .logoutWithCookies(browserId)
1872
+ .pipe(finalize(() => {
1873
+ this._user.next(null);
1874
+ localStorage.setItem('logout-event', 'logout' + Math.random());
1875
+ this.onAfterLogout();
1876
+ this.stopTokenTimer();
1877
+ }))
1878
+ .subscribe();
1879
+ }
1880
+ // Clears in-memory session state without calling the backend — used when a request comes back 401
1881
+ // (the backend has already cleared the auth cookies in that case).
1882
+ clearSession() {
1883
+ this.stopTokenTimer();
1884
+ this._user.next(null);
1885
+ this._currentUserPermissionCodes.next(null);
1886
+ localStorage.setItem('logout-event', 'logout' + Math.random());
1887
+ }
1888
+ // Called on app init and by the proactive timer. The refresh token is an HttpOnly cookie; a 401 ("no valid
1889
+ // session" — not logged in / expired) propagates from the interceptor and is handled by catchError below,
1890
+ // resolving the session to anonymous (null). map runs only for a real result, so _user is never partial.
1891
+ refreshToken() {
1892
+ const browserId = this.getBrowserId();
1893
+ return this.apiService.refreshTokenWithCookies(browserId).pipe(map$1((result) => {
1894
+ if (result) {
1895
+ // A re-established session makes any pending external-login error moot — drop it so it can't
1896
+ // surface as a stale toast on a later /login visit (e.g. after a subsequent logout).
1897
+ this.externalAuthErrorCode = null;
1898
+ this._user.next({ id: result.userId, email: result.email });
1899
+ this.accessTokenExpiresAt = result.accessTokenExpiresAt
1900
+ ? new Date(result.accessTokenExpiresAt)
1901
+ : undefined;
1902
+ this.startTokenTimer();
1903
+ this.onAfterRefreshToken();
1904
+ }
1905
+ return result;
1906
+ }), catchError(() => {
1907
+ this._user.next(null);
1908
+ return of(null);
1909
+ }));
1910
+ }
1911
+ // Reads ?externalAuthError= from the bootstrap URL (set by the backend OAuth callback on failure) and
1912
+ // strips it so a manual refresh won't re-trigger the message. Called from the app initializer, before the
1913
+ // router runs — otherwise an unauthenticated landing on "/" redirects to /login and drops the param.
1914
+ captureExternalAuthError() {
1915
+ if (isPlatformBrowser(this.platformId) === false) {
1916
+ return;
1917
+ }
1918
+ const params = new URLSearchParams(window.location.search);
1919
+ const code = params.get('externalAuthError');
1920
+ if (!code) {
1921
+ return;
1922
+ }
1923
+ this.externalAuthErrorCode = code;
1924
+ params.delete('externalAuthError');
1925
+ const query = params.toString();
1926
+ history.replaceState(history.state, '', window.location.pathname + (query ? `?${query}` : '') + window.location.hash);
1927
+ }
1928
+ getBrowserId() {
1929
+ let browserId = localStorage.getItem(this.config.browserIdKey); // not a token — a stable per-browser id
1930
+ if (!browserId) {
1931
+ browserId = crypto.randomUUID();
1932
+ localStorage.setItem(this.config.browserIdKey, browserId);
1933
+ }
1934
+ return browserId;
1935
+ }
1936
+ getTokenRemainingTime() {
1937
+ if (!this.accessTokenExpiresAt) {
1938
+ return 0;
1939
+ }
1940
+ return this.accessTokenExpiresAt.getTime() - Date.now();
1941
+ }
1942
+ startTokenTimer() {
1943
+ const timeout = this.getTokenRemainingTime();
1944
+ if (timeout <= 0) {
1945
+ return;
1946
+ }
1947
+ this.stopTokenTimer();
1948
+ this.timer = of(true)
1949
+ .pipe(delay(timeout), tap({
1950
+ next: () => this.refreshToken().subscribe(),
1951
+ }))
1952
+ .subscribe();
1953
+ }
1954
+ stopTokenTimer() {
1955
+ this.timer?.unsubscribe();
1956
+ }
1957
+ navigateToDashboard() {
1958
+ this.router.navigate(['/']);
1959
+ }
1960
+ setCurrentUserPermissionCodes() {
1961
+ return this.apiService.getCurrentUserPermissionCodes().pipe(map$1((permissionCodes) => {
1962
+ this._currentUserPermissionCodes.next(permissionCodes);
1963
+ return permissionCodes;
1964
+ }));
1965
+ }
1966
+ ngOnDestroy() {
1967
+ if (isPlatformBrowser(this.platformId)) {
1968
+ window.removeEventListener('storage', this.storageEventListener);
1969
+ }
1970
+ this.onAfterNgOnDestroy();
1971
+ }
1972
+ 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 }); }
1973
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthServiceBase, providedIn: 'root' }); }
1974
+ }
1975
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthServiceBase, decorators: [{
1976
+ type: Injectable,
1977
+ args: [{
1978
+ providedIn: 'root',
1979
+ }]
1980
+ }], ctorParameters: () => [{ type: i3$2.Router }, { type: i1$2.HttpClient }, { type: ApiSecurityService }, { type: ConfigServiceBase }, { type: Object, decorators: [{
1981
+ type: Inject,
1982
+ args: [PLATFORM_ID]
1983
+ }] }] });
1984
+
1985
+ class AuthCardComponent {
1986
+ constructor(authService) {
1987
+ this.authService = authService;
1988
+ this.companyDetailsSubscription = null;
1989
+ }
1990
+ ngOnInit() {
1991
+ this.companyDetailsSubscription = this.authService
1992
+ .initCompanyAuthDialogDetails()
1993
+ .subscribe((details) => {
1994
+ if (details != null) {
1995
+ this.image = details.image;
1996
+ this.companyName = details.companyName;
1997
+ }
1998
+ });
1999
+ }
2000
+ ngOnDestroy() {
2001
+ this.companyDetailsSubscription?.unsubscribe();
2002
+ }
2003
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthCardComponent, deps: [{ token: AuthServiceBase }], target: i0.ɵɵFactoryTarget.Component }); }
2004
+ 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"] }] }); }
2005
+ }
2006
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthCardComponent, decorators: [{
2007
+ type: Component,
2008
+ 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" }]
2009
+ }], ctorParameters: () => [{ type: AuthServiceBase }] });
2010
+
2011
+ 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>`;
2012
+ /**
2013
+ * Built-in default icons for external auth providers, keyed by provider code.
2014
+ * Values are inline data URIs — no network request, CSP entry, or asset wiring,
2015
+ * and they render offline. Consumers override per code via the `providerIcons`
2016
+ * input on ExternalLoginComponent / SpiderlyLoginComponent.
2017
+ */
2018
+ const DEFAULT_EXTERNAL_PROVIDER_ICONS = {
2019
+ google: `data:image/svg+xml,${encodeURIComponent(GOOGLE_G_SVG)}`,
2020
+ };
2021
+
2022
+ class ExternalLoginComponent {
2023
+ constructor(config, authService, apiService) {
2024
+ this.config = config;
2025
+ this.authService = authService;
2026
+ this.apiService = apiService;
2027
+ /** Per-code icon overrides; unset codes fall back to DEFAULT_EXTERNAL_PROVIDER_ICONS. */
2028
+ this.providerIcons = {};
2029
+ // Config-driven: populated from Security/GetExternalProviders (backend is the single source of truth for which providers are enabled).
2030
+ this.externalProviders = [];
2031
+ }
2032
+ ngOnInit() {
2033
+ this.apiService.getExternalProviders().subscribe({
2034
+ next: (providers) => {
2035
+ this.externalProviders = providers ?? [];
2036
+ },
2037
+ // The global unauthorized interceptor already surfaces the HTTP error to the user; here we just
2038
+ // leave the provider buttons hidden instead of letting the error reach the global error handler.
2039
+ error: () => {
2040
+ this.externalProviders = [];
2041
+ },
2042
+ });
2043
+ }
2044
+ iconFor(code) {
2045
+ return this.providerIcons[code] ?? DEFAULT_EXTERNAL_PROVIDER_ICONS[code];
2046
+ }
2047
+ loginWithExternalProvider(code) {
2048
+ // Server-side flow (B2): hand off to the backend challenge endpoint. The backend runs the OAuth
2049
+ // dance, sets the session cookies, and redirects back to returnUrl.
2050
+ const returnUrl = this.config.frontendUrl;
2051
+ const browserId = this.authService.getBrowserId();
2052
+ window.location.href =
2053
+ `${this.config.apiUrl}/Security/ExternalLoginChallenge` +
2054
+ `?provider=${encodeURIComponent(code)}` +
2055
+ `&returnUrl=${encodeURIComponent(returnUrl)}` +
2056
+ `&browserId=${encodeURIComponent(browserId)}`;
2057
+ }
2058
+ 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 }); }
2059
+ 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"] }] }); }
2060
+ }
2061
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: ExternalLoginComponent, decorators: [{
2062
+ type: Component,
2063
+ 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" }]
2064
+ }], ctorParameters: () => [{ type: ConfigServiceBase }, { type: AuthServiceBase }, { type: ApiSecurityService }], propDecorators: { providerIcons: [{
2065
+ type: Input
2066
+ }] } });
2067
+
2068
+ class UserBase extends BaseEntity {
2069
+ static { this.typeName = 'UserBase'; }
2070
+ constructor({ id, email, } = {}) {
2071
+ super();
2072
+ this.id = id;
2073
+ this.email = email;
2074
+ }
2075
+ static { this.schema = {
2076
+ id: {
2077
+ type: 'number',
2078
+ },
2079
+ email: {
2080
+ type: 'string',
2081
+ },
2082
+ }; }
2083
+ }
2084
+ class AuthResult extends BaseEntity {
2085
+ static { this.typeName = 'AuthResult'; }
2086
+ constructor({ userId, email, accessToken, refreshToken, } = {}) {
2087
+ super();
2088
+ this.userId = userId;
2089
+ this.email = email;
2090
+ this.accessToken = accessToken;
2091
+ this.refreshToken = refreshToken;
2092
+ }
2093
+ static { this.schema = {
2094
+ userId: {
2095
+ type: 'number',
2096
+ },
2097
+ email: {
2098
+ type: 'string',
2099
+ },
2100
+ accessToken: {
2101
+ type: 'string',
2102
+ },
2103
+ refreshToken: {
2104
+ type: 'string',
2105
+ },
2106
+ }; }
2107
+ }
2108
+ class VerificationTokenRequest extends BaseEntity {
2109
+ static { this.typeName = 'VerificationTokenRequest'; }
2110
+ constructor({ verificationCode, browserId, email, } = {}) {
2111
+ super();
2112
+ this.verificationCode = verificationCode;
2113
+ this.browserId = browserId;
2114
+ this.email = email;
2115
+ }
2116
+ static { this.schema = {
2117
+ verificationCode: {
2118
+ type: 'string',
2119
+ },
2120
+ browserId: {
2121
+ type: 'string',
2122
+ },
2123
+ email: {
2124
+ type: 'string',
2125
+ },
2126
+ }; }
2127
+ }
2128
+ class ExternalProvider extends BaseEntity {
2129
+ static { this.typeName = 'ExternalProvider'; }
2130
+ constructor({ provider, idToken, browserId, } = {}) {
2131
+ super();
2132
+ this.provider = provider;
2133
+ this.idToken = idToken;
2134
+ this.browserId = browserId;
2135
+ }
2136
+ static { this.schema = {
2137
+ provider: {
2138
+ type: 'string',
2139
+ },
2140
+ idToken: {
2141
+ type: 'string',
2142
+ },
2143
+ browserId: {
2144
+ type: 'string',
2145
+ },
2146
+ }; }
2147
+ }
2148
+ class ExternalProviderPublic extends BaseEntity {
2149
+ static { this.typeName = 'ExternalProviderPublic'; }
2150
+ constructor({ code, authority, clientId, label, } = {}) {
2151
+ super();
2152
+ this.code = code;
2153
+ this.authority = authority;
2154
+ this.clientId = clientId;
2155
+ this.label = label;
2156
+ }
2157
+ static { this.schema = {
2158
+ code: {
2159
+ type: 'string',
2160
+ },
2161
+ authority: {
2162
+ type: 'string',
2163
+ },
2164
+ clientId: {
2165
+ type: 'string',
2166
+ },
2167
+ label: {
2168
+ type: 'string',
2169
+ },
2170
+ }; }
2171
+ }
2172
+ class UserRole extends BaseEntity {
2173
+ static { this.typeName = 'UserRole'; }
2174
+ constructor({ roleId, userId, } = {}) {
2175
+ super();
2176
+ this.roleId = roleId;
2177
+ this.userId = userId;
2178
+ }
2179
+ static { this.schema = {
2180
+ roleId: {
2181
+ type: 'number',
2182
+ },
2183
+ userId: {
2184
+ type: 'number',
1674
2185
  },
1675
2186
  }; }
1676
2187
  }
@@ -2049,501 +2560,142 @@ class BaseFormComponent {
2049
2560
  if (!this.saveBodyClass)
2050
2561
  throw new SpiderlyError('You did not initialize saveBodyClass');
2051
2562
  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
- }
2563
+ throw new SpiderlyError('You did not initialize mainUIFormClass');
2564
+ let saveBody = this.parentFormGroup.getRawValue();
2565
+ this.onBeforeSave(saveBody);
2566
+ const isValid = this.baseFormService.isControlValid(this.parentFormGroup);
2567
+ if (isValid) {
2568
+ this.parentFormGroup
2569
+ .saveObservableMethod(saveBody)
2570
+ .subscribe((res) => {
2571
+ this.messageService.successMessage(this.successfulSaveToastDescription);
2572
+ if (rerouteToParentSlugAfterSave) {
2573
+ this.rerouteToSavedObject(undefined);
2574
+ }
2575
+ else {
2576
+ saveBody = this.baseFormService.mapMainUIFormToSaveBody(this.mainUIFormClass, res);
2577
+ this.baseFormService.initFormGroup(this.parentFormGroup, this.saveBodyClass, saveBody);
2578
+ const saveBodyMainDTOKey = this.baseFormService.getSaveBodyMainDTOKey(this.saveBodyClass);
2579
+ const savedObjectId = saveBody[saveBodyMainDTOKey]?.id;
2580
+ this.rerouteToSavedObject(savedObjectId); // You always need to have id, because of id == 0 and version change
2581
+ }
2582
+ this.onAfterSave();
2583
+ });
2584
+ this.onAfterSaveRequest();
2585
+ }
2586
+ else {
2587
+ this.baseFormService.showInvalidFieldsMessage();
2335
2588
  }
2336
2589
  };
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();
2590
+ /**
2591
+ * Hook that runs **before** form validation and the save request.
2592
+ * Use this to modify the save body or perform any pre-save logic (e.g., transforming data, setting computed fields).
2593
+ *
2594
+ * @param saveBody - The current save body built from the form's raw value. Mutate it directly to change what gets sent to the server.
2595
+ *
2596
+ * @example
2597
+ * ```ts
2598
+ * onBeforeSave = (saveBody?: ProductSaveBody) => {
2599
+ * saveBody.productDTO.fullName = saveBody.productDTO.firstName + ' ' + saveBody.productDTO.lastName;
2600
+ * };
2601
+ * ```
2602
+ */
2603
+ this.onBeforeSave = (saveBody) => { };
2604
+ /**
2605
+ * Hook that runs **after** a successful save response is received.
2606
+ * Use this for post-save side effects (e.g., refreshing related data, showing additional notifications).
2607
+ *
2608
+ * @example
2609
+ * ```ts
2610
+ * onAfterSave = () => {
2611
+ * this.loadRelatedProducts();
2612
+ * };
2613
+ * ```
2614
+ */
2615
+ this.onAfterSave = () => { };
2616
+ /**
2617
+ * Hook that runs immediately **after** the save HTTP request is sent, but **before** the response arrives.
2618
+ * Use this for side effects that should happen as soon as the request is dispatched (e.g., disabling UI elements, starting a loading indicator).
2619
+ *
2620
+ * @example
2621
+ * ```ts
2622
+ * onAfterSaveRequest = () => {
2623
+ * this.isSaving = true;
2624
+ * };
2625
+ * ```
2626
+ */
2627
+ this.onAfterSaveRequest = () => { };
2409
2628
  }
2410
- refreshToken() {
2411
- const refreshToken = localStorage.getItem(this.config.refreshTokenKey);
2412
- if (!refreshToken) {
2413
- this.clearLocalStorage();
2414
- return of(null);
2629
+ ngOnInit() { }
2630
+ /**
2631
+ * Handles navigation after a successful save.
2632
+ * Override this to customize the post-save navigation behavior.
2633
+ * By default, navigates to the parent URL when `rerouteId` is not provided, or to the saved object's URL otherwise.
2634
+ *
2635
+ * @param rerouteId - The ID of the saved object, used to build the target URL. When not provided, navigates to the parent URL.
2636
+ *
2637
+ * @example
2638
+ * ```ts
2639
+ * // Override to navigate to a custom route after save
2640
+ * override rerouteToSavedObject(rerouteId: number | string): void {
2641
+ * this.router.navigateByUrl(`/products/${rerouteId}/details`);
2642
+ * }
2643
+ * ```
2644
+ */
2645
+ rerouteToSavedObject(rerouteId) {
2646
+ if (rerouteId == null) {
2647
+ const currentUrl = this.router.url;
2648
+ const parentUrl = getParentUrl(currentUrl);
2649
+ this.router.navigateByUrl(parentUrl);
2650
+ return;
2415
2651
  }
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());
2652
+ const segments = this.router.url.split('/');
2653
+ segments[segments.length - 1] = rerouteId.toString();
2654
+ const newUrl = segments.join('/');
2655
+ this.router.navigateByUrl(newUrl);
2442
2656
  }
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);
2657
+ //#endregion
2658
+ //#region Model List
2659
+ getFormArrayControlByIndex(formControlName, formArray, index, filter) {
2660
+ // if(formArray.controlNamesFromHtml.findIndex(x => x === formControlName) === -1)
2661
+ // formArray.controlNamesFromHtml.push(formControlName);
2662
+ let filteredFormGroups;
2663
+ if (filter) {
2664
+ filteredFormGroups = filter(formArray.controls);
2448
2665
  }
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;
2666
+ else {
2667
+ return formArray.controls[index].controls[formControlName];
2459
2668
  }
2460
- const jwtToken = JSON.parse(atob(accessToken.split('.')[1]));
2461
- const expires = new Date(jwtToken.exp * 1000);
2462
- return expires.getTime() - Date.now();
2669
+ return filteredFormGroups[index]?.controls[formControlName]; // FT: Don't change this. It's always possible that change detection occurs before something.
2463
2670
  }
2464
- getAccessToken() {
2465
- if (isPlatformBrowser(this.platformId)) {
2466
- return localStorage.getItem(this.config.accessTokenKey);
2671
+ getFormArrayControls(formControlName, formArray, filter) {
2672
+ // if(formArray.controlNamesFromHtml.findIndex(x => x === formControlName) === -1)
2673
+ // formArray.controlNamesFromHtml.push(formControlName);
2674
+ let filteredFormGroups;
2675
+ if (filter) {
2676
+ filteredFormGroups = filter(formArray.controls);
2467
2677
  }
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);
2678
+ else {
2679
+ return formArray.controls.map((x) => x.controls[formControlName]);
2493
2680
  }
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();
2681
+ return filteredFormGroups.map((x) => x.controls[formControlName]);
2518
2682
  }
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);
2683
+ removeFormControlsFromTheFormArray(formArray, indexes) {
2684
+ // Sort indexes in descending order to avoid index shifts when removing controls
2685
+ const sortedIndexes = indexes.sort((a, b) => b - a);
2686
+ sortedIndexes.forEach((index) => {
2687
+ if (index >= 0 && index < formArray.length) {
2688
+ formArray.removeAt(index);
2527
2689
  }
2528
2690
  });
2529
2691
  }
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"] }] }); }
2692
+ 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 }); }
2693
+ 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
2694
  }
2541
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: AuthComponent, decorators: [{
2695
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: BaseFormComponent, decorators: [{
2542
2696
  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
- }] } });
2697
+ args: [{ selector: 'base-form', template: '', standalone: false }]
2698
+ }], 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
2699
 
2548
2700
  class PanelBodyComponent {
2549
2701
  constructor() {
@@ -2574,6 +2726,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
2574
2726
  class PanelHeaderComponent {
2575
2727
  constructor(translocoService) {
2576
2728
  this.translocoService = translocoService;
2729
+ /** Whether the header icon is shown. Defaults to `true`. */
2577
2730
  this.showIcon = true;
2578
2731
  }
2579
2732
  ngOnInit() {
@@ -2624,8 +2777,11 @@ class SpiderlyPanelComponent {
2624
2777
  this.toggleable = false;
2625
2778
  this.toggler = 'icon';
2626
2779
  this.collapsed = false;
2780
+ /** Whether the CRUD context-menu icon is shown. Defaults to `true`. */
2627
2781
  this.showCrudMenu = true;
2782
+ /** Whether a remove/delete icon is shown. Defaults to `false`. */
2628
2783
  this.showRemoveIcon = false;
2784
+ /** Whether the panel header is rendered. Defaults to `true`. */
2629
2785
  this.showPanelHeader = true;
2630
2786
  this.onMenuIconClick = new EventEmitter();
2631
2787
  this.onRemoveIconClick = new EventEmitter();
@@ -2793,8 +2949,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
2793
2949
  type: Input
2794
2950
  }] } });
2795
2951
 
2796
- class LoginComponent extends BaseFormComponent {
2797
- constructor(differs, http, messageService, changeDetectorRef, router, route, translocoService, baseFormService, authService, config) {
2952
+ class SpiderlyLoginComponent extends BaseFormComponent {
2953
+ constructor(differs, http, messageService, changeDetectorRef, router, route, translocoService, baseFormService, authService) {
2798
2954
  super(differs, http, messageService, changeDetectorRef, router, route, translocoService, baseFormService);
2799
2955
  this.differs = differs;
2800
2956
  this.http = http;
@@ -2805,21 +2961,31 @@ class LoginComponent extends BaseFormComponent {
2805
2961
  this.translocoService = translocoService;
2806
2962
  this.baseFormService = baseFormService;
2807
2963
  this.authService = authService;
2808
- this.config = config;
2809
2964
  this.loginFormGroup = new SpiderlyFormGroup({});
2810
2965
  this.showEmailSentDialog = false;
2966
+ /** Per-code provider icon overrides, forwarded to <spiderly-external-login>. */
2967
+ this.providerIcons = {};
2811
2968
  }
2812
2969
  ngOnInit() {
2813
2970
  this.initLoginFormGroup(new Login({}));
2971
+ this.showExternalAuthErrorIfPresent();
2972
+ }
2973
+ // Surface a friendly message when the server-side external login bounced back with an error (captured from
2974
+ // the URL at bootstrap by AuthServiceBase). "expired" = the user lingered on the provider's account picker.
2975
+ showExternalAuthErrorIfPresent() {
2976
+ const code = this.authService.externalAuthErrorCode;
2977
+ if (!code) {
2978
+ return;
2979
+ }
2980
+ this.authService.externalAuthErrorCode = null; // show once
2981
+ const messageKey = code === 'expired' ? 'ExternalLoginExpiredDetails' : 'ExternalLoginFailedDetails';
2982
+ this.messageService.warningMessage(this.translocoService.translate(messageKey));
2814
2983
  }
2815
2984
  initLoginFormGroup(model) {
2816
2985
  this.baseFormService.initFormGroup(this.loginFormGroup, Login, model, [
2817
2986
  'email',
2818
2987
  ]);
2819
2988
  }
2820
- companyNameChange(companyName) {
2821
- this.companyName = companyName;
2822
- }
2823
2989
  sendLoginVerificationEmail() {
2824
2990
  let isFormGroupValid = this.baseFormService.isControlValid(this.loginFormGroup);
2825
2991
  if (isFormGroupValid == false) {
@@ -2835,20 +3001,23 @@ class LoginComponent extends BaseFormComponent {
2835
3001
  this.showEmailSentDialog = true;
2836
3002
  });
2837
3003
  }
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"] }] }); }
3004
+ 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 }); }
3005
+ 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 <!--\n General projection slot for consumer-supplied login extras (a bot-challenge widget, a notice,\n an extra field, \u2026). Rendered in BOTH the email-entry and code-verification states \u2014 outside the\n showEmailSentDialog toggle \u2014 so projected content survives the email \u2192 resend flow (e.g. a\n single-use challenge widget that must stay mounted to issue a fresh token on resend).\n Project with the `loginExtra` attribute: <spiderly-login><my-widget loginExtra/></spiderly-login>.\n -->\n <ng-content select=\"[loginExtra]\"></ng-content>\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
3006
  }
2841
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: LoginComponent, decorators: [{
3007
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyLoginComponent, decorators: [{
2842
3008
  type: Component,
2843
- args: [{ selector: 'app-login', imports: [
3009
+ args: [{ selector: 'spiderly-login', imports: [
2844
3010
  CommonModule,
2845
3011
  ReactiveFormsModule,
2846
- AuthComponent,
3012
+ AuthCardComponent,
3013
+ ExternalLoginComponent,
2847
3014
  SpiderlyControlsModule,
2848
3015
  LoginVerificationComponent,
2849
3016
  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 }] });
3017
+ ], 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 <!--\n General projection slot for consumer-supplied login extras (a bot-challenge widget, a notice,\n an extra field, \u2026). Rendered in BOTH the email-entry and code-verification states \u2014 outside the\n showEmailSentDialog toggle \u2014 so projected content survives the email \u2192 resend flow (e.g. a\n single-use challenge widget that must stay mounted to issue a fresh token on resend).\n Project with the `loginExtra` attribute: <spiderly-login><my-widget loginExtra/></spiderly-login>.\n -->\n <ng-content select=\"[loginExtra]\"></ng-content>\n }\n</ng-container>\n" }]
3018
+ }], 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: [{
3019
+ type: Input
3020
+ }] } });
2852
3021
 
2853
3022
  class CardSkeletonComponent {
2854
3023
  constructor() {
@@ -2893,6 +3062,7 @@ class IndexCardComponent {
2893
3062
  constructor(formBuilder) {
2894
3063
  this.formBuilder = formBuilder;
2895
3064
  this.header = '';
3065
+ /** Whether the CRUD context-menu icon is shown. Defaults to `true`. */
2896
3066
  this.showCrudMenu = true;
2897
3067
  this.onMenuIconClick = new EventEmitter();
2898
3068
  this.onRemoveIconClick = new EventEmitter();
@@ -2934,6 +3104,7 @@ class InfoCardComponent {
2934
3104
  constructor(formBuilder) {
2935
3105
  this.formBuilder = formBuilder;
2936
3106
  this.header = '';
3107
+ /** Whether the small icon variant is shown. Defaults to `true`. */
2937
3108
  this.showSmallIcon = true;
2938
3109
  this.icon = 'pi pi-info-circle';
2939
3110
  this.textColor = '';
@@ -2976,22 +3147,16 @@ class LayoutServiceBase {
2976
3147
  inputStyle: 'outlined',
2977
3148
  menuMode: 'static',
2978
3149
  colorScheme: 'light',
2979
- theme: 'lara-light-indigo',
2980
- scale: 14,
2981
- color: `var(--p-primary-color)`,
2982
3150
  };
2983
3151
  this.state = {
2984
3152
  staticMenuDesktopInactive: false,
2985
3153
  overlayMenuActive: false,
2986
3154
  profileSidebarVisible: false,
2987
3155
  profileDropdownSidebarVisible: false,
2988
- configSidebarVisible: false,
2989
3156
  staticMenuMobileActive: false,
2990
3157
  menuHoverActive: false,
2991
3158
  };
2992
- this.configUpdate = new Subject();
2993
3159
  this.overlayOpen = new Subject();
2994
- this.configUpdate$ = this.configUpdate.asObservable();
2995
3160
  this.overlayOpen$ = this.overlayOpen.asObservable();
2996
3161
  //#region Top Bar
2997
3162
  this.initTopBarData = () => {
@@ -3042,9 +3207,6 @@ class LayoutServiceBase {
3042
3207
  this.overlayOpen.next(null);
3043
3208
  }
3044
3209
  }
3045
- showConfigSidebar() {
3046
- this.state.configSidebarVisible = true;
3047
- }
3048
3210
  isOverlay() {
3049
3211
  return this.layoutConfig.menuMode === 'overlay';
3050
3212
  }
@@ -3054,9 +3216,6 @@ class LayoutServiceBase {
3054
3216
  isMobile() {
3055
3217
  return !this.isDesktop();
3056
3218
  }
3057
- onConfigUpdate() {
3058
- this.configUpdate.next(this.layoutConfig);
3059
- }
3060
3219
  //#endregion
3061
3220
  ngOnDestroy() {
3062
3221
  if (this.userSubscription) {
@@ -3297,6 +3456,7 @@ class ProfileAvatarComponent {
3297
3456
  this.config = config;
3298
3457
  this.isSideMenuLayout = true;
3299
3458
  this.routeOnLargeProfileAvatarClick = true;
3459
+ /** Whether the login button is shown when no user is signed in. Defaults to `true`. */
3300
3460
  this.showLoginButton = true;
3301
3461
  this.routeToLoginPage = true;
3302
3462
  this.loginButtonOutlined = false;
@@ -3719,7 +3879,9 @@ class SpiderlyDataTableComponent {
3719
3879
  this.locale = locale;
3720
3880
  this.destroy$ = new Subject();
3721
3881
  this.tableIcon = 'pi pi-list';
3722
- this.showPaginator = true; // Pass only when hasLazyLoad === false
3882
+ /** Whether the paginator is shown. Pass only when `hasLazyLoad === false`. Defaults to `true`. */
3883
+ this.showPaginator = true;
3884
+ /** Whether the table is wrapped in a card container. Defaults to `false`. */
3723
3885
  this.showCardWrapper = false;
3724
3886
  this.readonly = false;
3725
3887
  this.idField = 'id';
@@ -3737,8 +3899,11 @@ class SpiderlyDataTableComponent {
3737
3899
  this.onIsAllSelectedChange = new EventEmitter();
3738
3900
  this.matchModeDateOptions = [];
3739
3901
  this.matchModeNumberOptions = [];
3902
+ /** Whether the "Add" button is shown. Defaults to `true`. */
3740
3903
  this.showAddButton = true;
3904
+ /** Whether the "Export to Excel" button is shown. Defaults to `true`. */
3741
3905
  this.showExportToExcelButton = true;
3906
+ /** Whether the reload-table button is shown. Defaults to `false`. */
3742
3907
  this.showReloadTableButton = false;
3743
3908
  this.hasLazyLoad = true;
3744
3909
  /** 'session' persists across refresh only; 'local' persists indefinitely. */
@@ -4388,12 +4553,14 @@ class SpiderlyDataViewComponent {
4388
4553
  this.rows = 10;
4389
4554
  this.filters = [];
4390
4555
  this.onLazyLoad = new EventEmitter();
4556
+ /** Whether the data view is wrapped in a card container. Defaults to `true`. */
4391
4557
  this.showCardWrapper = true;
4392
4558
  /**
4393
4559
  * Whether to display additional data on the right side of the paginator.
4394
4560
  * Defaults to `false`.
4395
4561
  */
4396
4562
  this.showPaginatorRightData = false;
4563
+ /** Whether the total records count is shown. Defaults to `false`. */
4397
4564
  this.showTotalRecordsNumber = false;
4398
4565
  this.applyFiltersIcon = 'pi pi-filter';
4399
4566
  this.clearFiltersIcon = 'pi pi-filter-slash';
@@ -4762,18 +4929,13 @@ class AuthGuard {
4762
4929
  return this.checkAuth();
4763
4930
  }
4764
4931
  checkAuth() {
4765
- return this.authService.user$.pipe(map((user) => {
4932
+ return this.authService.user$.pipe(filter$1((user) => user !== undefined), // wait until the session is resolved (undefined = still loading)
4933
+ take(1), map((user) => {
4766
4934
  if (user) {
4767
4935
  return true;
4768
4936
  }
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
- }
4937
+ this.router.navigate([this.config.loginSlug]);
4938
+ return false;
4777
4939
  }));
4778
4940
  }
4779
4941
  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 +4956,13 @@ class NotAuthGuard {
4794
4956
  return this.checkAuth();
4795
4957
  }
4796
4958
  checkAuth() {
4797
- return this.authService.user$.pipe(map((user) => {
4959
+ return this.authService.user$.pipe(filter$1((user) => user !== undefined), // wait until the session is resolved (undefined = still loading)
4960
+ take(1), map((user) => {
4798
4961
  if (user) {
4799
4962
  this.authService.navigateToDashboard();
4800
4963
  return false;
4801
4964
  }
4802
- else {
4803
- return true;
4804
- }
4965
+ return true;
4805
4966
  }));
4806
4967
  }
4807
4968
  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 +5070,90 @@ const convertToDate = (object, parent, key) => {
4909
5070
  }
4910
5071
  };
4911
5072
 
5073
+ // Cookie-based auth: the session JWTs are HttpOnly cookies, so we just send credentials on API calls.
5074
+ // The browser attaches/refreshes the cookies; JS never holds the tokens (XSS-safe).
5075
+ //
5076
+ // CSRF: state-changing requests (POST/PUT/DELETE/PATCH) authenticated via cookie must include the
5077
+ // X-CSRF header, otherwise Spiderly.Shared/Attributes/AuthGuardAttribute.cs returns 403 Forbidden
5078
+ // (the server-side check was added in commit 92f238d but the matching client-side header was never
5079
+ // emitted, so every cookie-authed write was failing in the admin). The check is presence-only —
5080
+ // any non-empty value works — and the protection comes from the fact that a cross-origin form
5081
+ // submission cannot set custom request headers without a CORS preflight.
5082
+ const SAFE_HTTP_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
4912
5083
  const jwtInterceptor = (req, next) => {
4913
5084
  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
5085
  const isApiUrl = req.url.startsWith(config.apiUrl);
4920
- if (accessToken && isApiUrl) {
5086
+ if (isApiUrl) {
5087
+ const isStateChanging = !SAFE_HTTP_METHODS.has(req.method.toUpperCase());
4921
5088
  req = req.clone({
4922
- setHeaders: { Authorization: `Bearer ${accessToken}` },
5089
+ withCredentials: true,
5090
+ ...(isStateChanging && { setHeaders: { 'X-CSRF': '1' } }),
4923
5091
  });
4924
5092
  }
4925
5093
  return next(req);
4926
5094
  };
4927
5095
 
5096
+ /**
5097
+ * Owns cross-cutting HTTP-error UX: shows the right message and, on an expired session, clears auth — then
5098
+ * RETHROWS. Errors stay errors: callers run only their success path, and an unhandled HttpErrorResponse that
5099
+ * reaches the global ErrorHandler is intentionally ignored there (HTTP-error UX lives here). This interceptor
5100
+ * must never convert an error into a value — doing so makes callers treat failures as data.
5101
+ */
4928
5102
  const unauthorizedInterceptor = (req, next) => {
4929
5103
  const messageService = inject(SpiderlyMessageService);
4930
5104
  const translocoService = inject(TranslocoService);
4931
5105
  const config = inject(ConfigServiceBase);
4932
5106
  const authService = inject(AuthServiceBase);
4933
- const handleAuthError = (err, request) => {
5107
+ const reactToError = (err, request) => {
4934
5108
  if (!config.production) {
4935
5109
  console.error(err);
4936
5110
  }
4937
5111
  let errorResponse = err.error;
4938
- if (request.responseType != 'json')
4939
- errorResponse = JSON.parse(err.error);
4940
- if (err.status == 0) {
5112
+ if (request.responseType !== 'json' && typeof err.error === 'string') {
5113
+ try {
5114
+ errorResponse = JSON.parse(err.error);
5115
+ }
5116
+ catch {
5117
+ errorResponse = null;
5118
+ }
5119
+ }
5120
+ if (err.status === 0) {
5121
+ // Server unreachable; defer so the message isn't lost during a shutdown/refresh race.
4941
5122
  setTimeout(() => {
4942
- // Had problem when the server is shut down, and try to refresh token, warning message didn't appear
4943
5123
  messageService.warningMessage(translocoService.translate('ServerLostConnectionDetails'), translocoService.translate('ServerLostConnectionTitle'));
4944
5124
  }, 100);
4945
- return of(err.message);
4946
5125
  }
4947
- else if (err.status == 400) {
4948
- messageService.warningMessage(errorResponse.message ??
4949
- translocoService.translate('BadRequestDetails'), translocoService.translate('Warning'));
4950
- return of(err.message);
5126
+ else if (err.status === 400) {
5127
+ messageService.warningMessage(errorResponse?.message ?? translocoService.translate('BadRequestDetails'), translocoService.translate('Warning'));
4951
5128
  }
4952
- else if (err.status == 401) {
5129
+ else if (err.status === 401) {
4953
5130
  if (errorResponse?.errorCode === ApiErrorCodes.InvalidToken) {
4954
- authService.clearLocalStorage();
4955
- return of(err.message);
5131
+ authService.clearSession(); // expired/invalid session — drop it; guards send the user to login
5132
+ }
5133
+ else {
5134
+ messageService.warningMessage(errorResponse?.message ?? translocoService.translate('LoginRequired'), translocoService.translate('Warning'));
4956
5135
  }
4957
- messageService.warningMessage(errorResponse?.message ?? translocoService.translate('LoginRequired'), translocoService.translate('Warning'));
4958
- return of(err.message);
4959
5136
  }
4960
- else if (err.status == 403) {
5137
+ else if (err.status === 403) {
4961
5138
  messageService.warningMessage(translocoService.translate('PermissionErrorDetails'), translocoService.translate('PermissionErrorTitle'));
4962
- return of(err.message);
4963
5139
  }
4964
- else if (err.status == 404) {
5140
+ else if (err.status === 404) {
4965
5141
  messageService.warningMessage(translocoService.translate('NotFoundDetails'), translocoService.translate('NotFoundTitle'));
4966
- return of(err.message);
4967
5142
  }
4968
5143
  else {
4969
5144
  messageService.errorMessage(translocoService.translate('UnexpectedErrorDetails'), translocoService.translate('UnexpectedErrorTitle'));
4970
- return of(err.message);
4971
5145
  }
4972
5146
  };
4973
5147
  return next(req).pipe(catchError((err) => {
4974
- return handleAuthError(err, req);
5148
+ reactToError(err, req);
5149
+ return throwError(() => err);
4975
5150
  }));
4976
5151
  };
4977
5152
 
4978
5153
  function authInitializer(authService, platformId) {
4979
5154
  if (isPlatformBrowser(platformId)) {
4980
5155
  return () => {
5156
+ authService.captureExternalAuthError(); // before the router can strip ?externalAuthError on the /login redirect
4981
5157
  return authService.refreshToken();
4982
5158
  };
4983
5159
  }
@@ -5028,5 +5204,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
5028
5204
  * Generated bundle index. Do not edit.
5029
5205
  */
5030
5206
 
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 };
5207
+ 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
5208
  //# sourceMappingURL=spiderly.mjs.map