@mintplayer/ng-spark 22.1.0 → 22.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"mintplayer-ng-spark-po-create.mjs","sources":["../../po-create/src/spark-po-create.component.ts","../../po-create/src/spark-po-create.component.html","../../po-create/mintplayer-ng-spark-po-create.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, inject, output, signal } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { CommonModule } from '@angular/common';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsContainerComponent } from '@mintplayer/ng-bootstrap/container';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { SparkService } from '@mintplayer/ng-spark/services';\nimport { SparkPoFormComponent } from '@mintplayer/ng-spark/po-form';\nimport { TranslateKeyPipe, ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\nimport {\n EntityType,\n PersistentObject,\n PersistentObjectAttribute,\n ValidationError,\n ShowedOn,\n hasShowedOnFlag,\n dictToNestedPo,\n EntityTypeResolver,\n} from '@mintplayer/ng-spark/models';\n\n@Component({\n selector: 'spark-po-create',\n imports: [CommonModule, BsAlertComponent, BsContainerComponent, BsSpinnerComponent, SparkPoFormComponent, ResolveTranslationPipe, TranslateKeyPipe],\n templateUrl: './spark-po-create.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class SparkPoCreateComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly sparkService = inject(SparkService);\n\n saved = output<PersistentObject>();\n cancelled = output<void>();\n\n colors = Color;\n entityType = signal<EntityType | null>(null);\n type = signal('');\n formData = signal<Record<string, any>>({});\n validationErrors = signal<ValidationError[]>([]);\n isSaving = signal(false);\n private allEntityTypes = signal<EntityType[]>([]);\n generalErrors = computed(() => this.validationErrors().filter(e => !e.attributeName));\n\n constructor() {\n this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => this.onParamsChange(params));\n }\n\n private async onParamsChange(params: any): Promise<void> {\n this.type.set(params.get('type') || '');\n const types = await this.sparkService.getEntityTypes();\n const entityType = types.find(t => t.id === this.type() || t.alias === this.type()) || null;\n this.entityType.set(entityType);\n this.allEntityTypes.set(types);\n this.initFormData();\n }\n\n initFormData(): void {\n const data: Record<string, any> = {};\n this.getEditableAttributes().forEach(attr => {\n if (attr.dataType === 'Reference') {\n data[attr.name] = null;\n } else if (attr.dataType === 'AsDetail') {\n data[attr.name] = attr.isArray ? [] : {};\n } else if (attr.dataType === 'boolean') {\n data[attr.name] = false;\n } else {\n data[attr.name] = '';\n }\n });\n this.formData.set(data);\n }\n\n getEditableAttributes() {\n return this.entityType()?.attributes\n .filter(a => a.isVisible && !a.isReadOnly && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject))\n .sort((a, b) => a.order - b.order) || [];\n }\n\n async onSave(): Promise<void> {\n if (!this.entityType()) return;\n\n this.validationErrors.set([]);\n this.isSaving.set(true);\n\n const resolver: EntityTypeResolver = (clrName) => this.allEntityTypes().find(t => t.clrType === clrName);\n const attributes: PersistentObjectAttribute[] = this.getEditableAttributes().map(attr => {\n const base: PersistentObjectAttribute = {\n id: attr.id,\n name: attr.name,\n value: this.formData()[attr.name],\n dataType: attr.dataType,\n isArray: attr.isArray,\n isRequired: attr.isRequired,\n isVisible: attr.isVisible,\n isReadOnly: attr.isReadOnly,\n isValueChanged: true,\n order: attr.order,\n rules: attr.rules,\n };\n\n // AsDetail: pack the flat form dict into nested PO wire shape. Server's polymorphic\n // converter ignores attr.value for AsDetail and reads attr.object / attr.objects.\n if (attr.dataType === 'AsDetail' && attr.asDetailType) {\n const nestedType = resolver(attr.asDetailType);\n if (nestedType) {\n const raw = this.formData()[attr.name];\n base.value = null;\n base.asDetailType = attr.asDetailType;\n if (attr.isArray) {\n const items: any[] = Array.isArray(raw) ? raw : [];\n base.objects = items.map(item => dictToNestedPo((item ?? {}) as Record<string, any>, nestedType, resolver));\n base.object = null;\n } else {\n base.object = raw ? dictToNestedPo(raw as Record<string, any>, nestedType, resolver) : null;\n base.objects = null;\n }\n }\n }\n return base;\n });\n\n const po: Partial<PersistentObject> = {\n name: this.formData()['Name'] || 'New Item',\n objectTypeId: this.entityType()!.id,\n attributes\n };\n\n try {\n const result = await this.sparkService.create(this.type(), po);\n this.isSaving.set(false);\n this.saved.emit(result);\n this.router.navigate(['/po', this.type(), result.id]);\n } catch (e) {\n this.isSaving.set(false);\n const error = e as HttpErrorResponse;\n if (error.status === 400 && error.error?.errors) {\n this.validationErrors.set(error.error.errors);\n } else {\n this.validationErrors.set([{\n attributeName: '',\n errorMessage: { en: error.message || 'An unexpected error occurred' },\n ruleType: 'error'\n }]);\n }\n }\n }\n\n onCancel(): void {\n this.cancelled.emit();\n window.history.back();\n }\n}\n","<bs-container>\n<div class=\"container\">\n @if (entityType(); as et) {\n <h2 class=\"mb-4\">{{ 'common.create' | t }} {{ (et.description | resolveTranslation) || et.name }}</h2>\n\n @for (error of generalErrors(); track error.errorMessage) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ error.errorMessage }}\n </bs-alert>\n }\n\n <spark-po-form\n [entityType]=\"et\"\n [(formData)]=\"formData\"\n [validationErrors]=\"validationErrors()\"\n [showButtons]=\"true\"\n [isSaving]=\"isSaving()\"\n (save)=\"onSave()\"\n (cancel)=\"onCancel()\">\n </spark-po-form>\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;;;;;;;;;MA6Ba,sBAAsB,CAAA;AAChB,IAAA,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC;AAC9B,IAAA,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;AACvB,IAAA,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;IAEpD,KAAK,GAAG,MAAM,EAAoB;IAClC,SAAS,GAAG,MAAM,EAAQ;IAE1B,MAAM,GAAG,KAAK;IACd,UAAU,GAAG,MAAM,CAAoB,IAAI;mFAAC;IAC5C,IAAI,GAAG,MAAM,CAAC,EAAE;6EAAC;IACjB,QAAQ,GAAG,MAAM,CAAsB,EAAE;iFAAC;IAC1C,gBAAgB,GAAG,MAAM,CAAoB,EAAE;yFAAC;IAChD,QAAQ,GAAG,MAAM,CAAC,KAAK;iFAAC;IAChB,cAAc,GAAG,MAAM,CAAe,EAAE;uFAAC;IACjD,aAAa,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC;sFAAC;AAErF,IAAA,WAAA,GAAA;QACE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACjG;IAEQ,MAAM,cAAc,CAAC,MAAW,EAAA;AACtC,QAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE;AACtD,QAAA,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI;AAC3F,QAAA,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC;AAC/B,QAAA,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC;QAC9B,IAAI,CAAC,YAAY,EAAE;IACrB;IAEA,YAAY,GAAA;QACV,MAAM,IAAI,GAAwB,EAAE;QACpC,IAAI,CAAC,qBAAqB,EAAE,CAAC,OAAO,CAAC,IAAI,IAAG;AAC1C,YAAA,IAAI,IAAI,CAAC,QAAQ,KAAK,WAAW,EAAE;AACjC,gBAAA,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI;YACxB;AAAO,iBAAA,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU,EAAE;AACvC,gBAAA,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,EAAE,GAAG,EAAE;YAC1C;AAAO,iBAAA,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE;AACtC,gBAAA,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK;YACzB;iBAAO;AACL,gBAAA,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;YACtB;AACF,QAAA,CAAC,CAAC;AACF,QAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;IACzB;IAEA,qBAAqB,GAAA;AACnB,QAAA,OAAO,IAAI,CAAC,UAAU,EAAE,EAAE;aACvB,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,UAAU,IAAI,eAAe,CAAC,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC;AAClG,aAAA,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE;IAC5C;AAEA,IAAA,MAAM,MAAM,GAAA;AACV,QAAA,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE;YAAE;AAExB,QAAA,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;AAC7B,QAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;QAEvB,MAAM,QAAQ,GAAuB,CAAC,OAAO,KAAK,IAAI,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC;QACxG,MAAM,UAAU,GAAgC,IAAI,CAAC,qBAAqB,EAAE,CAAC,GAAG,CAAC,IAAI,IAAG;AACtF,YAAA,MAAM,IAAI,GAA8B;gBACtC,EAAE,EAAE,IAAI,CAAC,EAAE;gBACX,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;gBACjC,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,UAAU,EAAE,IAAI,CAAC,UAAU;AAC3B,gBAAA,cAAc,EAAE,IAAI;gBACpB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,KAAK,EAAE,IAAI,CAAC,KAAK;aAClB;;;YAID,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU,IAAI,IAAI,CAAC,YAAY,EAAE;gBACrD,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC;gBAC9C,IAAI,UAAU,EAAE;oBACd,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;AACtC,oBAAA,IAAI,CAAC,KAAK,GAAG,IAAI;AACjB,oBAAA,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY;AACrC,oBAAA,IAAI,IAAI,CAAC,OAAO,EAAE;AAChB,wBAAA,MAAM,KAAK,GAAU,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,EAAE;wBAClD,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,cAAc,EAAE,IAAI,IAAI,EAAE,GAA0B,UAAU,EAAE,QAAQ,CAAC,CAAC;AAC3G,wBAAA,IAAI,CAAC,MAAM,GAAG,IAAI;oBACpB;yBAAO;AACL,wBAAA,IAAI,CAAC,MAAM,GAAG,GAAG,GAAG,cAAc,CAAC,GAA0B,EAAE,UAAU,EAAE,QAAQ,CAAC,GAAG,IAAI;AAC3F,wBAAA,IAAI,CAAC,OAAO,GAAG,IAAI;oBACrB;gBACF;YACF;AACA,YAAA,OAAO,IAAI;AACb,QAAA,CAAC,CAAC;AAEF,QAAA,MAAM,EAAE,GAA8B;YACpC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,IAAI,UAAU;AAC3C,YAAA,YAAY,EAAE,IAAI,CAAC,UAAU,EAAG,CAAC,EAAE;YACnC;SACD;AAED,QAAA,IAAI;AACF,YAAA,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC;AAC9D,YAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;AACxB,YAAA,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;AACvB,YAAA,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QACvD;QAAE,OAAO,CAAC,EAAE;AACV,YAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;YACxB,MAAM,KAAK,GAAG,CAAsB;AACpC,YAAA,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE;gBAC/C,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC;YAC/C;iBAAO;AACL,gBAAA,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;AACzB,wBAAA,aAAa,EAAE,EAAE;wBACjB,YAAY,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,IAAI,8BAA8B,EAAE;AACrE,wBAAA,QAAQ,EAAE;AACX,qBAAA,CAAC,CAAC;YACL;QACF;IACF;IAEA,QAAQ,GAAA;AACN,QAAA,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE;AACrB,QAAA,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE;IACvB;uGA5HW,sBAAsB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAtB,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,sBAAsB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,EAAA,KAAA,EAAA,OAAA,EAAA,SAAA,EAAA,WAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EC7BnC,wvBA2BA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EDFY,YAAY,+BAAE,gBAAgB,EAAA,QAAA,EAAA,UAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,UAAA,EAAA,WAAA,CAAA,EAAA,OAAA,EAAA,CAAA,iBAAA,EAAA,qBAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAE,oBAAoB,EAAA,QAAA,EAAA,cAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAE,kBAAkB,EAAA,QAAA,EAAA,YAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAE,oBAAoB,EAAA,QAAA,EAAA,eAAA,EAAA,MAAA,EAAA,CAAA,YAAA,EAAA,UAAA,EAAA,kBAAA,EAAA,aAAA,EAAA,UAAA,EAAA,UAAA,EAAA,YAAA,CAAA,EAAA,OAAA,EAAA,CAAA,gBAAA,EAAA,MAAA,EAAA,QAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,MAAA,EAAA,IAAA,EAAE,sBAAsB,sDAAE,gBAAgB,EAAA,IAAA,EAAA,GAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAIvI,sBAAsB,EAAA,UAAA,EAAA,CAAA;kBANlC,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,iBAAiB,WAClB,CAAC,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,gBAAgB,CAAC,EAAA,eAAA,EAElI,uBAAuB,CAAC,MAAM,EAAA,QAAA,EAAA,wvBAAA,EAAA;;;AE3BjD;;AAEG;;;;"}
1
+ {"version":3,"file":"mintplayer-ng-spark-po-create.mjs","sources":["../../po-create/src/spark-po-create.component.ts","../../po-create/src/spark-po-create.component.html","../../po-create/mintplayer-ng-spark-po-create.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, inject, output, signal } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { CommonModule } from '@angular/common';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsContainerComponent } from '@mintplayer/ng-bootstrap/container';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { SparkService } from '@mintplayer/ng-spark/services';\nimport { SparkPoFormComponent } from '@mintplayer/ng-spark/po-form';\nimport { TranslateKeyPipe, ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\nimport {\n EntityType,\n PersistentObject,\n PersistentObjectAttribute,\n ValidationError,\n ShowedOn,\n hasShowedOnFlag,\n dictToNestedPo,\n EntityTypeResolver,\n} from '@mintplayer/ng-spark/models';\n\n@Component({\n selector: 'spark-po-create',\n imports: [CommonModule, BsAlertComponent, BsContainerComponent, BsSpinnerComponent, SparkPoFormComponent, ResolveTranslationPipe, TranslateKeyPipe],\n templateUrl: './spark-po-create.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class SparkPoCreateComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly sparkService = inject(SparkService);\n\n saved = output<PersistentObject>();\n cancelled = output<void>();\n\n colors = Color;\n entityType = signal<EntityType | null>(null);\n type = signal('');\n formData = signal<Record<string, any>>({});\n validationErrors = signal<ValidationError[]>([]);\n isSaving = signal(false);\n private allEntityTypes = signal<EntityType[]>([]);\n generalErrors = computed(() => this.validationErrors().filter(e => !e.attributeName));\n\n constructor() {\n this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => this.onParamsChange(params));\n }\n\n private async onParamsChange(params: any): Promise<void> {\n this.type.set(params.get('type') || '');\n const types = await this.sparkService.getEntityTypes();\n const entityType = types.find(t => t.id === this.type() || t.alias === this.type()) || null;\n this.entityType.set(entityType);\n this.allEntityTypes.set(types);\n this.initFormData();\n }\n\n initFormData(): void {\n const data: Record<string, any> = {};\n this.getEditableAttributes().forEach(attr => {\n if (attr.dataType === 'Reference') {\n data[attr.name] = null;\n } else if (attr.dataType === 'AsDetail') {\n data[attr.name] = attr.isArray ? [] : {};\n } else if (attr.dataType === 'boolean') {\n data[attr.name] = false;\n } else {\n data[attr.name] = '';\n }\n });\n this.formData.set(data);\n }\n\n getEditableAttributes() {\n return this.entityType()?.attributes\n .filter(a => a.isVisible && !a.isReadOnly && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject))\n .sort((a, b) => a.order - b.order) || [];\n }\n\n async onSave(): Promise<void> {\n if (!this.entityType()) return;\n\n this.validationErrors.set([]);\n this.isSaving.set(true);\n\n const resolver: EntityTypeResolver = (clrName) => this.allEntityTypes().find(t => t.clrType === clrName);\n const attributes: PersistentObjectAttribute[] = this.getEditableAttributes().map(attr => {\n const base: PersistentObjectAttribute = {\n id: attr.id,\n name: attr.name,\n value: this.formData()[attr.name],\n dataType: attr.dataType,\n isArray: attr.isArray,\n isRequired: attr.isRequired,\n isVisible: attr.isVisible,\n isReadOnly: attr.isReadOnly,\n isValueChanged: true,\n order: attr.order,\n rules: attr.rules,\n };\n\n // AsDetail: pack the flat form dict into nested PO wire shape. Server's polymorphic\n // converter ignores attr.value for AsDetail and reads attr.object / attr.objects.\n if (attr.dataType === 'AsDetail' && attr.asDetailType) {\n const nestedType = resolver(attr.asDetailType);\n if (nestedType) {\n const raw = this.formData()[attr.name];\n base.value = null;\n base.asDetailType = attr.asDetailType;\n if (attr.isArray) {\n const items: any[] = Array.isArray(raw) ? raw : [];\n base.objects = items.map(item => dictToNestedPo((item ?? {}) as Record<string, any>, nestedType, resolver));\n base.object = null;\n } else {\n base.object = raw ? dictToNestedPo(raw as Record<string, any>, nestedType, resolver) : null;\n base.objects = null;\n }\n }\n }\n return base;\n });\n\n const po: Partial<PersistentObject> = {\n name: this.formData()['Name'] || 'New Item',\n objectTypeId: this.entityType()!.id,\n attributes\n };\n\n try {\n const result = await this.sparkService.create(this.type(), po);\n this.isSaving.set(false);\n this.saved.emit(result);\n this.router.navigate(['/po', this.type(), result.id]);\n } catch (e) {\n this.isSaving.set(false);\n const error = e as HttpErrorResponse;\n if (error.status === 400 && error.error?.errors) {\n this.validationErrors.set(error.error.errors);\n } else {\n this.validationErrors.set([{\n attributeName: '',\n errorMessage: { en: error.message || 'An unexpected error occurred' },\n ruleType: 'error'\n }]);\n }\n }\n }\n\n onCancel(): void {\n this.cancelled.emit();\n window.history.back();\n }\n}\n","<bs-container>\n<div class=\"container\">\n @if (entityType(); as et) {\n <h2 class=\"mb-4\">{{ 'common.create' | t }} {{ (et.description | resolveTranslation) || et.name }}</h2>\n\n @for (error of generalErrors(); track error.errorMessage) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ error.errorMessage | resolveTranslation }}\n </bs-alert>\n }\n\n <spark-po-form\n [entityType]=\"et\"\n [(formData)]=\"formData\"\n [validationErrors]=\"validationErrors()\"\n [showButtons]=\"true\"\n [isSaving]=\"isSaving()\"\n (save)=\"onSave()\"\n (cancel)=\"onCancel()\">\n </spark-po-form>\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, input, signal, computed, effect, ChangeDetectionStrategy, Component, output } from '@angular/core';
2
+ import { inject, input, signal, output, computed, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
4
  import * as i1 from '@angular/common';
5
5
  import { CommonModule, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
@@ -15,29 +15,108 @@ import { BsTableComponent } from '@mintplayer/ng-bootstrap/table';
15
15
  import { BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective } from '@mintplayer/ng-bootstrap/tab-control';
16
16
  import { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';
17
17
  import { SparkService, SparkLanguageService } from '@mintplayer/ng-spark/services';
18
+ import { SparkQueryRefreshService } from '@mintplayer/ng-spark/client-operations';
18
19
  import { ResolveTranslationPipe, AttributeValuePipe, ReferenceChipsPipe, TranslateKeyPipe, RawAttributeValuePipe, AsDetailColumnsPipe, AsDetailCellValuePipe, ArrayValuePipe, ReferenceLinkRoutePipe } from '@mintplayer/ng-spark/pipes';
19
20
  import { SparkIconComponent } from '@mintplayer/ng-spark/icon';
20
21
  import { DatatableSettings, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective } from '@mintplayer/ng-bootstrap/datatable';
21
- import { SPARK_ATTRIBUTE_RENDERERS, withDeclaredInputs, rendererValue } from '@mintplayer/ng-spark/renderers';
22
- import { hasShowedOnFlag, ShowedOn } from '@mintplayer/ng-spark/models';
22
+ import { SparkGridRenderers, SPARK_GRID_PAGE_SIZES, isVirtualScrollingQuery, visibleGridAttributes, initialGridSettings } from '@mintplayer/ng-spark/grid';
23
+ import { selectionModeFor, parseSelectionRule, filterQueryActions, hasShowedOnFlag, ShowedOn } from '@mintplayer/ng-spark/models';
24
+ import { SPARK_ATTRIBUTE_RENDERERS, rendererValue, withDeclaredInputs } from '@mintplayer/ng-spark/renderers';
23
25
 
24
26
  class SparkSubQueryComponent {
25
27
  sparkService = inject(SparkService);
26
- rendererRegistry = inject(SPARK_ATTRIBUTE_RENDERERS);
28
+ gridRenderers = inject(SparkGridRenderers);
29
+ lang = inject(SparkLanguageService);
27
30
  queryId = input.required(/* @ts-ignore */
28
31
  ...(ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []));
29
- parentId = input.required(/* @ts-ignore */
32
+ /**
33
+ * The parent persistent object this query is scoped to, when it has one.
34
+ *
35
+ * Optional, because not every query is a detail of something: a page can host
36
+ * a grid that stands on its own — "my accounts", a dashboard list — and the
37
+ * server already treats an absent parent as "no parent" rather than as an
38
+ * error. Leaving these required made that shape impossible to express: the
39
+ * component simply never loaded, with no request, no error and no log.
40
+ *
41
+ * Pass both or neither. One without the other is ignored, matching
42
+ * `SparkService.executeQuery`, which omits either param when it is falsy, and
43
+ * the execute endpoint, which resolves a parent only when both are present.
44
+ */
45
+ parentId = input('', /* @ts-ignore */
30
46
  ...(ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []));
31
- parentType = input.required(/* @ts-ignore */
47
+ parentType = input('', /* @ts-ignore */
32
48
  ...(ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []));
49
+ /**
50
+ * Change this to re-run the query. Any value works; only its identity matters.
51
+ *
52
+ * A declarative token rather than only a `reload()` method, because calling a
53
+ * method means holding a component handle, and hosts wrap this grid in `@if`,
54
+ * where a `viewChild` is intermittently undefined. Nothing else in ng-spark
55
+ * uses `viewChild` either — the house idiom is to re-seed a signal.
56
+ *
57
+ * This drives the CHEAP refresh (see {@link reload}). It deliberately does not
58
+ * feed the main effect: re-running `loadData` would re-resolve the query, the
59
+ * entity types, the permissions and the lookups, and reset the user's page and
60
+ * sort on every button press.
61
+ */
62
+ reloadToken = input(null, /* @ts-ignore */
63
+ ...(ngDevMode ? [{ debugName: "reloadToken" }] : /* istanbul ignore next */ []));
64
+ /**
65
+ * Render without the surrounding card, for a host that owns its own chrome — a tab
66
+ * body, a modal, a dashboard tile.
67
+ *
68
+ * This is the escape hatch for a genuinely chromeless embed. It only serves a host
69
+ * that instantiated this component by hand — where the component is auto-rendered
70
+ * from `EntityTypeDefinition.Queries` there is no host to pass it.
71
+ */
72
+ showCard = input(true, /* @ts-ignore */
73
+ ...(ngDevMode ? [{ debugName: "showCard" }] : /* istanbul ignore next */ []));
74
+ /**
75
+ * Replace the header for one hand-instantiated usage.
76
+ *
77
+ * A `TemplateRef` rather than `<ng-content>` deliberately: it matches
78
+ * `spark-po-detail`'s `extraActionsTemplate`/`extraContentTemplate`, and unlike
79
+ * projection it can be forwarded by a host that is itself several layers up.
80
+ *
81
+ * Precedence is headerTemplate -> caption + query actions.
82
+ */
83
+ headerTemplate = input(null, /* @ts-ignore */
84
+ ...(ngDevMode ? [{ debugName: "headerTemplate" }] : /* istanbul ignore next */ []));
85
+ colors = Color;
33
86
  query = signal(null, /* @ts-ignore */
34
87
  ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
35
88
  entityType = signal(null, /* @ts-ignore */
36
89
  ...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
37
90
  allEntityTypes = signal([], /* @ts-ignore */
38
91
  ...(ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []));
39
- resultCount = signal(null, /* @ts-ignore */
40
- ...(ngDevMode ? [{ debugName: "resultCount" }] : /* istanbul ignore next */ []));
92
+ /**
93
+ * Why the component renders its own failure instead of only reporting one.
94
+ *
95
+ * `SparkService` is a bare `firstValueFrom` passthrough with no interceptor, so
96
+ * every failure surfaces here and nowhere else. A host embedding this grid cannot
97
+ * surface what it never sees, and the default has to be visible with no host
98
+ * cooperation — hence a rendered alert, not just an output.
99
+ *
100
+ * A 404 is deliberately vague. `Endpoints/Queries/Get.cs` answers 404 for BOTH
101
+ * "no such query" and "you may not see it", with byte-identical bodies, so that
102
+ * existence is not disclosed (security audit M-3). This component therefore
103
+ * genuinely cannot tell the two apart, and any message claiming otherwise would
104
+ * either leak or mislead.
105
+ */
106
+ errorMessage = signal(null, /* @ts-ignore */
107
+ ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
108
+ /**
109
+ * Actions the query declares, rendered in this component's own header.
110
+ *
111
+ * This is what makes a query's chrome work with no host: a sub-query is rendered
112
+ * automatically from `EntityTypeDefinition.Queries`, so there is nobody to project
113
+ * a toolbar in. The query says what belongs in its header, and it follows the query
114
+ * wherever it is rendered.
115
+ */
116
+ customActions = signal([], /* @ts-ignore */
117
+ ...(ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []));
118
+ /** Emitted whenever a load or a page fetch fails, for a host in bespoke chrome. */
119
+ error = output();
41
120
  lookupReferenceOptions = signal({}, /* @ts-ignore */
42
121
  ...(ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : /* istanbul ignore next */ []));
43
122
  loading = signal(true, /* @ts-ignore */
@@ -45,35 +124,123 @@ class SparkSubQueryComponent {
45
124
  canRead = signal(false, /* @ts-ignore */
46
125
  ...(ngDevMode ? [{ debugName: "canRead" }] : /* istanbul ignore next */ []));
47
126
  settings = signal(new DatatableSettings({
48
- perPage: { values: [10, 25, 50], selected: 10 },
127
+ perPage: { values: SPARK_GRID_PAGE_SIZES, selected: SPARK_GRID_PAGE_SIZES[0] },
49
128
  page: { values: [1], selected: 1 },
50
129
  sortColumns: []
51
130
  }), /* @ts-ignore */
52
131
  ...(ngDevMode ? [{ debugName: "settings" }] : /* istanbul ignore next */ []));
53
132
  fetchFn = signal(null, /* @ts-ignore */
54
133
  ...(ngDevMode ? [{ debugName: "fetchFn" }] : /* istanbul ignore next */ []));
55
- isVirtualScrolling = computed(() => this.query()?.renderMode === 'VirtualScrolling', /* @ts-ignore */
134
+ /**
135
+ * Rows the user has ticked. Lives here rather than in the datatable so the action bar can
136
+ * read it, and MUST be cleared whenever the source changes — otherwise route A's selection
137
+ * is POSTed as ids of route B's type.
138
+ */
139
+ selection = signal([], /* @ts-ignore */
140
+ ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
141
+ queryRefresh = inject(SparkQueryRefreshService);
142
+ /** 'none' unless an action is selection-gated, so unaffected grids gain no checkbox column. */
143
+ selectionMode = computed(() => selectionModeFor(this.customActions()), /* @ts-ignore */
144
+ ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
145
+ /** Whether an action's selection rule is satisfied right now. The server checks it again. */
146
+ isActionEnabled(action) {
147
+ return parseSelectionRule(action.selectionRule)(this.selection().length);
148
+ }
149
+ isVirtualScrolling = computed(() => isVirtualScrollingQuery(this.query()), /* @ts-ignore */
56
150
  ...(ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []));
57
- visibleAttributes = computed(() => {
58
- return this.entityType()?.attributes
59
- .filter(a => a.isVisible && hasShowedOnFlag(a.showedOn, ShowedOn.Query))
60
- .sort((a, b) => a.order - b.order) || [];
61
- }, /* @ts-ignore */
151
+ visibleAttributes = computed(() => visibleGridAttributes(this.entityType()), /* @ts-ignore */
62
152
  ...(ngDevMode ? [{ debugName: "visibleAttributes" }] : /* istanbul ignore next */ []));
63
153
  constructor() {
64
154
  effect(() => {
65
155
  const qId = this.queryId();
66
156
  const pId = this.parentId();
67
157
  const pType = this.parentType();
68
- if (qId && pId && pType) {
158
+ // Only the query id is required. Requiring a parent here is what made a
159
+ // standalone grid silently render nothing.
160
+ if (qId) {
69
161
  this.loadData(qId, pId, pType);
70
162
  }
163
+ else {
164
+ // No query id at all. `loading` starts true, so without this the component
165
+ // would spin forever instead of saying anything.
166
+ this.loading.set(false);
167
+ }
168
+ });
169
+ // Separate effect, so the token drives the cheap refresh and never the full
170
+ // metadata reload. `first` skips the initial run: the effect above has already
171
+ // fetched, and reacting to the token's starting value would double-fetch on mount.
172
+ let first = true;
173
+ effect(() => {
174
+ // Both the host's token and the server's refreshQuery drive the same cheap refresh.
175
+ this.reloadToken();
176
+ this.queryRefresh.tokenFor(this.queryId());
177
+ if (first) {
178
+ first = false;
179
+ return;
180
+ }
181
+ untracked(() => this.reload());
71
182
  });
72
183
  }
184
+ /**
185
+ * Re-run the query, keeping the current page, sort and scroll position.
186
+ *
187
+ * Data-level on purpose: it re-seeds the fetch closure and nothing else, mirroring
188
+ * `SparkQueryListComponent.reload()`. Use it after something mutates server-side
189
+ * state the query reads from. For a definition change — new columns, a renamed
190
+ * query — the inputs themselves must change; that is the expensive path.
191
+ */
192
+ async onCustomAction(action) {
193
+ if (action.confirmationMessageKey) {
194
+ const message = this.lang.t(action.confirmationMessageKey) || 'Are you sure?';
195
+ if (!confirm(message))
196
+ return;
197
+ }
198
+ try {
199
+ await this.sparkService.executeCustomAction(this.entityType().id, action.name, undefined, this.selection());
200
+ if (action.refreshOnCompleted)
201
+ this.reload();
202
+ }
203
+ catch (e) {
204
+ const err = e;
205
+ this.errorMessage.set(err.error?.error || err.message || this.lang.t('common.actionFailed') || 'Action failed');
206
+ }
207
+ }
208
+ reportError(err) {
209
+ this.errorMessage.set(this.describe(err));
210
+ this.error.emit(err);
211
+ }
212
+ /**
213
+ * A 404 is deliberately generic.
214
+ *
215
+ * `Endpoints/Queries/Get.cs` answers 404 with byte-identical bodies for "no such
216
+ * query" and "you may not see it", so existence is not disclosed (audit M-3). The
217
+ * component therefore cannot tell them apart, and both "Not found" and "Access
218
+ * denied" would be a guess — one of them leaking, the other misleading.
219
+ */
220
+ describe(err) {
221
+ if (err?.status === 404) {
222
+ return this.lang.t('spark.query.unavailable') || 'This list is not available.';
223
+ }
224
+ return err?.error?.error || err?.message
225
+ || this.lang.t('common.unexpectedError') || 'An unexpected error occurred';
226
+ }
227
+ reload() {
228
+ const q = this.query();
229
+ if (q)
230
+ this.fetchFn.set(this.makeFetch(q, this.parentId(), this.parentType()));
231
+ }
73
232
  async loadData(queryId, parentId, parentType) {
74
233
  this.loading.set(true);
75
- this.resultCount.set(null);
234
+ this.errorMessage.set(null);
76
235
  this.fetchFn.set(null);
236
+ // Reset everything derived from the previous query, not just the fetch. Leaving
237
+ // `entityType`/`canRead` behind let a failed reload build a row link out of the
238
+ // PREVIOUS type and the previous permission.
239
+ this.query.set(null);
240
+ this.entityType.set(null);
241
+ this.canRead.set(false);
242
+ this.customActions.set([]);
243
+ this.selection.set([]);
77
244
  try {
78
245
  const [resolvedQuery, entityTypes] = await Promise.all([
79
246
  this.sparkService.getQuery(queryId),
@@ -81,31 +248,28 @@ class SparkSubQueryComponent {
81
248
  ]);
82
249
  this.query.set(resolvedQuery);
83
250
  this.allEntityTypes.set(entityTypes);
84
- const initialSortColumns = (resolvedQuery.sortColumns || []).map(sc => ({
85
- property: sc.property,
86
- direction: sc.direction === 'desc' ? 'descending' : 'ascending'
87
- }));
88
251
  // Resolve entity type from query's entityType field
89
252
  if (resolvedQuery.entityType) {
90
253
  const et = entityTypes.find(t => t.name === resolvedQuery.entityType || t.alias === resolvedQuery.entityType?.toLowerCase());
91
254
  this.entityType.set(et || null);
92
255
  if (et) {
93
- const permissions = await this.sparkService.getPermissions(et.id);
256
+ const [permissions, actions] = await Promise.all([
257
+ this.sparkService.getPermissions(et.id),
258
+ this.sparkService.getCustomActions(et.id),
259
+ ]);
94
260
  this.canRead.set(permissions.canRead);
261
+ this.customActions.set(filterQueryActions(actions));
95
262
  }
96
263
  }
97
- this.settings.set(new DatatableSettings({
98
- perPage: { values: [10, 25, 50], selected: 10 },
99
- page: { values: [1], selected: 1 },
100
- sortColumns: initialSortColumns
101
- }));
264
+ this.settings.set(initialGridSettings(resolvedQuery));
102
265
  // The datatable drives paging/sorting via [(settings)] and calls fetchFn
103
266
  // per page. Virtual scrolling is just the [virtualScroll] template flag.
104
267
  this.fetchFn.set(this.makeFetch(resolvedQuery, parentId, parentType));
105
268
  this.loadLookupReferenceOptions();
106
269
  }
107
- catch {
270
+ catch (e) {
108
271
  this.fetchFn.set(null);
272
+ this.reportError(e);
109
273
  }
110
274
  finally {
111
275
  this.loading.set(false);
@@ -118,7 +282,7 @@ class SparkSubQueryComponent {
118
282
  take: req.perPage,
119
283
  parentId, parentType,
120
284
  }).then(r => {
121
- this.resultCount.set(r.totalRecords);
285
+ this.errorMessage.set(null);
122
286
  return {
123
287
  data: r.data,
124
288
  totalRecords: r.totalRecords,
@@ -126,48 +290,35 @@ class SparkSubQueryComponent {
126
290
  perPage: req.perPage,
127
291
  page: req.page,
128
292
  };
129
- }).catch(() => {
130
- this.resultCount.set(0);
293
+ }).catch((e) => {
294
+ // Report before returning the empty page, or a failed fetch is indistinguishable
295
+ // from a query that legitimately has no rows.
296
+ this.reportError(e);
131
297
  return { data: [], totalRecords: 0, totalPages: 1, perPage: req.perPage, page: req.page };
132
298
  });
133
299
  }
134
300
  async loadLookupReferenceOptions() {
135
- const lookupAttrs = this.visibleAttributes().filter(a => a.lookupReferenceType);
136
- if (lookupAttrs.length === 0)
137
- return;
138
- const lookupNames = [...new Set(lookupAttrs.map(a => a.lookupReferenceType))];
139
- const entries = await Promise.all(lookupNames.map(async (name) => {
140
- const result = await this.sparkService.getLookupReference(name);
141
- return [name, result];
142
- }));
143
- this.lookupReferenceOptions.set(entries.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}));
301
+ this.lookupReferenceOptions.set(await this.gridRenderers.loadLookupOptions(this.visibleAttributes()));
144
302
  }
145
303
  getColumnRendererComponent(attr) {
146
- if (!attr.renderer)
147
- return null;
148
- return this.rendererRegistry.find(r => r.name === attr.renderer)?.columnComponent ?? null;
304
+ return this.gridRenderers.columnComponentFor(attr);
149
305
  }
150
306
  getColumnRendererInputs(component, item, attr) {
151
- const itemAttr = item.attributes.find(a => a.name === attr.name);
152
- return withDeclaredInputs(component, {
153
- value: rendererValue(itemAttr),
154
- attribute: attr,
155
- options: attr.rendererOptions,
156
- item,
157
- });
307
+ return this.gridRenderers.columnInputsFor(component, item, attr);
158
308
  }
159
309
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkSubQueryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
160
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: SparkSubQueryComponent, isStandalone: true, selector: "spark-sub-query", inputs: { queryId: { classPropertyName: "queryId", publicName: "queryId", isSignal: true, isRequired: true, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: true, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@if (query(); as q) {\n <bs-card style=\"display: block; margin: 1rem 0;\">\n <bs-card-header>{{ (q.description | resolveTranslation) || q.name }}</bs-card-header>\n @if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n } @else {\n <div class=\"p-3\">\n <bs-datatable\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [fetch]=\"fetchFn()\"\n [(settings)]=\"settings\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (row) {\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, row.id]\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n }\n } @else {\n &nbsp;\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n </div>\n }\n </bs-card>\n}\n\n<ng-template #cellContent let-item let-attr=\"attr\">\n @if (getColumnRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getColumnRendererInputs(rendererType, item, attr)\"></ng-container>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n disabled\n onclick=\"return false;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of (attr.name | referenceChips:item); track chip.id) {\n <span class=\"badge rounded-pill border bg-body-secondary text-body px-3 py-2\">{{ chip.label }}</span>\n }\n </span>\n } @else {\n {{ (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) }}\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["color", "outline"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["color", "navStyle"] }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["columns", "data", "fetch", "settings", "selectionMode", "selectable", "selection", "rowKey", "resizableColumns", "pagination", "virtualScroll", "itemSize", "virtualBuffer", "isResponsive", "compareWith", "tree", "idKey", "childCountKey", "treeIndent", "expandedIds", "selectionStrategy"], outputs: ["settingsChange", "selectionChange", "rowClick", "rowDblClick", "rowContextMenu", "expandedIdsChange", "rowExpand", "rowCollapse"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable"] }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: AttributeValuePipe, name: "attributeValue" }, { kind: "pipe", type: ReferenceChipsPipe, name: "referenceChips" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
310
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: SparkSubQueryComponent, isStandalone: true, selector: "spark-sub-query", inputs: { queryId: { classPropertyName: "queryId", publicName: "queryId", isSignal: true, isRequired: true, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: false, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: false, transformFunction: null }, reloadToken: { classPropertyName: "reloadToken", publicName: "reloadToken", isSignal: true, isRequired: false, transformFunction: null }, showCard: { classPropertyName: "showCard", publicName: "showCard", isSignal: true, isRequired: false, transformFunction: null }, headerTemplate: { classPropertyName: "headerTemplate", publicName: "headerTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { error: "error" }, ngImport: i0, template: "<!--\n Three explicit states, in this order, and the order is load-bearing.\n\n This template used to open with `@if (query(); as q)` wrapping EVERYTHING, and\n `query()` is only set after loadData's awaits resolve. Three separate bugs fell\n out of that one line: the spinner was unreachable on a first load (it rendered\n inside the gate, while `loading` starts true and `query` starts null), a\n first-load failure rendered ZERO DOM \u2014 no card, no message, nothing to see or\n report \u2014 and a failed RELOAD left the previous query's card on screen.\n\n So: `loading` is checked first and outside any dependency on `query`, and the\n final `@else` always renders something. Do not fold these back into one gate,\n and do not move the spinner inside the `query()` branch.\n-->\n<ng-template #content>\n @if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n } @else if (query(); as q) {\n @if (headerTemplate(); as headerTpl) {\n <bs-card-header>\n <ng-container *ngTemplateOutlet=\"headerTpl; context: { $implicit: q }\"></ng-container>\n </bs-card-header>\n } @else {\n <bs-card-header>\n @if (customActions().length) {\n <!-- Caption and actions share the header. The nav renders only when the query\n declares actions, so a query without them is pixel-identical to before. -->\n <div class=\"d-flex align-items-center gap-2\">\n <span class=\"me-auto\">{{ (q.description | resolveTranslation) || q.name }}</span>\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of customActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" class=\"btn btn-sm btn-outline-primary\"\n [disabled]=\"!isActionEnabled(action)\" (click)=\"onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n </bs-priority-nav>\n </div>\n } @else {\n {{ (q.description | resolveTranslation) || q.name }}\n }\n </bs-card-header>\n }\n <div class=\"p-3\">\n <!-- A page-fetch failure while the grid is already resolved. Without this the\n catch returns an empty page and a failed query is indistinguishable from\n one that legitimately has no rows. -->\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [fetch]=\"fetchFn()\"\n [(settings)]=\"settings\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (row) {\n <!-- The first column links to the row's detail page. Two things about\n this are routinely misread:\n\n 1. `cellContent` is projected INSIDE the anchor, so a custom\n `renderer` does NOT suppress this link \u2014 a renderer that emits\n its own <a> produces nested anchors (invalid HTML) rather than\n replacing this one.\n 2. `canRead()` is the whole gate, and it is the rights model:\n `Query` without `Read` lists the rows and withholds the link.\n Withhold `Read` for a query whose rows no detail page can load\n \u2014 a Custom.* query that fabricates rows in memory, say. -->\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, row.id]\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n }\n } @else {\n &nbsp;\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n </div>\n } @else {\n <!-- The query could not be resolved. Rendering something keeps the host's layout\n intact and, crucially, makes the failure visible: this branch used to render\n nothing at all. -->\n <div class=\"p-3\">\n <bs-alert [type]=\"colors.danger\" class=\"mb-0\">\n {{ errorMessage() || ('spark.query.unavailable' | t) }}\n </bs-alert>\n </div>\n }\n</ng-template>\n\n<!--\n The card is the component's, unless the host says otherwise. `bs-card-header` is\n styled by a GLOBAL `.card-header` rule rather than `::slotted`, so the header keeps\n its appearance with no `bs-card` ancestor \u2014 which is what makes the bare branch a\n wrapper change only, with no duplicated content.\n-->\n@if (showCard()) {\n <bs-card style=\"display: block; margin: 1rem 0;\">\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </bs-card>\n} @else {\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n}\n\n<ng-template #cellContent let-item let-attr=\"attr\">\n @if (getColumnRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getColumnRendererInputs(rendererType, item, attr)\"></ng-container>\n } @else if (attr.dataType === 'boolean') {\n @let boolVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n <!-- [indeterminate] matters: without it a null boolean renders as an unchecked\n box, which reads as an explicit `false`. spark-query-list has always bound\n it; this component had drifted. -->\n <input type=\"checkbox\"\n [checked]=\"boolVal === true\"\n [indeterminate]=\"boolVal === null || boolVal === undefined\"\n disabled\n onclick=\"return false;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of (attr.name | referenceChips:item); track chip.id) {\n <span class=\"badge rounded-pill border bg-body-secondary text-body px-3 py-2\">{{ chip.label }}</span>\n }\n </span>\n } @else {\n {{ (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) }}\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["color", "outline"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["color", "navStyle"] }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["columns", "data", "fetch", "settings", "selectionMode", "selectable", "selection", "rowKey", "resizableColumns", "pagination", "virtualScroll", "itemSize", "virtualBuffer", "isResponsive", "compareWith", "tree", "idKey", "childCountKey", "treeIndent", "expandedIds", "selectionStrategy"], outputs: ["settingsChange", "selectionChange", "rowClick", "rowDblClick", "rowContextMenu", "expandedIdsChange", "rowExpand", "rowCollapse"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable"] }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]" }, { kind: "component", type: BsPriorityNavComponent, selector: "bs-priority-nav", inputs: ["moreLabel", "moreLabelTemplate", "collapseAt", "overflowFrom", "hideEmptyMore", "ariaLabel"], outputs: ["overflowChange"] }, { kind: "directive", type: BsPriorityNavItemDirective, selector: "[bsPriorityNavItem]", inputs: ["bsPriorityNavItem", "bsPriorityNavItemHideBelow"] }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: AttributeValuePipe, name: "attributeValue" }, { kind: "pipe", type: ReferenceChipsPipe, name: "referenceChips" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
161
311
  }
162
312
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkSubQueryComponent, decorators: [{
163
313
  type: Component,
164
- args: [{ selector: 'spark-sub-query', imports: [CommonModule, NgComponentOutlet, RouterModule, BsCardComponent, BsCardHeaderComponent, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective, BsSpinnerComponent, ResolveTranslationPipe, AttributeValuePipe, ReferenceChipsPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (query(); as q) {\n <bs-card style=\"display: block; margin: 1rem 0;\">\n <bs-card-header>{{ (q.description | resolveTranslation) || q.name }}</bs-card-header>\n @if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n } @else {\n <div class=\"p-3\">\n <bs-datatable\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [fetch]=\"fetchFn()\"\n [(settings)]=\"settings\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (row) {\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, row.id]\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n }\n } @else {\n &nbsp;\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n </div>\n }\n </bs-card>\n}\n\n<ng-template #cellContent let-item let-attr=\"attr\">\n @if (getColumnRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getColumnRendererInputs(rendererType, item, attr)\"></ng-container>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n disabled\n onclick=\"return false;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of (attr.name | referenceChips:item); track chip.id) {\n <span class=\"badge rounded-pill border bg-body-secondary text-body px-3 py-2\">{{ chip.label }}</span>\n }\n </span>\n } @else {\n {{ (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) }}\n }\n</ng-template>\n" }]
165
- }], ctorParameters: () => [], propDecorators: { queryId: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryId", required: true }] }], parentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentId", required: true }] }], parentType: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentType", required: true }] }] } });
314
+ args: [{ selector: 'spark-sub-query', imports: [CommonModule, NgComponentOutlet, RouterModule, BsAlertComponent, BsCardComponent, BsCardHeaderComponent, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective, BsPriorityNavComponent, BsPriorityNavItemDirective, BsSpinnerComponent, ResolveTranslationPipe, AttributeValuePipe, ReferenceChipsPipe, TranslateKeyPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Three explicit states, in this order, and the order is load-bearing.\n\n This template used to open with `@if (query(); as q)` wrapping EVERYTHING, and\n `query()` is only set after loadData's awaits resolve. Three separate bugs fell\n out of that one line: the spinner was unreachable on a first load (it rendered\n inside the gate, while `loading` starts true and `query` starts null), a\n first-load failure rendered ZERO DOM \u2014 no card, no message, nothing to see or\n report \u2014 and a failed RELOAD left the previous query's card on screen.\n\n So: `loading` is checked first and outside any dependency on `query`, and the\n final `@else` always renders something. Do not fold these back into one gate,\n and do not move the spinner inside the `query()` branch.\n-->\n<ng-template #content>\n @if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n } @else if (query(); as q) {\n @if (headerTemplate(); as headerTpl) {\n <bs-card-header>\n <ng-container *ngTemplateOutlet=\"headerTpl; context: { $implicit: q }\"></ng-container>\n </bs-card-header>\n } @else {\n <bs-card-header>\n @if (customActions().length) {\n <!-- Caption and actions share the header. The nav renders only when the query\n declares actions, so a query without them is pixel-identical to before. -->\n <div class=\"d-flex align-items-center gap-2\">\n <span class=\"me-auto\">{{ (q.description | resolveTranslation) || q.name }}</span>\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of customActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" class=\"btn btn-sm btn-outline-primary\"\n [disabled]=\"!isActionEnabled(action)\" (click)=\"onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n </bs-priority-nav>\n </div>\n } @else {\n {{ (q.description | resolveTranslation) || q.name }}\n }\n </bs-card-header>\n }\n <div class=\"p-3\">\n <!-- A page-fetch failure while the grid is already resolved. Without this the\n catch returns an empty page and a failed query is indistinguishable from\n one that legitimately has no rows. -->\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [fetch]=\"fetchFn()\"\n [(settings)]=\"settings\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (row) {\n <!-- The first column links to the row's detail page. Two things about\n this are routinely misread:\n\n 1. `cellContent` is projected INSIDE the anchor, so a custom\n `renderer` does NOT suppress this link \u2014 a renderer that emits\n its own <a> produces nested anchors (invalid HTML) rather than\n replacing this one.\n 2. `canRead()` is the whole gate, and it is the rights model:\n `Query` without `Read` lists the rows and withholds the link.\n Withhold `Read` for a query whose rows no detail page can load\n \u2014 a Custom.* query that fabricates rows in memory, say. -->\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, row.id]\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n }\n } @else {\n &nbsp;\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n </div>\n } @else {\n <!-- The query could not be resolved. Rendering something keeps the host's layout\n intact and, crucially, makes the failure visible: this branch used to render\n nothing at all. -->\n <div class=\"p-3\">\n <bs-alert [type]=\"colors.danger\" class=\"mb-0\">\n {{ errorMessage() || ('spark.query.unavailable' | t) }}\n </bs-alert>\n </div>\n }\n</ng-template>\n\n<!--\n The card is the component's, unless the host says otherwise. `bs-card-header` is\n styled by a GLOBAL `.card-header` rule rather than `::slotted`, so the header keeps\n its appearance with no `bs-card` ancestor \u2014 which is what makes the bare branch a\n wrapper change only, with no duplicated content.\n-->\n@if (showCard()) {\n <bs-card style=\"display: block; margin: 1rem 0;\">\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </bs-card>\n} @else {\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n}\n\n<ng-template #cellContent let-item let-attr=\"attr\">\n @if (getColumnRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getColumnRendererInputs(rendererType, item, attr)\"></ng-container>\n } @else if (attr.dataType === 'boolean') {\n @let boolVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n <!-- [indeterminate] matters: without it a null boolean renders as an unchecked\n box, which reads as an explicit `false`. spark-query-list has always bound\n it; this component had drifted. -->\n <input type=\"checkbox\"\n [checked]=\"boolVal === true\"\n [indeterminate]=\"boolVal === null || boolVal === undefined\"\n disabled\n onclick=\"return false;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of (attr.name | referenceChips:item); track chip.id) {\n <span class=\"badge rounded-pill border bg-body-secondary text-body px-3 py-2\">{{ chip.label }}</span>\n }\n </span>\n } @else {\n {{ (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) }}\n }\n</ng-template>\n" }]
315
+ }], ctorParameters: () => [], propDecorators: { queryId: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryId", required: true }] }], parentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentId", required: false }] }], parentType: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentType", required: false }] }], reloadToken: [{ type: i0.Input, args: [{ isSignal: true, alias: "reloadToken", required: false }] }], showCard: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCard", required: false }] }], headerTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerTemplate", required: false }] }], error: [{ type: i0.Output, args: ["error"] }] } });
166
316
 
167
317
  class SparkPoDetailComponent {
168
318
  route = inject(ActivatedRoute);
169
319
  router = inject(Router);
170
320
  sparkService = inject(SparkService);
321
+ queryRefresh = inject(SparkQueryRefreshService);
171
322
  lang = inject(SparkLanguageService);
172
323
  rendererRegistry = inject(SPARK_ATTRIBUTE_RENDERERS);
173
324
  showCustomActions = input(true, /* @ts-ignore */
@@ -359,6 +510,12 @@ class SparkPoDetailComponent {
359
510
  if (action.refreshOnCompleted) {
360
511
  const item = await this.sparkService.get(this.type, this.id);
361
512
  this.item.set(item);
513
+ // The sub-query grids below do not depend on item(), so re-fetching the PO left them
514
+ // showing pre-action rows -- the action appeared to have done nothing to the very
515
+ // lists it changed.
516
+ for (const queryAlias of (this.entityType()?.queries ?? [])) {
517
+ this.queryRefresh.request(queryAlias);
518
+ }
362
519
  }
363
520
  }
364
521
  catch (e) {
@@ -381,7 +538,7 @@ class SparkPoDetailComponent {
381
538
  window.history.back();
382
539
  }
383
540
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkPoDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
384
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: SparkPoDetailComponent, isStandalone: true, selector: "spark-po-detail", inputs: { showCustomActions: { classPropertyName: "showCustomActions", publicName: "showCustomActions", isSignal: true, isRequired: false, transformFunction: null }, extraActionsTemplate: { classPropertyName: "extraActionsTemplate", publicName: "extraActionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, extraContentTemplate: { classPropertyName: "extraContentTemplate", publicName: "extraContentTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { edited: "edited", deleted: "deleted", customActionExecuted: "customActionExecuted" }, ngImport: i0, template: "<bs-container>\n<div class=\"container\">\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n } @else if (item(); as currentItem) {\n @if (entityType(); as et) {\n <div class=\"spark-actionbar px-3 py-2\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <button *bsPriorityNavItem=\"1\" class=\"btn btn-outline-secondary\" (click)=\"onBack()\">\n <spark-icon name=\"arrow-left\" /> {{ 'common.back' | t }}\n </button>\n @if (canEdit()) {\n <button *bsPriorityNavItem=\"2\" class=\"btn btn-primary\" (click)=\"onEdit()\">\n <spark-icon name=\"pencil\" /> {{ 'common.edit' | t }}\n </button>\n }\n @if (canDelete()) {\n <button *bsPriorityNavItem=\"3\" class=\"btn btn-danger\" (click)=\"onDelete()\">\n <spark-icon name=\"trash\" /> {{ 'common.delete' | t }}\n </button>\n }\n @if (showCustomActions()) {\n @for (action of customActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" class=\"btn btn-outline-primary\" (click)=\"onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n </bs-priority-nav>\n </div>\n <h2>{{ currentItem.breadcrumb || currentItem.name }}</h2>\n\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"detailTabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #detailTabContent let-tab>\n @if (tab.id === '__default__') {\n @let ungroupedAttrs = ungroupedAttributes();\n @if (ungroupedAttrs.length > 0) {\n <bs-card style=\"display: block; margin: 1rem;\">\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of ungroupedAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card style=\"display: block; margin: 1rem;\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #detailAttrField let-attr let-currentItem=\"item\">\n <dt [sm]=\"3\">{{ (attr.label | resolveTranslation) || attr.name }}</dt>\n <dd [sm]=\"9\">\n @if (getDetailRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getDetailRendererInputs(rendererType, attr, currentItem)\"></ng-container>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\">\n <thead>\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ (col.label | resolveTranslation) || col.name }}</th>\n }\n </tr>\n </thead>\n <tbody class=\"align-middle\">\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n @if (getAsDetailCellRendererComponent(col); as cellRenderer) {\n <ng-container *ngComponentOutlet=\"cellRenderer; inputs: getAsDetailCellRendererInputs(cellRenderer, row, col)\"></ng-container>\n } @else if (col.dataType === 'Reference' && col.referenceType) {\n @let route = (col.referenceType | referenceLinkRoute:row[col.name]:allEntityTypes());\n @if (route) {\n <a [routerLink]=\"route\">{{ (row | asDetailCellValue:attr:col:asDetailReferenceOptions()) }}</a>\n } @else {\n {{ (row | asDetailCellValue:attr:col:asDetailReferenceOptions()) }}\n }\n } @else {\n {{ (row | asDetailCellValue:attr:col:asDetailReferenceOptions()) }}\n }\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n [indeterminate]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) == null\"\n disabled\n onclick=\"return false;\"\n style=\"opacity: 1;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded me-2\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n {{ colorVal }}\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n @let chips = (attr.name | referenceChips:item());\n @if (chips.length) {\n <div class=\"d-flex flex-wrap gap-1\">\n @for (chip of chips; track chip.id) {\n @let chipRoute = (attr.referenceType ? (attr.referenceType | referenceLinkRoute:chip.id:allEntityTypes()) : null);\n @if (chipRoute) {\n <a class=\"badge rounded-pill border bg-body-secondary text-body text-decoration-none px-3 py-2\" [routerLink]=\"chipRoute\">{{ chip.label }}</a>\n } @else {\n <span class=\"badge rounded-pill border bg-body-secondary text-body px-3 py-2\">{{ chip.label }}</span>\n }\n }\n </div>\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.referenceType) {\n @let refRoute = (attr.referenceType | referenceLinkRoute:(attr.name | rawAttributeValue:item()):allEntityTypes());\n @if (refRoute && (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes())) {\n <a [routerLink]=\"refRoute\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) }}</a>\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else if (attr.dataType === 'MultiLineString') {\n <div style=\"white-space: pre-wrap;\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}</div>\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n @if (et.queries?.length) {\n @for (queryAlias of et.queries; track queryAlias) {\n <spark-sub-query [queryId]=\"queryAlias\" [parentId]=\"currentItem.id!\" [parentType]=\"et.name\" />\n }\n }\n @if (extraContentTemplate(); as extraContentTpl) {\n <ng-container *ngTemplateOutlet=\"extraContentTpl; context: { $implicit: currentItem, entityType: et }\"></ng-container>\n }\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n", styles: [".spark-actionbar{position:sticky;top:0;z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem}.spark-actionbar .btn{border-radius:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["color", "outline"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["color", "navStyle"] }, { kind: "component", type: BsContainerComponent, selector: "bs-container" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColumnDirective, selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]", inputs: ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] }, { kind: "component", type: BsPriorityNavComponent, selector: "bs-priority-nav", inputs: ["moreLabel", "moreLabelTemplate", "collapseAt", "overflowFrom", "hideEmptyMore", "ariaLabel"], outputs: ["overflowChange"] }, { kind: "directive", type: BsPriorityNavItemDirective, selector: "[bsPriorityNavItem]", inputs: ["bsPriorityNavItem", "bsPriorityNavItemHideBelow"] }, { kind: "component", type: BsTableComponent, selector: "bs-table", inputs: ["isResponsive", "striped", "hover", "border", "ariaRowCount"] }, { kind: "component", type: BsTabControlComponent, selector: "bs-tab-control", inputs: ["border", "selectFirstTab", "tabsPosition"] }, { kind: "component", type: BsTabPageComponent, selector: "bs-tab-page", inputs: ["disabled"] }, { kind: "directive", type: BsTabPageHeaderDirective, selector: "[bsTabPageHeader]" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "component", type: SparkSubQueryComponent, selector: "spark-sub-query", inputs: ["queryId", "parentId", "parentType"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: AttributeValuePipe, name: "attributeValue" }, { kind: "pipe", type: RawAttributeValuePipe, name: "rawAttributeValue" }, { kind: "pipe", type: AsDetailColumnsPipe, name: "asDetailColumns" }, { kind: "pipe", type: AsDetailCellValuePipe, name: "asDetailCellValue" }, { kind: "pipe", type: ArrayValuePipe, name: "arrayValue" }, { kind: "pipe", type: ReferenceLinkRoutePipe, name: "referenceLinkRoute" }, { kind: "pipe", type: ReferenceChipsPipe, name: "referenceChips" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
541
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: SparkPoDetailComponent, isStandalone: true, selector: "spark-po-detail", inputs: { showCustomActions: { classPropertyName: "showCustomActions", publicName: "showCustomActions", isSignal: true, isRequired: false, transformFunction: null }, extraActionsTemplate: { classPropertyName: "extraActionsTemplate", publicName: "extraActionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, extraContentTemplate: { classPropertyName: "extraContentTemplate", publicName: "extraContentTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { edited: "edited", deleted: "deleted", customActionExecuted: "customActionExecuted" }, ngImport: i0, template: "<bs-container>\n<div class=\"container\">\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n } @else if (item(); as currentItem) {\n @if (entityType(); as et) {\n <div class=\"spark-actionbar px-3 py-2\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <button *bsPriorityNavItem=\"1\" class=\"btn btn-outline-secondary\" (click)=\"onBack()\">\n <spark-icon name=\"arrow-left\" /> {{ 'common.back' | t }}\n </button>\n @if (canEdit()) {\n <button *bsPriorityNavItem=\"2\" class=\"btn btn-primary\" (click)=\"onEdit()\">\n <spark-icon name=\"pencil\" /> {{ 'common.edit' | t }}\n </button>\n }\n @if (canDelete()) {\n <button *bsPriorityNavItem=\"3\" class=\"btn btn-danger\" (click)=\"onDelete()\">\n <spark-icon name=\"trash\" /> {{ 'common.delete' | t }}\n </button>\n }\n @if (showCustomActions()) {\n @for (action of customActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" class=\"btn btn-outline-primary\" (click)=\"onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n </bs-priority-nav>\n </div>\n <h2>{{ currentItem.breadcrumb || currentItem.name }}</h2>\n\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"detailTabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #detailTabContent let-tab>\n @if (tab.id === '__default__') {\n @let ungroupedAttrs = ungroupedAttributes();\n @if (ungroupedAttrs.length > 0) {\n <bs-card style=\"display: block; margin: 1rem;\">\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of ungroupedAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card style=\"display: block; margin: 1rem;\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #detailAttrField let-attr let-currentItem=\"item\">\n <dt [sm]=\"3\">{{ (attr.label | resolveTranslation) || attr.name }}</dt>\n <dd [sm]=\"9\">\n @if (getDetailRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getDetailRendererInputs(rendererType, attr, currentItem)\"></ng-container>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\">\n <thead>\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ (col.label | resolveTranslation) || col.name }}</th>\n }\n </tr>\n </thead>\n <tbody class=\"align-middle\">\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n @if (getAsDetailCellRendererComponent(col); as cellRenderer) {\n <ng-container *ngComponentOutlet=\"cellRenderer; inputs: getAsDetailCellRendererInputs(cellRenderer, row, col)\"></ng-container>\n } @else if (col.dataType === 'Reference' && col.referenceType) {\n @let route = (col.referenceType | referenceLinkRoute:row[col.name]:allEntityTypes());\n @if (route) {\n <a [routerLink]=\"route\">{{ (row | asDetailCellValue:attr:col:asDetailReferenceOptions()) }}</a>\n } @else {\n {{ (row | asDetailCellValue:attr:col:asDetailReferenceOptions()) }}\n }\n } @else {\n {{ (row | asDetailCellValue:attr:col:asDetailReferenceOptions()) }}\n }\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n [indeterminate]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) == null\"\n disabled\n onclick=\"return false;\"\n style=\"opacity: 1;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded me-2\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n {{ colorVal }}\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n @let chips = (attr.name | referenceChips:item());\n @if (chips.length) {\n <div class=\"d-flex flex-wrap gap-1\">\n @for (chip of chips; track chip.id) {\n @let chipRoute = (attr.referenceType ? (attr.referenceType | referenceLinkRoute:chip.id:allEntityTypes()) : null);\n @if (chipRoute) {\n <a class=\"badge rounded-pill border bg-body-secondary text-body text-decoration-none px-3 py-2\" [routerLink]=\"chipRoute\">{{ chip.label }}</a>\n } @else {\n <span class=\"badge rounded-pill border bg-body-secondary text-body px-3 py-2\">{{ chip.label }}</span>\n }\n }\n </div>\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.referenceType) {\n @let refRoute = (attr.referenceType | referenceLinkRoute:(attr.name | rawAttributeValue:item()):allEntityTypes());\n @if (refRoute && (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes())) {\n <a [routerLink]=\"refRoute\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) }}</a>\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else if (attr.dataType === 'MultiLineString') {\n <div style=\"white-space: pre-wrap;\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}</div>\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n @if (et.queries?.length) {\n @for (queryAlias of et.queries; track queryAlias) {\n <spark-sub-query [queryId]=\"queryAlias\" [parentId]=\"currentItem.id!\" [parentType]=\"et.name\" />\n }\n }\n @if (extraContentTemplate(); as extraContentTpl) {\n <ng-container *ngTemplateOutlet=\"extraContentTpl; context: { $implicit: currentItem, entityType: et }\"></ng-container>\n }\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n", styles: [".spark-actionbar{position:sticky;top:0;z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem}.spark-actionbar .btn{border-radius:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["color", "outline"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["color", "navStyle"] }, { kind: "component", type: BsContainerComponent, selector: "bs-container" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColumnDirective, selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]", inputs: ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] }, { kind: "component", type: BsPriorityNavComponent, selector: "bs-priority-nav", inputs: ["moreLabel", "moreLabelTemplate", "collapseAt", "overflowFrom", "hideEmptyMore", "ariaLabel"], outputs: ["overflowChange"] }, { kind: "directive", type: BsPriorityNavItemDirective, selector: "[bsPriorityNavItem]", inputs: ["bsPriorityNavItem", "bsPriorityNavItemHideBelow"] }, { kind: "component", type: BsTableComponent, selector: "bs-table", inputs: ["isResponsive", "striped", "hover", "border", "ariaRowCount"] }, { kind: "component", type: BsTabControlComponent, selector: "bs-tab-control", inputs: ["border", "selectFirstTab", "tabsPosition"] }, { kind: "component", type: BsTabPageComponent, selector: "bs-tab-page", inputs: ["disabled"] }, { kind: "directive", type: BsTabPageHeaderDirective, selector: "[bsTabPageHeader]" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "component", type: SparkSubQueryComponent, selector: "spark-sub-query", inputs: ["queryId", "parentId", "parentType", "reloadToken", "showCard", "headerTemplate"], outputs: ["error"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: AttributeValuePipe, name: "attributeValue" }, { kind: "pipe", type: RawAttributeValuePipe, name: "rawAttributeValue" }, { kind: "pipe", type: AsDetailColumnsPipe, name: "asDetailColumns" }, { kind: "pipe", type: AsDetailCellValuePipe, name: "asDetailCellValue" }, { kind: "pipe", type: ArrayValuePipe, name: "arrayValue" }, { kind: "pipe", type: ReferenceLinkRoutePipe, name: "referenceLinkRoute" }, { kind: "pipe", type: ReferenceChipsPipe, name: "referenceChips" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
385
542
  }
386
543
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkPoDetailComponent, decorators: [{
387
544
  type: Component,