@mintplayer/ng-spark 22.25.0 → 22.27.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.
- package/README.md +6 -4
- package/fesm2022/mintplayer-ng-spark-grid.mjs +71 -44
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs +16 -7
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-create.mjs +17 -12
- package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +28 -13
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +9 -2
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +67 -13
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +18 -2
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +2 -0
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-services.mjs +12 -9
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +4 -3
- package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -1
- package/package.json +3 -3
- package/types/mintplayer-ng-spark-grid.d.ts +47 -32
- package/types/mintplayer-ng-spark-models.d.ts +62 -27
- package/types/mintplayer-ng-spark-po-create.d.ts +10 -0
- package/types/mintplayer-ng-spark-po-detail.d.ts +8 -2
- package/types/mintplayer-ng-spark-po-form.d.ts +20 -0
- package/types/mintplayer-ng-spark-query-list.d.ts +11 -1
- package/types/mintplayer-ng-spark-services.d.ts +42 -8
- package/types/mintplayer-ng-spark-soft-delete.d.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-po-create.mjs","names":[],"sources":["../../po-create/src/spark-po-create.component.ts","../../po-create/src/spark-po-create.component.html"],"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 isDateDataType,\n fromDateInputValue,\n toDateInputValue,\n RefreshOverlay,\n applyOverlay,\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 /** Bound two-way to the form. See {@link getEditableAttributes}. */\n refreshOverlay = signal<RefreshOverlay>({});\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.allEntityTypes.set(types);\n this.initFormData(entityType);\n if (entityType) await this.applyServerDefaults(entityType);\n this.entityType.set(entityType);\n }\n\n /**\n * The blank object comes from the server (#460, D19): `/po/new` runs the type's `OnNewAsync`, so a\n * default a hook sets — and, for a New started from a sub-query, the reference to the parent the\n * base hook fills — reaches the form. The sub-query card passes the parent as query parameters\n * (`parentId`, `parentType`, `queryId`), so it survives the navigation and a reload of this page.\n *\n * A refusal (the parent is gone, or not the caller's to see) is shown and the client-side blank\n * form stays usable, exactly as it was before the round-trip existed.\n */\n private async applyServerDefaults(entityType: EntityType): Promise<void> {\n const query = this.route.snapshot.queryParamMap;\n const parentId = query.get('parentId') ?? undefined;\n const parentType = query.get('parentType') ?? undefined;\n const queryId = query.get('queryId') ?? undefined;\n const fromSubQuery = !!(parentId && parentType && queryId);\n\n try {\n const po = await this.sparkService.newObject(this.type(), fromSubQuery ? { parentId, parentType, queryId } : undefined);\n const editable = new Map(this.getEditableAttributes(entityType).map(a => [a.name, a] as const));\n const data = { ...this.formData() };\n for (const attr of po?.attributes ?? []) {\n const definition = editable.get(attr.name);\n if (!definition || attr.value === null || attr.value === undefined || definition.dataType === 'AsDetail') continue;\n data[attr.name] = isDateDataType(definition.dataType)\n ? toDateInputValue(definition.dataType, attr.value)\n : attr.value;\n }\n this.formData.set(data);\n } catch (e) {\n const error = e as HttpErrorResponse;\n const errors = error.error?.result?.errors ?? error.error?.errors;\n this.validationErrors.set(errors ?? [{\n attributeName: '',\n errorMessage: { en: error.message || 'The new item could not be prepared.' },\n ruleType: 'error'\n }]);\n }\n }\n\n initFormData(entityType: EntityType | null = this.entityType()): void {\n const data: Record<string, any> = {};\n this.getEditableAttributes(entityType).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' && !attr.lookupReferenceType) {\n // Same reason as spark-po-edit: a lookup-backed boolean's third state is the absence of a\n // value, and seeding `false` here would make every new object explicitly opted out.\n data[attr.name] = false;\n } else {\n data[attr.name] = '';\n }\n });\n this.formData.set(data);\n }\n\n /**\n * ⚠️ Overlaid before filtering — same reason as <c>spark-po-edit</c>, and worse here: this list is\n * the <em>only</em> source of the create payload, so an attribute a refresh hook revealed was not\n * merely sent stale, it was absent from the new object entirely.\n */\n getEditableAttributes(entityType: EntityType | null = this.entityType()) {\n const overlay = this.refreshOverlay();\n return entityType?.attributes\n .map(a => applyOverlay(a, overlay[a.name]))\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 // A date control hands back a bare local wall clock; the wire needs a complete ISO-8601\n // instant carrying the viewer's offset for the entered date.\n value: isDateDataType(attr.dataType)\n ? fromDateInputValue(attr.dataType, this.formData()[attr.name])\n : 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 // The errors live under `result` (every endpoint wraps its body in a ClientOperationEnvelope:\n // `{ result: { errors: [...] }, operations: [] }`), as in po-edit. Reading only\n // `error.error.errors` showed every create-time refusal as the raw Angular HTTP string.\n const errors = error.error?.result?.errors ?? error.error?.errors;\n if (error.status === 400 && errors) {\n this.validationErrors.set(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 [(refreshOverlay)]=\"refreshOverlay\"\n [objectTypeId]=\"type()\"\n [parentType]=\"type()\"\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"],"mappings":";;;;;;;;;;;;;AAkCA,IAAa,yBAAb,MAAa,uBAAsB;CAChB,QAAQ,OAAO,cAAc;CAC7B,SAAS,OAAO,MAAM;CACtB,eAAe,OAAO,YAAY;CAEnD,QAAQ,OAAM;CACd,YAAY,OAAM;CAElB,SAAS;CACT,aAAa,OAA0B,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAC5C,OAAO,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACjB,WAAW,OAA4B,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAE1C,iBAAiB,OAAuB,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAC3C,mBAAmB,OAA0B,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAChD,WAAW,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAChB,iBAAiB,OAAqB,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CACjD,gBAAgB,eAAe,KAAK,iBAAgB,CAAE,CAAC,QAAO,MAAK,CAAC,EAAE,aAAa,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAErF,cAAA;EACE,KAAK,MAAM,SAAS,KAAK,mBAAkB,CAAE,CAAC,CAAC,WAAU,WAAU,KAAK,eAAe,MAAM,CAAC;CAChG;CAEQ,MAAM,eAAe,QAAW;EACtC,KAAK,KAAK,IAAI,OAAO,IAAI,MAAM,KAAK,EAAE;EACtC,MAAM,QAAQ,MAAM,KAAK,aAAa,eAAc;EACpD,MAAM,aAAa,MAAM,MAAK,MAAK,EAAE,OAAO,KAAK,KAAI,KAAM,EAAE,UAAU,KAAK,KAAI,CAAE,KAAK;EACvF,KAAK,eAAe,IAAI,KAAK;EAC7B,KAAK,aAAa,UAAU;EAC5B,IAAI,YAAY,MAAM,KAAK,oBAAoB,UAAU;EACzD,KAAK,WAAW,IAAI,UAAU;CAChC;CAWQ,MAAM,oBAAoB,YAAsB;EACtD,MAAM,QAAQ,KAAK,MAAM,SAAS;EAClC,MAAM,WAAW,MAAM,IAAI,UAAU,KAAK;EAC1C,MAAM,aAAa,MAAM,IAAI,YAAY,KAAK;EAC9C,MAAM,UAAU,MAAM,IAAI,SAAS,KAAK;EACxC,MAAM,eAAe,CAAC,EAAE,YAAY,cAAc;EAElD,IAAI;GACF,MAAM,KAAK,MAAM,KAAK,aAAa,UAAU,KAAK,KAAI,GAAI,eAAe;IAAE;IAAU;IAAY;GAAO,IAAK,MAAS;GACtH,MAAM,WAAW,IAAI,IAAI,KAAK,sBAAsB,UAAU,CAAC,CAAC,KAAI,MAAK,CAAC,EAAE,MAAM,CAAC,CAAU,CAAC;GAC9F,MAAM,OAAO,EAAE,GAAG,KAAK,SAAQ,EAAE;GACjC,KAAK,MAAM,QAAQ,IAAI,cAAc,CAAA,GAAI;IACvC,MAAM,aAAa,SAAS,IAAI,KAAK,IAAI;IACzC,IAAI,CAAC,cAAc,KAAK,UAAU,QAAQ,KAAK,UAAU,UAAa,WAAW,aAAa,YAAY;IAC1G,KAAK,KAAK,QAAQ,eAAe,WAAW,QAAQ,IAChD,iBAAiB,WAAW,UAAU,KAAK,KAAK,IAChD,KAAK;GACX;GACA,KAAK,SAAS,IAAI,IAAI;EACxB,SAAS,GAAG;GACV,MAAM,QAAQ;GACd,MAAM,SAAS,MAAM,OAAO,QAAQ,UAAU,MAAM,OAAO;GAC3D,KAAK,iBAAiB,IAAI,UAAU,CAAC;IACnC,eAAe;IACf,cAAc,EAAE,IAAI,MAAM,WAAW,sCAAqC;IAC1E,UAAU;GACX,CAAA,CAAC;EACJ;CACF;CAEA,aAAa,aAAgC,KAAK,WAAU,GAAE;EAC5D,MAAM,OAA4B,CAAA;EAClC,KAAK,sBAAsB,UAAU,CAAC,CAAC,SAAQ,SAAO;GACpD,IAAI,KAAK,aAAa,aACpB,KAAK,KAAK,QAAQ;QACb,IAAI,KAAK,aAAa,YAC3B,KAAK,KAAK,QAAQ,KAAK,UAAU,CAAA,IAAK,CAAA;QACjC,IAAI,KAAK,aAAa,aAAa,CAAC,KAAK,qBAG9C,KAAK,KAAK,QAAQ;QAElB,KAAK,KAAK,QAAQ;EAEtB,CAAC;EACD,KAAK,SAAS,IAAI,IAAI;CACxB;CAOA,sBAAsB,aAAgC,KAAK,WAAU,GAAE;EACrE,MAAM,UAAU,KAAK,eAAc;EACnC,OAAO,YAAY,WAChB,KAAI,MAAK,aAAa,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC,CAC1C,QAAO,MAAK,EAAE,aAAa,CAAC,EAAE,cAAc,gBAAgB,EAAE,UAAU,SAAS,gBAAgB,CAAC,CAAC,CACnG,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,KAAK,CAAA;CAC1C;CAEA,MAAM,SAAM;EACV,IAAI,CAAC,KAAK,WAAU,GAAI;EAExB,KAAK,iBAAiB,IAAI,CAAA,CAAE;EAC5B,KAAK,SAAS,IAAI,IAAI;EAEtB,MAAM,YAAgC,YAAY,KAAK,eAAc,CAAE,CAAC,MAAK,MAAK,EAAE,YAAY,OAAO;EACvG,MAAM,aAA0C,KAAK,sBAAqB,CAAE,CAAC,KAAI,SAAO;GACtF,MAAM,OAAkC;IACtC,IAAI,KAAK;IACT,MAAM,KAAK;IAGX,OAAO,eAAe,KAAK,QAAQ,IAC/B,mBAAmB,KAAK,UAAU,KAAK,SAAQ,CAAE,CAAC,KAAK,KAAK,IAC5D,KAAK,SAAQ,CAAE,CAAC,KAAK;IACzB,UAAU,KAAK;IACf,SAAS,KAAK;IACd,YAAY,KAAK;IACjB,WAAW,KAAK;IAChB,YAAY,KAAK;IACjB,gBAAgB;IAChB,OAAO,KAAK;IACZ,OAAO,KAAK;GACb;GAID,IAAI,KAAK,aAAa,cAAc,KAAK,cAAc;IACrD,MAAM,aAAa,SAAS,KAAK,YAAY;IAC7C,IAAI,YAAY;KACd,MAAM,MAAM,KAAK,SAAQ,CAAE,CAAC,KAAK;KACjC,KAAK,QAAQ;KACb,KAAK,eAAe,KAAK;KACzB,IAAI,KAAK,SAAS;MAEhB,KAAK,WADgB,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA,EACjC,CAAM,KAAI,SAAQ,eAAgB,QAAQ,CAAA,GAA4B,YAAY,QAAQ,CAAC;MAC1G,KAAK,SAAS;KAChB,OAAO;MACL,KAAK,SAAS,MAAM,eAAe,KAA4B,YAAY,QAAQ,IAAI;MACvF,KAAK,UAAU;KACjB;IACF;GACF;GACA,OAAO;EACT,CAAC;EAED,MAAM,KAAgC;GACpC,MAAM,KAAK,SAAQ,CAAE,CAAC,WAAW;GACjC,cAAc,KAAK,WAAU,CAAG,CAAC;GACjC;EACD;EAED,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,aAAa,OAAO,KAAK,KAAI,GAAI,EAAE;GAC7D,KAAK,SAAS,IAAI,KAAK;GACvB,KAAK,MAAM,KAAK,MAAM;GACtB,KAAK,OAAO,SAAS;IAAC;IAAO,KAAK,KAAI;IAAI,OAAO;GAAE,CAAC;EACtD,SAAS,GAAG;GACV,KAAK,SAAS,IAAI,KAAK;GACvB,MAAM,QAAQ;GAId,MAAM,SAAS,MAAM,OAAO,QAAQ,UAAU,MAAM,OAAO;GAC3D,IAAI,MAAM,WAAW,OAAO,QAC1B,KAAK,iBAAiB,IAAI,MAAM;QAEhC,KAAK,iBAAiB,IAAI,CAAC;IACzB,eAAe;IACf,cAAc,EAAE,IAAI,MAAM,WAAW,+BAA8B;IACnE,UAAU;GACX,CAAA,CAAC;EAEN;CACF;CAEA,WAAQ;EACN,KAAK,UAAU,KAAI;EACnB,OAAO,QAAQ,KAAI;CACrB;CAvLW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAsB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAtB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAsB,cAAA;EAAA,UAAA;EAAA,SAAA;GAAA,OAAA;GAAA,WAAA;EAAA;EAAA,UAAA;EAAA,UClCnC;EA8BA,cAAA;GAAA;IAAA,MAAA;IAAA,MDAY;GAAY;GAAA;IAAA,MAAA;IAAA,MAAE;IAAgB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,mBAAA,qBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAoB,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAkB,UAAA;IAAA,QAAA,CAAA,QAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAoB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAsB,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAgB,MAAA;GAAA;EAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAIvI,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAsB,YAAA,CAAA;EANlC,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAiB,SAClB;IAAC;IAAc;IAAkB;IAAsB;IAAoB;IAAsB;IAAwB;GAAgB;GAAC,iBAElI,wBAAwB;GAAM,UAAA;EAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-po-create.mjs","names":[],"sources":["../../po-create/src/spark-po-create.component.ts","../../po-create/src/spark-po-create.component.html"],"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 isDateDataType,\n fromDateInputValue,\n toDateInputValue,\n RefreshOverlay,\n applyOverlay,\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 /** Bound two-way to the form. See {@link getEditableAttributes}. */\n refreshOverlay = signal<RefreshOverlay>({});\n validationErrors = signal<ValidationError[]>([]);\n isSaving = signal(false);\n private allEntityTypes = signal<EntityType[]>([]);\n generalErrors = computed(() => this.validationErrors().filter(e => !e.attributeName));\n /**\n * The parent a sub-query card's New passed in the URL (`parentId`, `parentType`, `queryId`), or\n * null. Forwarded to the form so a Reference attribute's option query runs under the object the\n * New was started from — the same parent `/po/new` receives.\n */\n subQueryParent = signal<{ parentId: string; parentType: string; queryId: string } | null>(null);\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.allEntityTypes.set(types);\n this.initFormData(entityType);\n if (entityType) await this.applyServerDefaults(entityType);\n this.entityType.set(entityType);\n }\n\n /**\n * The blank object comes from the server (#460, D19): `/po/new` runs the type's `OnNewAsync`, so a\n * default a hook sets — and, for a New started from a sub-query, the reference to the parent the\n * base hook fills — reaches the form. The sub-query card passes the parent as query parameters\n * (`parentId`, `parentType`, `queryId`), so it survives the navigation and a reload of this page.\n *\n * A refusal (the parent is gone, or not the caller's to see) is shown and the client-side blank\n * form stays usable, exactly as it was before the round-trip existed.\n */\n private async applyServerDefaults(entityType: EntityType): Promise<void> {\n const query = this.route.snapshot.queryParamMap;\n const parentId = query.get('parentId');\n const parentType = query.get('parentType');\n const queryId = query.get('queryId');\n const subQueryParent = parentId && parentType && queryId ? { parentId, parentType, queryId } : null;\n this.subQueryParent.set(subQueryParent);\n\n try {\n const po = await this.sparkService.newObject(this.type(), subQueryParent ?? undefined);\n const editable = new Map(this.getEditableAttributes(entityType).map(a => [a.name, a] as const));\n const data = { ...this.formData() };\n for (const attr of po?.attributes ?? []) {\n const definition = editable.get(attr.name);\n if (!definition || attr.value === null || attr.value === undefined || definition.dataType === 'AsDetail') continue;\n data[attr.name] = isDateDataType(definition.dataType)\n ? toDateInputValue(definition.dataType, attr.value)\n : attr.value;\n }\n this.formData.set(data);\n } catch (e) {\n const error = e as HttpErrorResponse;\n const errors = error.error?.result?.errors ?? error.error?.errors;\n this.validationErrors.set(errors ?? [{\n attributeName: '',\n errorMessage: { en: error.message || 'The new item could not be prepared.' },\n ruleType: 'error'\n }]);\n }\n }\n\n initFormData(entityType: EntityType | null = this.entityType()): void {\n const data: Record<string, any> = {};\n this.getEditableAttributes(entityType).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' && !attr.lookupReferenceType) {\n // Same reason as spark-po-edit: a lookup-backed boolean's third state is the absence of a\n // value, and seeding `false` here would make every new object explicitly opted out.\n data[attr.name] = false;\n } else {\n data[attr.name] = '';\n }\n });\n this.formData.set(data);\n }\n\n /**\n * ⚠️ Overlaid before filtering — same reason as <c>spark-po-edit</c>, and worse here: this list is\n * the <em>only</em> source of the create payload, so an attribute a refresh hook revealed was not\n * merely sent stale, it was absent from the new object entirely.\n */\n getEditableAttributes(entityType: EntityType | null = this.entityType()) {\n const overlay = this.refreshOverlay();\n return entityType?.attributes\n .map(a => applyOverlay(a, overlay[a.name]))\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 // A date control hands back a bare local wall clock; the wire needs a complete ISO-8601\n // instant carrying the viewer's offset for the entered date.\n value: isDateDataType(attr.dataType)\n ? fromDateInputValue(attr.dataType, this.formData()[attr.name])\n : 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, this.subQueryParent() ?? undefined);\n this.isSaving.set(false);\n // A server interceptor cancelled the create (the user answered its prompt with Cancel): nothing was\n // created, so the form stays as it is.\n if (!result) return;\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 // The errors live under `result` (every endpoint wraps its body in a ClientOperationEnvelope:\n // `{ result: { errors: [...] }, operations: [] }`), as in po-edit. Reading only\n // `error.error.errors` showed every create-time refusal as the raw Angular HTTP string.\n const errors = error.error?.result?.errors ?? error.error?.errors;\n if (error.status === 400 && errors) {\n this.validationErrors.set(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.label | 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 [(refreshOverlay)]=\"refreshOverlay\"\n [objectTypeId]=\"type()\"\n [parentType]=\"type()\"\n [referenceParentId]=\"subQueryParent()?.parentId\"\n [referenceParentType]=\"subQueryParent()?.parentType\"\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"],"mappings":"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"}
|
|
@@ -22,7 +22,7 @@ import { SparkIconComponent } from "@mintplayer/ng-spark/icon";
|
|
|
22
22
|
import { SparkAttributeDescriptionComponent } from "@mintplayer/ng-spark/attribute-description";
|
|
23
23
|
import { SparkGridCellComponent, SparkQueryCardComponent } from "@mintplayer/ng-spark/grid";
|
|
24
24
|
import { SPARK_ATTRIBUTE_RENDERERS, rendererValue, rowRendererInputs, withDeclaredInputs } from "@mintplayer/ng-spark/renderers";
|
|
25
|
-
import { ShowedOn, filterDetailActions, hasShowedOnFlag, subQueriesOf } from "@mintplayer/ng-spark/models";
|
|
25
|
+
import { ShowedOn, builtInAction, confirmationText, filterDetailActions, hasShowedOnFlag, isShowedOnDetail, subQueriesOf } from "@mintplayer/ng-spark/models";
|
|
26
26
|
import { SPARK_DETAIL_ACTIONS, SPARK_DETAIL_PANELS, orderSparkExtensions, parseSparkDeletedParam } from "@mintplayer/ng-spark/panels";
|
|
27
27
|
import { combineLatest } from "rxjs";
|
|
28
28
|
var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
@@ -56,8 +56,10 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
56
56
|
asDetailReferenceOptions = signal({}, ...ngDevMode ? [{ debugName: "asDetailReferenceOptions" }] : /* istanbul ignore next */ []);
|
|
57
57
|
type = "";
|
|
58
58
|
id = "";
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
editAction = signal(null, ...ngDevMode ? [{ debugName: "editAction" }] : /* istanbul ignore next */ []);
|
|
60
|
+
deleteAction = signal(null, ...ngDevMode ? [{ debugName: "deleteAction" }] : /* istanbul ignore next */ []);
|
|
61
|
+
canEdit = computed(() => this.editAction() !== null, ...ngDevMode ? [{ debugName: "canEdit" }] : /* istanbul ignore next */ []);
|
|
62
|
+
canDelete = computed(() => this.deleteAction() !== null, ...ngDevMode ? [{ debugName: "canDelete" }] : /* istanbul ignore next */ []);
|
|
61
63
|
customActions = signal([], ...ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []);
|
|
62
64
|
permissions = signal(null, ...ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []);
|
|
63
65
|
deletedMode = signal(null, ...ngDevMode ? [{ debugName: "deletedMode" }] : /* istanbul ignore next */ []);
|
|
@@ -150,8 +152,10 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
150
152
|
const withheld = new Set((item.disabledActions ?? []).map((name) => name.toLowerCase()));
|
|
151
153
|
const deletedView = this.isDeletedView();
|
|
152
154
|
this.permissions.set(permissions);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
const edit = builtInAction(actions, "Edit");
|
|
156
|
+
const del = builtInAction(actions, "Delete");
|
|
157
|
+
this.editAction.set(edit && isShowedOnDetail(edit.showedOn) && !deletedView && (can ? can.edit : permissions.canEdit) && !withheld.has("edit") && !withheld.has("save") ? edit : null);
|
|
158
|
+
this.deleteAction.set(del && isShowedOnDetail(del.showedOn) && !deletedView && (can ? can.delete : permissions.canDelete) && !withheld.has("delete") ? del : null);
|
|
155
159
|
this.customActions.set(deletedView ? [] : filterDetailActions(actions));
|
|
156
160
|
}
|
|
157
161
|
} catch (e) {
|
|
@@ -266,6 +270,16 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
266
270
|
}
|
|
267
271
|
this.asDetailTypes.set(newAsDetailTypes);
|
|
268
272
|
}
|
|
273
|
+
builtInActionClass(action, fallback) {
|
|
274
|
+
const variant = action.variant?.toLowerCase();
|
|
275
|
+
return [
|
|
276
|
+
"danger",
|
|
277
|
+
"warning",
|
|
278
|
+
"primary",
|
|
279
|
+
"secondary",
|
|
280
|
+
"success"
|
|
281
|
+
].includes(variant ?? "") ? `btn btn-${variant}` : `btn btn-${fallback}`;
|
|
282
|
+
}
|
|
269
283
|
customActionClass(action) {
|
|
270
284
|
const variant = action.variant?.toLowerCase();
|
|
271
285
|
switch (variant) {
|
|
@@ -280,10 +294,8 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
280
294
|
runningAction = signal(null, ...ngDevMode ? [{ debugName: "runningAction" }] : /* istanbul ignore next */ []);
|
|
281
295
|
async onCustomAction(action) {
|
|
282
296
|
if (this.runningAction()) return;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (!confirm(message)) return;
|
|
286
|
-
}
|
|
297
|
+
const message = confirmationText(action, 1);
|
|
298
|
+
if (message && !confirm(message)) return;
|
|
287
299
|
this.runningAction.set(action.name);
|
|
288
300
|
try {
|
|
289
301
|
await this.sparkService.executeCustomAction(this.type, action.name, this.item() || void 0);
|
|
@@ -313,8 +325,11 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
313
325
|
]);
|
|
314
326
|
}
|
|
315
327
|
async onDelete() {
|
|
316
|
-
|
|
317
|
-
|
|
328
|
+
const etag = this.item()?.etag;
|
|
329
|
+
if (!etag) return;
|
|
330
|
+
const message = confirmationText(this.deleteAction(), 1);
|
|
331
|
+
if (!message || confirm(message)) {
|
|
332
|
+
await this.sparkService.delete(this.type, this.id, etag);
|
|
318
333
|
this.deleted.emit();
|
|
319
334
|
await this.returnNavigation.returnToList(this.entityType()?.name);
|
|
320
335
|
}
|
|
@@ -386,7 +401,7 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
386
401
|
customActionExecuted: "customActionExecuted"
|
|
387
402
|
},
|
|
388
403
|
ngImport: i0,
|
|
389
|
-
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 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 visibleCustomActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\" [disabled]=\"runningAction() !== null\" (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 <!-- Add-on actions (SPARK_DETAIL_ACTIONS): each decides for itself whether it applies. -->\n @if (detailContext(); as ctx) {\n @for (action of detailActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n <h2>\n {{ currentItem.breadcrumb || currentItem.name }}\n @if (isDeletedView()) {\n <bs-badge [type]=\"colors.secondary\" class=\"align-middle ms-2 spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </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 class=\"d-block m-3\">\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 class=\"d-block m-3\">\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 }}<spark-attribute-description [description]=\"attr.description\" /></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():rendererRegistry); track col.name) {\n <th>{{ (col.label | resolveTranslation) || col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n </tr>\n </thead>\n <tbody class=\"align-middle\">\n @let rowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <td>\n <!--\n Shared with the query grid. This cell used to hand-roll the renderer\n dispatch and print everything else as text, so a `boolean` column read\n \"true\" here and rendered a checkbox in a grid, and a `color` showed its\n hex. Only the VALUE is resolved locally: these rows are plain\n dictionaries, not PersistentObjects.\n\n `chips` is left unset for a reference array on purpose — an AsDetail\n row's `__sparkBreadcrumbs` is keyed by column, not by id, so there are\n no labels for the members. Text beats a row of raw ids.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(row | asDetailCellValue:attr:col:asDetailReferenceOptions())\"\n [rendererValue]=\"row[col.name]\"\n [item]=\"row\"\n [link]=\"col.dataType === 'Reference' && col.referenceType\n ? (col.referenceType | referenceLinkRoute:row[col.name]:allEntityTypes())\n : null\" />\n <!-- A row renderer (rowComponent: contributions' attribution line) draws\n its attributes once, under the row's first cell, instead of as columns. -->\n @if (firstCol && rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row, currentItem)\"></ng-container>\n </div>\n }\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).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 class=\"opacity-100\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded me-2\" [style.background-color]=\"colorVal\"></span>\n {{ colorVal }}\n } @else {\n -\n }\n } @else if (attr.dataType === 'image') {\n @let imageSrc = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (imageSrc) {\n <img [src]=\"imageSrc\" [alt]=\"(attr.label | resolveTranslation) || attr.name\" loading=\"lazy\"\n class=\"img-fluid rounded\" style=\"max-height: 12rem;\">\n } @else {\n -\n }\n } @else if (attr.dataType === 'url') {\n @let hrefVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (hrefVal) {\n <a [href]=\"hrefVal\" target=\"_blank\" rel=\"noopener noreferrer\">{{ hrefVal }}</a>\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=\"spark-chip\" [routerLink]=\"chipRoute\">{{ chip.label }}</a>\n } @else {\n <span class=\"spark-chip\">{{ 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 class=\"spark-multiline-value\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}</div>\n } @else if (attr.dataType === 'date' || attr.dataType === 'datetime') {\n <!--\n Formatted the same way, and in the same timezone, as the grid renders it.\n\n Before this, a detail page printed the raw wire value — `2026-12-31T23:59:00-08:00`\n in a definition list — while the grid showed `01/01/2027, 08:59` for the same\n document. Not a rounding difference: a DateTimeOffset whose offset differs from the\n viewer's can disagree on the DATE, so the two pages named different days for one car.\n\n The grid's meaning wins because it is the one a reader can act on: a timestamp is an\n instant, shown in the viewer's own zone. The originating offset is still on the wire\n for code that needs it — server-side logic, exports, \"which country's morning was\n this?\" — it simply is not what a viewer is shown by default.\n -->\n @let when = (attr.name | rawAttributeValue:item()) | parsedDate;\n @if (when) {\n {{ when | date:(attr.dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n @for (subQuery of subQueries(); track subQuery.query) {\n <!-- The entry's selectionMode overrides the query's own for this parent (#460, D17). -->\n <spark-query-card [queryId]=\"subQuery.query\" [parentId]=\"currentItem.id!\" [parentType]=\"et.name\"\n [selectionMode]=\"subQuery.selectionMode ?? null\"\n [parentDeleted]=\"isDeletedView()\"\n [iconTemplate]=\"queryIconTemplate()\" [captionTemplate]=\"queryCaptionTemplate()\"\n [actionsTemplate]=\"queryActionsTemplate()\" />\n }\n @if (extraContentTemplate(); as extraContentTpl) {\n <ng-container *ngTemplateOutlet=\"extraContentTpl; context: { $implicit: currentItem, entityType: et }\"></ng-container>\n }\n <!-- Add-on panels (SPARK_DETAIL_PANELS), e.g. <spark-po-history>. -->\n @if (detailContext(); as ctx) {\n @for (panel of detailPanels; track panel.id) {\n <div class=\"spark-detail-panel m-3\">\n <ng-container *ngComponentOutlet=\"panel.component; inputs: { context: ctx }\" />\n </div>\n }\n }\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n",
|
|
404
|
+
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 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 <!-- Edit and Delete are the composed catalogue's (#467, D8): label, icon and variant included. -->\n @if (editAction(); as edit) {\n <button *bsPriorityNavItem=\"2\" [class]=\"builtInActionClass(edit, 'primary')\" (click)=\"onEdit()\">\n @if (edit.icon) { <spark-icon [name]=\"edit.icon\" /> } {{ edit.label | resolveTranslation }}\n </button>\n }\n @if (deleteAction(); as del) {\n <button *bsPriorityNavItem=\"3\" [class]=\"builtInActionClass(del, 'danger')\" (click)=\"onDelete()\">\n @if (del.icon) { <spark-icon [name]=\"del.icon\" /> } {{ del.label | resolveTranslation }}\n </button>\n }\n @if (showCustomActions()) {\n @for (action of visibleCustomActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\" [disabled]=\"runningAction() !== null\" (click)=\"onCustomAction(action)\">\n {{ action.label | 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 <!-- Add-on actions (SPARK_DETAIL_ACTIONS): each decides for itself whether it applies. -->\n @if (detailContext(); as ctx) {\n @for (action of detailActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n <h2>\n {{ currentItem.breadcrumb || currentItem.name }}\n @if (isDeletedView()) {\n <bs-badge [type]=\"colors.secondary\" class=\"align-middle ms-2 spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </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 class=\"d-block m-3\">\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 class=\"d-block m-3\">\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 }}<spark-attribute-description [description]=\"attr.description\" /></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():rendererRegistry); track col.name) {\n <th>{{ (col.label | resolveTranslation) || col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n </tr>\n </thead>\n <tbody class=\"align-middle\">\n @let rowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <td>\n <!--\n Shared with the query grid. This cell used to hand-roll the renderer\n dispatch and print everything else as text, so a `boolean` column read\n \"true\" here and rendered a checkbox in a grid, and a `color` showed its\n hex. Only the VALUE is resolved locally: these rows are plain\n dictionaries, not PersistentObjects.\n\n `chips` is left unset for a reference array on purpose — an AsDetail\n row's `__sparkBreadcrumbs` is keyed by column, not by id, so there are\n no labels for the members. Text beats a row of raw ids.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(row | asDetailCellValue:attr:col:asDetailReferenceOptions())\"\n [rendererValue]=\"row[col.name]\"\n [item]=\"row\"\n [link]=\"col.dataType === 'Reference' && col.referenceType\n ? (col.referenceType | referenceLinkRoute:row[col.name]:allEntityTypes())\n : null\" />\n <!-- A row renderer (rowComponent: contributions' attribution line) draws\n its attributes once, under the row's first cell, instead of as columns. -->\n @if (firstCol && rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row, currentItem)\"></ng-container>\n </div>\n }\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).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 class=\"opacity-100\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded me-2\" [style.background-color]=\"colorVal\"></span>\n {{ colorVal }}\n } @else {\n -\n }\n } @else if (attr.dataType === 'image') {\n @let imageSrc = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (imageSrc) {\n <img [src]=\"imageSrc\" [alt]=\"(attr.label | resolveTranslation) || attr.name\" loading=\"lazy\"\n class=\"img-fluid rounded\" style=\"max-height: 12rem;\">\n } @else {\n -\n }\n } @else if (attr.dataType === 'url') {\n @let hrefVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (hrefVal) {\n <a [href]=\"hrefVal\" target=\"_blank\" rel=\"noopener noreferrer\">{{ hrefVal }}</a>\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=\"spark-chip\" [routerLink]=\"chipRoute\">{{ chip.label }}</a>\n } @else {\n <span class=\"spark-chip\">{{ 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 class=\"spark-multiline-value\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}</div>\n } @else if (attr.dataType === 'date' || attr.dataType === 'datetime') {\n <!--\n Formatted the same way, and in the same timezone, as the grid renders it.\n\n Before this, a detail page printed the raw wire value — `2026-12-31T23:59:00-08:00`\n in a definition list — while the grid showed `01/01/2027, 08:59` for the same\n document. Not a rounding difference: a DateTimeOffset whose offset differs from the\n viewer's can disagree on the DATE, so the two pages named different days for one car.\n\n The grid's meaning wins because it is the one a reader can act on: a timestamp is an\n instant, shown in the viewer's own zone. The originating offset is still on the wire\n for code that needs it — server-side logic, exports, \"which country's morning was\n this?\" — it simply is not what a viewer is shown by default.\n -->\n @let when = (attr.name | rawAttributeValue:item()) | parsedDate;\n @if (when) {\n {{ when | date:(attr.dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n @for (subQuery of subQueries(); track subQuery.query) {\n <!-- The entry's selectionMode overrides the query's own for this parent (#460, D17). -->\n <spark-query-card [queryId]=\"subQuery.query\" [parentId]=\"currentItem.id!\" [parentType]=\"et.name\"\n [selectionMode]=\"subQuery.selectionMode ?? null\"\n [parentDeleted]=\"isDeletedView()\"\n [iconTemplate]=\"queryIconTemplate()\" [captionTemplate]=\"queryCaptionTemplate()\"\n [actionsTemplate]=\"queryActionsTemplate()\" />\n }\n @if (extraContentTemplate(); as extraContentTpl) {\n <ng-container *ngTemplateOutlet=\"extraContentTpl; context: { $implicit: currentItem, entityType: et }\"></ng-container>\n }\n <!-- Add-on panels (SPARK_DETAIL_PANELS), e.g. <spark-po-history>. -->\n @if (detailContext(); as ctx) {\n @for (panel of detailPanels; track panel.id) {\n <div class=\"spark-detail-panel m-3\">\n <ng-container *ngComponentOutlet=\"panel.component; inputs: { context: ctx }\" />\n </div>\n }\n }\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n",
|
|
390
405
|
styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--spark-shell-main-bg, var(--bs-body-bg));border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}.spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}.container{--spark-content-inset-inline: 0px}.spark-multiline-value{white-space:pre-wrap}\n"],
|
|
391
406
|
dependencies: [
|
|
392
407
|
{
|
|
@@ -723,7 +738,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
723
738
|
SparkAttributeDescriptionComponent
|
|
724
739
|
],
|
|
725
740
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
726
|
-
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 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 visibleCustomActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\" [disabled]=\"runningAction() !== null\" (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 <!-- Add-on actions (SPARK_DETAIL_ACTIONS): each decides for itself whether it applies. -->\n @if (detailContext(); as ctx) {\n @for (action of detailActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n <h2>\n {{ currentItem.breadcrumb || currentItem.name }}\n @if (isDeletedView()) {\n <bs-badge [type]=\"colors.secondary\" class=\"align-middle ms-2 spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </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 class=\"d-block m-3\">\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 class=\"d-block m-3\">\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 }}<spark-attribute-description [description]=\"attr.description\" /></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():rendererRegistry); track col.name) {\n <th>{{ (col.label | resolveTranslation) || col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n </tr>\n </thead>\n <tbody class=\"align-middle\">\n @let rowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <td>\n <!--\n Shared with the query grid. This cell used to hand-roll the renderer\n dispatch and print everything else as text, so a `boolean` column read\n \"true\" here and rendered a checkbox in a grid, and a `color` showed its\n hex. Only the VALUE is resolved locally: these rows are plain\n dictionaries, not PersistentObjects.\n\n `chips` is left unset for a reference array on purpose — an AsDetail\n row's `__sparkBreadcrumbs` is keyed by column, not by id, so there are\n no labels for the members. Text beats a row of raw ids.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(row | asDetailCellValue:attr:col:asDetailReferenceOptions())\"\n [rendererValue]=\"row[col.name]\"\n [item]=\"row\"\n [link]=\"col.dataType === 'Reference' && col.referenceType\n ? (col.referenceType | referenceLinkRoute:row[col.name]:allEntityTypes())\n : null\" />\n <!-- A row renderer (rowComponent: contributions' attribution line) draws\n its attributes once, under the row's first cell, instead of as columns. -->\n @if (firstCol && rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row, currentItem)\"></ng-container>\n </div>\n }\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).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 class=\"opacity-100\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded me-2\" [style.background-color]=\"colorVal\"></span>\n {{ colorVal }}\n } @else {\n -\n }\n } @else if (attr.dataType === 'image') {\n @let imageSrc = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (imageSrc) {\n <img [src]=\"imageSrc\" [alt]=\"(attr.label | resolveTranslation) || attr.name\" loading=\"lazy\"\n class=\"img-fluid rounded\" style=\"max-height: 12rem;\">\n } @else {\n -\n }\n } @else if (attr.dataType === 'url') {\n @let hrefVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (hrefVal) {\n <a [href]=\"hrefVal\" target=\"_blank\" rel=\"noopener noreferrer\">{{ hrefVal }}</a>\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=\"spark-chip\" [routerLink]=\"chipRoute\">{{ chip.label }}</a>\n } @else {\n <span class=\"spark-chip\">{{ 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 class=\"spark-multiline-value\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}</div>\n } @else if (attr.dataType === 'date' || attr.dataType === 'datetime') {\n <!--\n Formatted the same way, and in the same timezone, as the grid renders it.\n\n Before this, a detail page printed the raw wire value — `2026-12-31T23:59:00-08:00`\n in a definition list — while the grid showed `01/01/2027, 08:59` for the same\n document. Not a rounding difference: a DateTimeOffset whose offset differs from the\n viewer's can disagree on the DATE, so the two pages named different days for one car.\n\n The grid's meaning wins because it is the one a reader can act on: a timestamp is an\n instant, shown in the viewer's own zone. The originating offset is still on the wire\n for code that needs it — server-side logic, exports, \"which country's morning was\n this?\" — it simply is not what a viewer is shown by default.\n -->\n @let when = (attr.name | rawAttributeValue:item()) | parsedDate;\n @if (when) {\n {{ when | date:(attr.dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n @for (subQuery of subQueries(); track subQuery.query) {\n <!-- The entry's selectionMode overrides the query's own for this parent (#460, D17). -->\n <spark-query-card [queryId]=\"subQuery.query\" [parentId]=\"currentItem.id!\" [parentType]=\"et.name\"\n [selectionMode]=\"subQuery.selectionMode ?? null\"\n [parentDeleted]=\"isDeletedView()\"\n [iconTemplate]=\"queryIconTemplate()\" [captionTemplate]=\"queryCaptionTemplate()\"\n [actionsTemplate]=\"queryActionsTemplate()\" />\n }\n @if (extraContentTemplate(); as extraContentTpl) {\n <ng-container *ngTemplateOutlet=\"extraContentTpl; context: { $implicit: currentItem, entityType: et }\"></ng-container>\n }\n <!-- Add-on panels (SPARK_DETAIL_PANELS), e.g. <spark-po-history>. -->\n @if (detailContext(); as ctx) {\n @for (panel of detailPanels; track panel.id) {\n <div class=\"spark-detail-panel m-3\">\n <ng-container *ngComponentOutlet=\"panel.component; inputs: { context: ctx }\" />\n </div>\n }\n }\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n",
|
|
741
|
+
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 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 <!-- Edit and Delete are the composed catalogue's (#467, D8): label, icon and variant included. -->\n @if (editAction(); as edit) {\n <button *bsPriorityNavItem=\"2\" [class]=\"builtInActionClass(edit, 'primary')\" (click)=\"onEdit()\">\n @if (edit.icon) { <spark-icon [name]=\"edit.icon\" /> } {{ edit.label | resolveTranslation }}\n </button>\n }\n @if (deleteAction(); as del) {\n <button *bsPriorityNavItem=\"3\" [class]=\"builtInActionClass(del, 'danger')\" (click)=\"onDelete()\">\n @if (del.icon) { <spark-icon [name]=\"del.icon\" /> } {{ del.label | resolveTranslation }}\n </button>\n }\n @if (showCustomActions()) {\n @for (action of visibleCustomActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\" [disabled]=\"runningAction() !== null\" (click)=\"onCustomAction(action)\">\n {{ action.label | 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 <!-- Add-on actions (SPARK_DETAIL_ACTIONS): each decides for itself whether it applies. -->\n @if (detailContext(); as ctx) {\n @for (action of detailActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n <h2>\n {{ currentItem.breadcrumb || currentItem.name }}\n @if (isDeletedView()) {\n <bs-badge [type]=\"colors.secondary\" class=\"align-middle ms-2 spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </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 class=\"d-block m-3\">\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 class=\"d-block m-3\">\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 }}<spark-attribute-description [description]=\"attr.description\" /></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():rendererRegistry); track col.name) {\n <th>{{ (col.label | resolveTranslation) || col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n </tr>\n </thead>\n <tbody class=\"align-middle\">\n @let rowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <td>\n <!--\n Shared with the query grid. This cell used to hand-roll the renderer\n dispatch and print everything else as text, so a `boolean` column read\n \"true\" here and rendered a checkbox in a grid, and a `color` showed its\n hex. Only the VALUE is resolved locally: these rows are plain\n dictionaries, not PersistentObjects.\n\n `chips` is left unset for a reference array on purpose — an AsDetail\n row's `__sparkBreadcrumbs` is keyed by column, not by id, so there are\n no labels for the members. Text beats a row of raw ids.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(row | asDetailCellValue:attr:col:asDetailReferenceOptions())\"\n [rendererValue]=\"row[col.name]\"\n [item]=\"row\"\n [link]=\"col.dataType === 'Reference' && col.referenceType\n ? (col.referenceType | referenceLinkRoute:row[col.name]:allEntityTypes())\n : null\" />\n <!-- A row renderer (rowComponent: contributions' attribution line) draws\n its attributes once, under the row's first cell, instead of as columns. -->\n @if (firstCol && rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row, currentItem)\"></ng-container>\n </div>\n }\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).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 class=\"opacity-100\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded me-2\" [style.background-color]=\"colorVal\"></span>\n {{ colorVal }}\n } @else {\n -\n }\n } @else if (attr.dataType === 'image') {\n @let imageSrc = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (imageSrc) {\n <img [src]=\"imageSrc\" [alt]=\"(attr.label | resolveTranslation) || attr.name\" loading=\"lazy\"\n class=\"img-fluid rounded\" style=\"max-height: 12rem;\">\n } @else {\n -\n }\n } @else if (attr.dataType === 'url') {\n @let hrefVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (hrefVal) {\n <a [href]=\"hrefVal\" target=\"_blank\" rel=\"noopener noreferrer\">{{ hrefVal }}</a>\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=\"spark-chip\" [routerLink]=\"chipRoute\">{{ chip.label }}</a>\n } @else {\n <span class=\"spark-chip\">{{ 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 class=\"spark-multiline-value\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}</div>\n } @else if (attr.dataType === 'date' || attr.dataType === 'datetime') {\n <!--\n Formatted the same way, and in the same timezone, as the grid renders it.\n\n Before this, a detail page printed the raw wire value — `2026-12-31T23:59:00-08:00`\n in a definition list — while the grid showed `01/01/2027, 08:59` for the same\n document. Not a rounding difference: a DateTimeOffset whose offset differs from the\n viewer's can disagree on the DATE, so the two pages named different days for one car.\n\n The grid's meaning wins because it is the one a reader can act on: a timestamp is an\n instant, shown in the viewer's own zone. The originating offset is still on the wire\n for code that needs it — server-side logic, exports, \"which country's morning was\n this?\" — it simply is not what a viewer is shown by default.\n -->\n @let when = (attr.name | rawAttributeValue:item()) | parsedDate;\n @if (when) {\n {{ when | date:(attr.dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n @for (subQuery of subQueries(); track subQuery.query) {\n <!-- The entry's selectionMode overrides the query's own for this parent (#460, D17). -->\n <spark-query-card [queryId]=\"subQuery.query\" [parentId]=\"currentItem.id!\" [parentType]=\"et.name\"\n [selectionMode]=\"subQuery.selectionMode ?? null\"\n [parentDeleted]=\"isDeletedView()\"\n [iconTemplate]=\"queryIconTemplate()\" [captionTemplate]=\"queryCaptionTemplate()\"\n [actionsTemplate]=\"queryActionsTemplate()\" />\n }\n @if (extraContentTemplate(); as extraContentTpl) {\n <ng-container *ngTemplateOutlet=\"extraContentTpl; context: { $implicit: currentItem, entityType: et }\"></ng-container>\n }\n <!-- Add-on panels (SPARK_DETAIL_PANELS), e.g. <spark-po-history>. -->\n @if (detailContext(); as ctx) {\n @for (panel of detailPanels; track panel.id) {\n <div class=\"spark-detail-panel m-3\">\n <ng-container *ngComponentOutlet=\"panel.component; inputs: { context: ctx }\" />\n </div>\n }\n }\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n",
|
|
727
742
|
styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--spark-shell-main-bg, var(--bs-body-bg));border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}.spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}.container{--spark-content-inset-inline: 0px}.spark-multiline-value{white-space:pre-wrap}\n"]
|
|
728
743
|
}]
|
|
729
744
|
}],
|