@mintplayer/ng-spark 22.2.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.
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs +69 -4
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-grid.mjs +111 -0
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-models.mjs +112 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-create.mjs +2 -2
- package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +211 -54
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +2 -2
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +88 -46
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/package.json +5 -1
- package/types/mintplayer-ng-spark-client-operations.d.ts +30 -4
- package/types/mintplayer-ng-spark-grid.d.ts +63 -0
- package/types/mintplayer-ng-spark-models.d.ts +49 -2
- package/types/mintplayer-ng-spark-po-detail.d.ts +112 -3
- package/types/mintplayer-ng-spark-query-list.d.ts +37 -4
|
@@ -146,9 +146,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImpor
|
|
|
146
146
|
}] });
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
149
|
+
* Carries a server-issued `refreshQuery` to whichever grids are showing that query.
|
|
150
|
+
*
|
|
151
|
+
* A broadcast signal rather than a registry of component handles: grids come and go behind
|
|
152
|
+
* `@if` and lazy routes, and nothing else in ng-spark holds a component reference. A grid
|
|
153
|
+
* reads {@link tokenFor} in an effect and re-fetches when it changes, which is the same
|
|
154
|
+
* declarative shape as the `reloadToken` input a host would use.
|
|
155
|
+
*
|
|
156
|
+
* Until this existed the server could emit the operation and the dispatcher dropped it: only
|
|
157
|
+
* `notify` was registered, and unknown types are ignored silently — so `refreshOnCompleted`
|
|
158
|
+
* on the server had no effect on any grid the action did not happen to be hosted in.
|
|
159
|
+
*/
|
|
160
|
+
class SparkQueryRefreshService {
|
|
161
|
+
tokens = signal({}, /* @ts-ignore */
|
|
162
|
+
...(ngDevMode ? [{ debugName: "tokens" }] : /* istanbul ignore next */ []));
|
|
163
|
+
/** Bumped every time the server asks for this query to refresh. */
|
|
164
|
+
tokenFor(queryId) {
|
|
165
|
+
if (!queryId)
|
|
166
|
+
return 0;
|
|
167
|
+
return this.tokens()[queryId] ?? 0;
|
|
168
|
+
}
|
|
169
|
+
/** Ask every grid showing `queryId` to re-fetch. Matched on id AND alias, since a grid may hold either. */
|
|
170
|
+
request(queryId) {
|
|
171
|
+
this.tokens.update(current => ({ ...current, [queryId]: (current[queryId] ?? 0) + 1 }));
|
|
172
|
+
}
|
|
173
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryRefreshService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
174
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryRefreshService, providedIn: 'root' });
|
|
175
|
+
}
|
|
176
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryRefreshService, decorators: [{
|
|
177
|
+
type: Injectable,
|
|
178
|
+
args: [{ providedIn: 'root' }]
|
|
179
|
+
}] });
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Registers the built-in client-operation handlers: `notify` and `refreshQuery`.
|
|
183
|
+
* Apps add this once in their bootstrap providers.
|
|
184
|
+
*
|
|
185
|
+
* Unregistered operation types are dropped SILENTLY by the dispatcher, which is why
|
|
186
|
+
* `refreshQuery` did nothing at all for as long as it went unhandled — the server emitted
|
|
187
|
+
* it, nothing listened, and no error said so. `disableAction` is in that state today: it is
|
|
188
|
+
* registered below purely to log, so the gap is visible rather than invisible.
|
|
152
189
|
*
|
|
153
190
|
* To register custom operation types alongside the built-ins, add additional
|
|
154
191
|
* `multi: true` providers using <see cref="SPARK_CLIENT_OPERATION_HANDLERS" />.
|
|
@@ -169,6 +206,34 @@ function provideSparkClientOperations() {
|
|
|
169
206
|
},
|
|
170
207
|
multi: true,
|
|
171
208
|
},
|
|
209
|
+
{
|
|
210
|
+
provide: SPARK_CLIENT_OPERATION_HANDLERS,
|
|
211
|
+
useFactory: () => {
|
|
212
|
+
const refresh = inject(SparkQueryRefreshService);
|
|
213
|
+
return {
|
|
214
|
+
type: 'refreshQuery',
|
|
215
|
+
handler: (operation) => {
|
|
216
|
+
refresh.request(operation.queryId);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
multi: true,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
provide: SPARK_CLIENT_OPERATION_HANDLERS,
|
|
224
|
+
useFactory: () => ({
|
|
225
|
+
type: 'disableAction',
|
|
226
|
+
handler: (operation) => {
|
|
227
|
+
// Deliberately a no-op with a warning, not silence. The server's
|
|
228
|
+
// IClientAccessor.DisableQueryActions presumes a client that honours it;
|
|
229
|
+
// nothing renders the disabled state yet, and a silently dropped operation
|
|
230
|
+
// reads as "the server did not send it" when debugging.
|
|
231
|
+
const disable = operation;
|
|
232
|
+
console.warn(`[spark] disableAction('${disable.actionName}') is not implemented by this client; the action stays enabled.`);
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
235
|
+
multi: true,
|
|
236
|
+
},
|
|
172
237
|
]);
|
|
173
238
|
}
|
|
174
239
|
|
|
@@ -176,5 +241,5 @@ function provideSparkClientOperations() {
|
|
|
176
241
|
* Generated bundle index. Do not edit.
|
|
177
242
|
*/
|
|
178
243
|
|
|
179
|
-
export { NotificationKind, SPARK_CLIENT_OPERATION_HANDLERS, SparkClientOperationDispatcher, SparkNotificationService, SparkToastContainerComponent, provideSparkClientOperations };
|
|
244
|
+
export { NotificationKind, SPARK_CLIENT_OPERATION_HANDLERS, SparkClientOperationDispatcher, SparkNotificationService, SparkQueryRefreshService, SparkToastContainerComponent, provideSparkClientOperations };
|
|
180
245
|
//# sourceMappingURL=mintplayer-ng-spark-client-operations.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-client-operations.mjs","sources":["../../client-operations/src/operations.ts","../../client-operations/src/handlers.token.ts","../../client-operations/src/dispatcher.service.ts","../../client-operations/src/notification.service.ts","../../client-operations/src/toast-container.component.ts","../../client-operations/src/provide.ts","../../client-operations/mintplayer-ng-spark-client-operations.ts"],"sourcesContent":["// Wire types matching MintPlayer.Spark.Abstractions.ClientOperations on the server.\n// Discriminator is the `type` field. Unknown operation types are silently dropped\n// by the dispatcher (forward-compat: new types can land server-side without updating\n// older clients).\n\nimport type { PersistentObject } from '@mintplayer/ng-spark/models';\n\nexport enum NotificationKind {\n Info = 0,\n Success = 1,\n Warning = 2,\n Error = 3,\n}\n\nexport interface NavigateOperation {\n type: 'navigate';\n objectTypeId?: string;\n id?: string;\n routeName?: string;\n}\n\nexport interface NotifyOperation {\n type: 'notify';\n message: string;\n kind: NotificationKind;\n durationMs?: number;\n}\n\nexport interface RefreshAttributeOperation {\n type: 'refreshAttribute';\n objectTypeId: string;\n id: string;\n attributeName: string;\n value?: unknown;\n}\n\nexport interface RefreshQueryOperation {\n type: 'refreshQuery';\n queryId: string;\n}\n\nexport type DisableTarget =\n | { kind: 'persistentObject'; objectTypeId: string; id: string }\n | { kind: 'query'; queryId: string }\n | { kind: 'currentResponse' }\n | { kind: 'session' };\n\nexport interface DisableActionOperation {\n type: 'disableAction';\n actionName: string;\n target: DisableTarget;\n}\n\nexport interface RetryOperation {\n type: 'retry';\n step: number;\n title: string;\n options: string[];\n defaultOption?: string | null;\n persistentObject?: PersistentObject | null;\n message?: string | null;\n}\n\n/**\n * Discriminated union of known operation types, plus an open shape for unknown\n * future operations. Handlers should narrow via the `type` discriminator before\n * accessing fields specific to their operation type.\n */\nexport type ClientOperation =\n | NavigateOperation\n | NotifyOperation\n | RefreshAttributeOperation\n | RefreshQueryOperation\n | DisableActionOperation\n | RetryOperation\n | { type: string; [key: string]: unknown };\n\n/**\n * Wire envelope returned by every action endpoint. `result` carries the primary\n * payload (the PersistentObject for a Create, the QueryResult for an Execute,\n * etc.); `operations` carries the side-effects the frontend dispatches.\n */\nexport interface ClientOperationEnvelope<T = unknown> {\n result: T | null;\n operations: ClientOperation[];\n}\n","import { InjectionToken } from '@angular/core';\nimport type { ClientOperation } from './operations';\n\n/**\n * A handler for a specific operation type. Receives the operation and\n * executes the side-effect (e.g. show a toast, navigate, refresh a query).\n * Handlers should `as`-narrow the operation to the type they registered for.\n */\nexport type ClientOperationHandler = (operation: ClientOperation) => void;\n\n/**\n * One entry in the multi-provider registration. Apps can register custom\n * handlers alongside the built-in ones to extend the operation set with\n * app-specific operation types.\n */\nexport interface ClientOperationHandlerRegistration {\n type: string;\n handler: ClientOperationHandler;\n}\n\n/**\n * Multi-provider token. `provideSparkClientOperations()` registers the\n * built-in handlers; apps can add their own with additional `multi: true`\n * providers using this token.\n */\nexport const SPARK_CLIENT_OPERATION_HANDLERS = new InjectionToken<readonly ClientOperationHandlerRegistration[]>(\n 'SPARK_CLIENT_OPERATION_HANDLERS',\n);\n","import { Injectable, inject } from '@angular/core';\nimport type { ClientOperation } from './operations';\nimport { SPARK_CLIENT_OPERATION_HANDLERS, type ClientOperationHandler } from './handlers.token';\n\n/**\n * Routes received operations to registered handlers. Unknown operation types\n * (no matching registration) are silently dropped — this is the forward-compat\n * contract that lets new operation types ship server-side without coordinated\n * client updates.\n *\n * Last-registered-wins on duplicate `type` values, matching standard\n * Angular multi-provider override semantics.\n *\n * R2-H19 — security contract for handler authors:\n * The dispatcher treats handler resolution as allow-list-by-type (unknown\n * types drop). It does NOT validate the *content* of each operation. Handlers\n * that act on URL-shaped fields (navigate, redirect, openWindow) MUST run\n * the value through `sanitizeReturnUrl` from `@mintplayer/ng-spark-auth/models`\n * (or an equivalent same-origin check) before acting on it. Otherwise a\n * single attribute-echo XSS or a single mid-channel byte flip on a non-TLS\n * path lets the server drive client navigation to an attacker host. The\n * built-in `notify` handler renders via Angular interpolation (escaped) so\n * it's safe to pass through, but anything more powerful must validate.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkClientOperationDispatcher {\n private readonly handlerMap: ReadonlyMap<string, ClientOperationHandler>;\n\n constructor() {\n const registrations = inject(SPARK_CLIENT_OPERATION_HANDLERS, { optional: true }) ?? [];\n const map = new Map<string, ClientOperationHandler>();\n for (const { type, handler } of registrations) {\n map.set(type, handler);\n }\n this.handlerMap = map;\n }\n\n dispatch(operations: readonly ClientOperation[] | null | undefined): void {\n if (!operations || operations.length === 0) return;\n for (const operation of operations) {\n const handler = this.handlerMap.get(operation.type);\n if (handler) {\n handler(operation);\n }\n // Unknown types: silently dropped (forward-compat).\n }\n }\n}\n","import { Injectable, signal } from '@angular/core';\nimport { NotificationKind } from './operations';\n\nexport interface SparkToast {\n id: string;\n message: string;\n kind: NotificationKind;\n durationMs: number;\n}\n\nconst DEFAULT_DURATION_MS = 4000;\n\n/**\n * Holds the active toasts as a signal. The `<spark-toast-container>` component\n * renders them; the built-in `notify` operation handler pushes new toasts here.\n *\n * Auto-dismissal: each toast schedules its own removal after `durationMs`. Pass\n * `0` to make a toast sticky (manual dismissal only).\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkNotificationService {\n private readonly _toasts = signal<readonly SparkToast[]>([]);\n readonly toasts = this._toasts.asReadonly();\n\n show(message: string, kind: NotificationKind = NotificationKind.Info, durationMs?: number): void {\n const id = typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;\n const effectiveDuration = durationMs ?? DEFAULT_DURATION_MS;\n const toast: SparkToast = { id, message, kind, durationMs: effectiveDuration };\n this._toasts.update(toasts => [...toasts, toast]);\n\n if (effectiveDuration > 0) {\n setTimeout(() => this.dismiss(id), effectiveDuration);\n }\n }\n\n dismiss(id: string): void {\n this._toasts.update(toasts => toasts.filter(t => t.id !== id));\n }\n\n clear(): void {\n this._toasts.set([]);\n }\n}\n","import { ChangeDetectionStrategy, Component, inject } from '@angular/core';\nimport { SparkNotificationService } from './notification.service';\nimport { NotificationKind } from './operations';\n\n@Component({\n selector: 'spark-toast-container',\n standalone: true,\n template: `\n <div class=\"spark-toast-container\">\n @for (toast of notifications.toasts(); track toast.id) {\n <div\n class=\"spark-toast\"\n [class.spark-toast--info]=\"toast.kind === Kind.Info\"\n [class.spark-toast--success]=\"toast.kind === Kind.Success\"\n [class.spark-toast--warning]=\"toast.kind === Kind.Warning\"\n [class.spark-toast--error]=\"toast.kind === Kind.Error\"\n (click)=\"notifications.dismiss(toast.id)\"\n >\n {{ toast.message }}\n </div>\n }\n </div>\n `,\n styles: [`\n .spark-toast-container {\n position: fixed;\n top: 1rem;\n right: 1rem;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n pointer-events: none;\n }\n .spark-toast {\n padding: 0.75rem 1rem;\n border-radius: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n cursor: pointer;\n min-width: 220px;\n max-width: 400px;\n color: white;\n font-size: 0.95rem;\n pointer-events: auto;\n animation: spark-toast-in 0.18s ease-out;\n }\n .spark-toast--info { background: #0d6efd; }\n .spark-toast--success { background: #198754; }\n .spark-toast--warning { background: #ffc107; color: #000; }\n .spark-toast--error { background: #dc3545; }\n @keyframes spark-toast-in {\n from { opacity: 0; transform: translateX(8px); }\n to { opacity: 1; transform: translateX(0); }\n }\n `],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SparkToastContainerComponent {\n protected readonly notifications = inject(SparkNotificationService);\n protected readonly Kind = NotificationKind;\n}\n","import { type EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport type { ClientOperation, NotifyOperation } from './operations';\nimport { SPARK_CLIENT_OPERATION_HANDLERS } from './handlers.token';\nimport { SparkNotificationService } from './notification.service';\n\n/**\n * Registers the built-in client-operation handlers. Currently registers `notify`;\n * additional types (`navigate`, `refreshQuery`, `refreshAttribute`, `disableAction`)\n * land in subsequent commits. Apps add this once in their bootstrap providers.\n *\n * To register custom operation types alongside the built-ins, add additional\n * `multi: true` providers using <see cref=\"SPARK_CLIENT_OPERATION_HANDLERS\" />.\n */\nexport function provideSparkClientOperations(): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: SPARK_CLIENT_OPERATION_HANDLERS,\n useFactory: () => {\n const notifications = inject(SparkNotificationService);\n return {\n type: 'notify',\n handler: (operation: ClientOperation) => {\n const notify = operation as NotifyOperation;\n notifications.show(notify.message, notify.kind, notify.durationMs);\n },\n };\n },\n multi: true,\n },\n ]);\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-client-operations.mjs","sources":["../../client-operations/src/operations.ts","../../client-operations/src/handlers.token.ts","../../client-operations/src/dispatcher.service.ts","../../client-operations/src/notification.service.ts","../../client-operations/src/toast-container.component.ts","../../client-operations/src/query-refresh.service.ts","../../client-operations/src/provide.ts","../../client-operations/mintplayer-ng-spark-client-operations.ts"],"sourcesContent":["// Wire types matching MintPlayer.Spark.Abstractions.ClientOperations on the server.\n// Discriminator is the `type` field. Unknown operation types are silently dropped\n// by the dispatcher (forward-compat: new types can land server-side without updating\n// older clients).\n\nimport type { PersistentObject } from '@mintplayer/ng-spark/models';\n\nexport enum NotificationKind {\n Info = 0,\n Success = 1,\n Warning = 2,\n Error = 3,\n}\n\nexport interface NavigateOperation {\n type: 'navigate';\n objectTypeId?: string;\n id?: string;\n routeName?: string;\n}\n\nexport interface NotifyOperation {\n type: 'notify';\n message: string;\n kind: NotificationKind;\n durationMs?: number;\n}\n\nexport interface RefreshAttributeOperation {\n type: 'refreshAttribute';\n objectTypeId: string;\n id: string;\n attributeName: string;\n value?: unknown;\n}\n\nexport interface RefreshQueryOperation {\n type: 'refreshQuery';\n queryId: string;\n}\n\nexport type DisableTarget =\n | { kind: 'persistentObject'; objectTypeId: string; id: string }\n | { kind: 'query'; queryId: string }\n | { kind: 'currentResponse' }\n | { kind: 'session' };\n\nexport interface DisableActionOperation {\n type: 'disableAction';\n actionName: string;\n target: DisableTarget;\n}\n\nexport interface RetryOperation {\n type: 'retry';\n step: number;\n title: string;\n options: string[];\n defaultOption?: string | null;\n persistentObject?: PersistentObject | null;\n message?: string | null;\n}\n\n/**\n * Discriminated union of known operation types, plus an open shape for unknown\n * future operations. Handlers should narrow via the `type` discriminator before\n * accessing fields specific to their operation type.\n */\nexport type ClientOperation =\n | NavigateOperation\n | NotifyOperation\n | RefreshAttributeOperation\n | RefreshQueryOperation\n | DisableActionOperation\n | RetryOperation\n | { type: string; [key: string]: unknown };\n\n/**\n * Wire envelope returned by every action endpoint. `result` carries the primary\n * payload (the PersistentObject for a Create, the QueryResult for an Execute,\n * etc.); `operations` carries the side-effects the frontend dispatches.\n */\nexport interface ClientOperationEnvelope<T = unknown> {\n result: T | null;\n operations: ClientOperation[];\n}\n","import { InjectionToken } from '@angular/core';\nimport type { ClientOperation } from './operations';\n\n/**\n * A handler for a specific operation type. Receives the operation and\n * executes the side-effect (e.g. show a toast, navigate, refresh a query).\n * Handlers should `as`-narrow the operation to the type they registered for.\n */\nexport type ClientOperationHandler = (operation: ClientOperation) => void;\n\n/**\n * One entry in the multi-provider registration. Apps can register custom\n * handlers alongside the built-in ones to extend the operation set with\n * app-specific operation types.\n */\nexport interface ClientOperationHandlerRegistration {\n type: string;\n handler: ClientOperationHandler;\n}\n\n/**\n * Multi-provider token. `provideSparkClientOperations()` registers the\n * built-in handlers; apps can add their own with additional `multi: true`\n * providers using this token.\n */\nexport const SPARK_CLIENT_OPERATION_HANDLERS = new InjectionToken<readonly ClientOperationHandlerRegistration[]>(\n 'SPARK_CLIENT_OPERATION_HANDLERS',\n);\n","import { Injectable, inject } from '@angular/core';\nimport type { ClientOperation } from './operations';\nimport { SPARK_CLIENT_OPERATION_HANDLERS, type ClientOperationHandler } from './handlers.token';\n\n/**\n * Routes received operations to registered handlers. Unknown operation types\n * (no matching registration) are silently dropped — this is the forward-compat\n * contract that lets new operation types ship server-side without coordinated\n * client updates.\n *\n * Last-registered-wins on duplicate `type` values, matching standard\n * Angular multi-provider override semantics.\n *\n * R2-H19 — security contract for handler authors:\n * The dispatcher treats handler resolution as allow-list-by-type (unknown\n * types drop). It does NOT validate the *content* of each operation. Handlers\n * that act on URL-shaped fields (navigate, redirect, openWindow) MUST run\n * the value through `sanitizeReturnUrl` from `@mintplayer/ng-spark-auth/models`\n * (or an equivalent same-origin check) before acting on it. Otherwise a\n * single attribute-echo XSS or a single mid-channel byte flip on a non-TLS\n * path lets the server drive client navigation to an attacker host. The\n * built-in `notify` handler renders via Angular interpolation (escaped) so\n * it's safe to pass through, but anything more powerful must validate.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkClientOperationDispatcher {\n private readonly handlerMap: ReadonlyMap<string, ClientOperationHandler>;\n\n constructor() {\n const registrations = inject(SPARK_CLIENT_OPERATION_HANDLERS, { optional: true }) ?? [];\n const map = new Map<string, ClientOperationHandler>();\n for (const { type, handler } of registrations) {\n map.set(type, handler);\n }\n this.handlerMap = map;\n }\n\n dispatch(operations: readonly ClientOperation[] | null | undefined): void {\n if (!operations || operations.length === 0) return;\n for (const operation of operations) {\n const handler = this.handlerMap.get(operation.type);\n if (handler) {\n handler(operation);\n }\n // Unknown types: silently dropped (forward-compat).\n }\n }\n}\n","import { Injectable, signal } from '@angular/core';\nimport { NotificationKind } from './operations';\n\nexport interface SparkToast {\n id: string;\n message: string;\n kind: NotificationKind;\n durationMs: number;\n}\n\nconst DEFAULT_DURATION_MS = 4000;\n\n/**\n * Holds the active toasts as a signal. The `<spark-toast-container>` component\n * renders them; the built-in `notify` operation handler pushes new toasts here.\n *\n * Auto-dismissal: each toast schedules its own removal after `durationMs`. Pass\n * `0` to make a toast sticky (manual dismissal only).\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkNotificationService {\n private readonly _toasts = signal<readonly SparkToast[]>([]);\n readonly toasts = this._toasts.asReadonly();\n\n show(message: string, kind: NotificationKind = NotificationKind.Info, durationMs?: number): void {\n const id = typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;\n const effectiveDuration = durationMs ?? DEFAULT_DURATION_MS;\n const toast: SparkToast = { id, message, kind, durationMs: effectiveDuration };\n this._toasts.update(toasts => [...toasts, toast]);\n\n if (effectiveDuration > 0) {\n setTimeout(() => this.dismiss(id), effectiveDuration);\n }\n }\n\n dismiss(id: string): void {\n this._toasts.update(toasts => toasts.filter(t => t.id !== id));\n }\n\n clear(): void {\n this._toasts.set([]);\n }\n}\n","import { ChangeDetectionStrategy, Component, inject } from '@angular/core';\nimport { SparkNotificationService } from './notification.service';\nimport { NotificationKind } from './operations';\n\n@Component({\n selector: 'spark-toast-container',\n standalone: true,\n template: `\n <div class=\"spark-toast-container\">\n @for (toast of notifications.toasts(); track toast.id) {\n <div\n class=\"spark-toast\"\n [class.spark-toast--info]=\"toast.kind === Kind.Info\"\n [class.spark-toast--success]=\"toast.kind === Kind.Success\"\n [class.spark-toast--warning]=\"toast.kind === Kind.Warning\"\n [class.spark-toast--error]=\"toast.kind === Kind.Error\"\n (click)=\"notifications.dismiss(toast.id)\"\n >\n {{ toast.message }}\n </div>\n }\n </div>\n `,\n styles: [`\n .spark-toast-container {\n position: fixed;\n top: 1rem;\n right: 1rem;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n pointer-events: none;\n }\n .spark-toast {\n padding: 0.75rem 1rem;\n border-radius: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n cursor: pointer;\n min-width: 220px;\n max-width: 400px;\n color: white;\n font-size: 0.95rem;\n pointer-events: auto;\n animation: spark-toast-in 0.18s ease-out;\n }\n .spark-toast--info { background: #0d6efd; }\n .spark-toast--success { background: #198754; }\n .spark-toast--warning { background: #ffc107; color: #000; }\n .spark-toast--error { background: #dc3545; }\n @keyframes spark-toast-in {\n from { opacity: 0; transform: translateX(8px); }\n to { opacity: 1; transform: translateX(0); }\n }\n `],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SparkToastContainerComponent {\n protected readonly notifications = inject(SparkNotificationService);\n protected readonly Kind = NotificationKind;\n}\n","import { Injectable, signal } from '@angular/core';\n\n/**\n * Carries a server-issued `refreshQuery` to whichever grids are showing that query.\n *\n * A broadcast signal rather than a registry of component handles: grids come and go behind\n * `@if` and lazy routes, and nothing else in ng-spark holds a component reference. A grid\n * reads {@link tokenFor} in an effect and re-fetches when it changes, which is the same\n * declarative shape as the `reloadToken` input a host would use.\n *\n * Until this existed the server could emit the operation and the dispatcher dropped it: only\n * `notify` was registered, and unknown types are ignored silently — so `refreshOnCompleted`\n * on the server had no effect on any grid the action did not happen to be hosted in.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkQueryRefreshService {\n private readonly tokens = signal<Record<string, number>>({});\n\n /** Bumped every time the server asks for this query to refresh. */\n tokenFor(queryId: string | undefined): number {\n if (!queryId) return 0;\n return this.tokens()[queryId] ?? 0;\n }\n\n /** Ask every grid showing `queryId` to re-fetch. Matched on id AND alias, since a grid may hold either. */\n request(queryId: string): void {\n this.tokens.update(current => ({ ...current, [queryId]: (current[queryId] ?? 0) + 1 }));\n }\n}\n","import { type EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport type { ClientOperation, DisableActionOperation, NotifyOperation, RefreshQueryOperation } from './operations';\nimport { SPARK_CLIENT_OPERATION_HANDLERS } from './handlers.token';\nimport { SparkNotificationService } from './notification.service';\nimport { SparkQueryRefreshService } from './query-refresh.service';\n\n/**\n * Registers the built-in client-operation handlers: `notify` and `refreshQuery`.\n * Apps add this once in their bootstrap providers.\n *\n * Unregistered operation types are dropped SILENTLY by the dispatcher, which is why\n * `refreshQuery` did nothing at all for as long as it went unhandled — the server emitted\n * it, nothing listened, and no error said so. `disableAction` is in that state today: it is\n * registered below purely to log, so the gap is visible rather than invisible.\n *\n * To register custom operation types alongside the built-ins, add additional\n * `multi: true` providers using <see cref=\"SPARK_CLIENT_OPERATION_HANDLERS\" />.\n */\nexport function provideSparkClientOperations(): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: SPARK_CLIENT_OPERATION_HANDLERS,\n useFactory: () => {\n const notifications = inject(SparkNotificationService);\n return {\n type: 'notify',\n handler: (operation: ClientOperation) => {\n const notify = operation as NotifyOperation;\n notifications.show(notify.message, notify.kind, notify.durationMs);\n },\n };\n },\n multi: true,\n },\n {\n provide: SPARK_CLIENT_OPERATION_HANDLERS,\n useFactory: () => {\n const refresh = inject(SparkQueryRefreshService);\n return {\n type: 'refreshQuery',\n handler: (operation: ClientOperation) => {\n refresh.request((operation as RefreshQueryOperation).queryId);\n },\n };\n },\n multi: true,\n },\n {\n provide: SPARK_CLIENT_OPERATION_HANDLERS,\n useFactory: () => ({\n type: 'disableAction',\n handler: (operation: ClientOperation) => {\n // Deliberately a no-op with a warning, not silence. The server's\n // IClientAccessor.DisableQueryActions presumes a client that honours it;\n // nothing renders the disabled state yet, and a silently dropped operation\n // reads as \"the server did not send it\" when debugging.\n const disable = operation as DisableActionOperation;\n console.warn(\n `[spark] disableAction('${disable.actionName}') is not implemented by this client; the action stays enabled.`);\n },\n }),\n multi: true,\n },\n ]);\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { DatatableSettings } from '@mintplayer/ng-bootstrap/datatable';
|
|
2
|
+
import { hasShowedOnFlag, ShowedOn } from '@mintplayer/ng-spark/models';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { inject, Injectable } from '@angular/core';
|
|
5
|
+
import { SPARK_ATTRIBUTE_RENDERERS, withDeclaredInputs, rendererValue } from '@mintplayer/ng-spark/renderers';
|
|
6
|
+
import { SparkService } from '@mintplayer/ng-spark/services';
|
|
7
|
+
|
|
8
|
+
/** Page sizes offered by every Spark grid. */
|
|
9
|
+
const SPARK_GRID_PAGE_SIZES = [10, 25, 50];
|
|
10
|
+
/**
|
|
11
|
+
* The attributes a grid shows, in display order.
|
|
12
|
+
*
|
|
13
|
+
* Shared so the two grids cannot disagree about what "visible" means — they each had their own
|
|
14
|
+
* copy of this expression, which is the kind of thing that stays identical right up until it
|
|
15
|
+
* doesn't.
|
|
16
|
+
*/
|
|
17
|
+
function visibleGridAttributes(entityType) {
|
|
18
|
+
return entityType?.attributes
|
|
19
|
+
.filter(a => a.isVisible && hasShowedOnFlag(a.showedOn, ShowedOn.Query))
|
|
20
|
+
.sort((a, b) => a.order - b.order) ?? [];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Initial datatable settings for a query, seeded with the query's declared sort.
|
|
24
|
+
*
|
|
25
|
+
* The datatable owns paging and sorting from here on and calls the fetch callback per page; this
|
|
26
|
+
* only decides where it starts.
|
|
27
|
+
*/
|
|
28
|
+
function initialGridSettings(query) {
|
|
29
|
+
// Nullable because one caller resolves its query from a route param and may not have one yet;
|
|
30
|
+
// an unsorted grid is the right fallback, not a crash.
|
|
31
|
+
const sortColumns = (query?.sortColumns || []).map(sc => ({
|
|
32
|
+
property: sc.property,
|
|
33
|
+
direction: sc.direction === 'desc' ? 'descending' : 'ascending',
|
|
34
|
+
}));
|
|
35
|
+
return new DatatableSettings({
|
|
36
|
+
perPage: { values: SPARK_GRID_PAGE_SIZES, selected: SPARK_GRID_PAGE_SIZES[0] },
|
|
37
|
+
page: { values: [1], selected: 1 },
|
|
38
|
+
sortColumns,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Whether a query renders as a virtual-scrolling grid rather than a paged one. */
|
|
42
|
+
function isVirtualScrollingQuery(query) {
|
|
43
|
+
return query?.renderMode === 'VirtualScrolling';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The parts of a Spark grid that both grid components need and that were, until this existed,
|
|
48
|
+
* written out twice.
|
|
49
|
+
*
|
|
50
|
+
* `spark-query-list` and `spark-sub-query` had byte-identical copies of the renderer lookup, the
|
|
51
|
+
* renderer input construction and the lookup-reference loading — around 120 lines between them.
|
|
52
|
+
* That duplication is not a tidiness complaint: it is what produced the drift. The two copies
|
|
53
|
+
* disagreed about `[indeterminate]`, about resetting permission state, about whether a fetch
|
|
54
|
+
* failure surfaces or is swallowed, and about virtual-scroll sizing — four user-visible bugs, each
|
|
55
|
+
* fixed on one side and not the other, because nothing made the two files move together.
|
|
56
|
+
*
|
|
57
|
+
* Kept deliberately small and stateless. The two components differ in real ways — one is
|
|
58
|
+
* route-coupled and carries streaming, search and a websocket dependency graph — so merging them
|
|
59
|
+
* into a single component would drag all of that into every detail page's bundle. Shared logic
|
|
60
|
+
* belongs here; shared *state* does not.
|
|
61
|
+
*/
|
|
62
|
+
class SparkGridRenderers {
|
|
63
|
+
registry = inject(SPARK_ATTRIBUTE_RENDERERS);
|
|
64
|
+
sparkService = inject(SparkService);
|
|
65
|
+
/** The registered column component for an attribute, or null to fall back to the default cell. */
|
|
66
|
+
columnComponentFor(attr) {
|
|
67
|
+
if (!attr.renderer)
|
|
68
|
+
return null;
|
|
69
|
+
return this.registry.find(r => r.name === attr.renderer)?.columnComponent ?? null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Inputs for a column renderer, filtered to what the component actually declares —
|
|
73
|
+
* `NgComponentOutlet` throws on an input the target does not have, which is what lets every
|
|
74
|
+
* member of the renderer contract be optional.
|
|
75
|
+
*/
|
|
76
|
+
columnInputsFor(component, item, attr) {
|
|
77
|
+
const itemAttr = item.attributes.find(a => a.name === attr.name);
|
|
78
|
+
return withDeclaredInputs(component, {
|
|
79
|
+
value: rendererValue(itemAttr),
|
|
80
|
+
attribute: attr,
|
|
81
|
+
options: attr.rendererOptions,
|
|
82
|
+
item,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Loads every lookup reference the visible attributes need, in one pass.
|
|
87
|
+
*
|
|
88
|
+
* Returns an empty map rather than throwing when there are none, so callers never branch on it.
|
|
89
|
+
*/
|
|
90
|
+
async loadLookupOptions(attributes) {
|
|
91
|
+
const lookupAttrs = attributes.filter(a => a.lookupReferenceType);
|
|
92
|
+
if (lookupAttrs.length === 0)
|
|
93
|
+
return {};
|
|
94
|
+
const names = [...new Set(lookupAttrs.map(a => a.lookupReferenceType))];
|
|
95
|
+
const entries = await Promise.all(names.map(async (name) => [name, await this.sparkService.getLookupReference(name)]));
|
|
96
|
+
return entries.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {});
|
|
97
|
+
}
|
|
98
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkGridRenderers, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
99
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkGridRenderers, providedIn: 'root' });
|
|
100
|
+
}
|
|
101
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkGridRenderers, decorators: [{
|
|
102
|
+
type: Injectable,
|
|
103
|
+
args: [{ providedIn: 'root' }]
|
|
104
|
+
}] });
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generated bundle index. Do not edit.
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
export { SPARK_GRID_PAGE_SIZES, SparkGridRenderers, initialGridSettings, isVirtualScrollingQuery, visibleGridAttributes };
|
|
111
|
+
//# sourceMappingURL=mintplayer-ng-spark-grid.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-grid.mjs","sources":["../../grid/src/spark-grid-columns.ts","../../grid/src/spark-grid-renderers.ts","../../grid/mintplayer-ng-spark-grid.ts"],"sourcesContent":["import { DatatableSettings } from '@mintplayer/ng-bootstrap/datatable';\nimport { SortColumn } from '@mintplayer/pagination';\nimport {\n EntityAttributeDefinition,\n EntityType,\n ShowedOn,\n SparkQuery,\n hasShowedOnFlag,\n} from '@mintplayer/ng-spark/models';\n\n/** Page sizes offered by every Spark grid. */\nexport const SPARK_GRID_PAGE_SIZES = [10, 25, 50];\n\n/**\n * The attributes a grid shows, in display order.\n *\n * Shared so the two grids cannot disagree about what \"visible\" means — they each had their own\n * copy of this expression, which is the kind of thing that stays identical right up until it\n * doesn't.\n */\nexport function visibleGridAttributes(entityType: EntityType | null): EntityAttributeDefinition[] {\n return entityType?.attributes\n .filter(a => a.isVisible && hasShowedOnFlag(a.showedOn, ShowedOn.Query))\n .sort((a, b) => a.order - b.order) ?? [];\n}\n\n/**\n * Initial datatable settings for a query, seeded with the query's declared sort.\n *\n * The datatable owns paging and sorting from here on and calls the fetch callback per page; this\n * only decides where it starts.\n */\nexport function initialGridSettings(query: SparkQuery | null): DatatableSettings {\n // Nullable because one caller resolves its query from a route param and may not have one yet;\n // an unsorted grid is the right fallback, not a crash.\n const sortColumns: SortColumn[] = (query?.sortColumns || []).map(sc => ({\n property: sc.property,\n direction: sc.direction === 'desc' ? 'descending' as const : 'ascending' as const,\n }));\n\n return new DatatableSettings({\n perPage: { values: SPARK_GRID_PAGE_SIZES, selected: SPARK_GRID_PAGE_SIZES[0] },\n page: { values: [1], selected: 1 },\n sortColumns,\n });\n}\n\n/** Whether a query renders as a virtual-scrolling grid rather than a paged one. */\nexport function isVirtualScrollingQuery(query: SparkQuery | null): boolean {\n return query?.renderMode === 'VirtualScrolling';\n}\n","import { inject, Injectable, Type } from '@angular/core';\nimport {\n EntityAttributeDefinition,\n LookupReference,\n PersistentObject,\n} from '@mintplayer/ng-spark/models';\nimport { SPARK_ATTRIBUTE_RENDERERS, rendererValue, withDeclaredInputs } from '@mintplayer/ng-spark/renderers';\nimport { SparkService } from '@mintplayer/ng-spark/services';\n\n/**\n * The parts of a Spark grid that both grid components need and that were, until this existed,\n * written out twice.\n *\n * `spark-query-list` and `spark-sub-query` had byte-identical copies of the renderer lookup, the\n * renderer input construction and the lookup-reference loading — around 120 lines between them.\n * That duplication is not a tidiness complaint: it is what produced the drift. The two copies\n * disagreed about `[indeterminate]`, about resetting permission state, about whether a fetch\n * failure surfaces or is swallowed, and about virtual-scroll sizing — four user-visible bugs, each\n * fixed on one side and not the other, because nothing made the two files move together.\n *\n * Kept deliberately small and stateless. The two components differ in real ways — one is\n * route-coupled and carries streaming, search and a websocket dependency graph — so merging them\n * into a single component would drag all of that into every detail page's bundle. Shared logic\n * belongs here; shared *state* does not.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkGridRenderers {\n private readonly registry = inject(SPARK_ATTRIBUTE_RENDERERS);\n private readonly sparkService = inject(SparkService);\n\n /** The registered column component for an attribute, or null to fall back to the default cell. */\n columnComponentFor(attr: EntityAttributeDefinition): Type<any> | null {\n if (!attr.renderer) return null;\n return this.registry.find(r => r.name === attr.renderer)?.columnComponent ?? null;\n }\n\n /**\n * Inputs for a column renderer, filtered to what the component actually declares —\n * `NgComponentOutlet` throws on an input the target does not have, which is what lets every\n * member of the renderer contract be optional.\n */\n columnInputsFor(component: Type<any>, item: PersistentObject, attr: EntityAttributeDefinition): Record<string, any> {\n const itemAttr = item.attributes.find(a => a.name === attr.name);\n return withDeclaredInputs(component, {\n value: rendererValue(itemAttr),\n attribute: attr,\n options: attr.rendererOptions,\n item,\n });\n }\n\n /**\n * Loads every lookup reference the visible attributes need, in one pass.\n *\n * Returns an empty map rather than throwing when there are none, so callers never branch on it.\n */\n async loadLookupOptions(attributes: EntityAttributeDefinition[]): Promise<Record<string, LookupReference>> {\n const lookupAttrs = attributes.filter(a => a.lookupReferenceType);\n if (lookupAttrs.length === 0) return {};\n\n const names = [...new Set(lookupAttrs.map(a => a.lookupReferenceType!))];\n const entries = await Promise.all(\n names.map(async name => [name, await this.sparkService.getLookupReference(name)] as const),\n );\n return entries.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {} as Record<string, LookupReference>);\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -174,9 +174,120 @@ function buildAttribute(attrDef, raw, resolve) {
|
|
|
174
174
|
return attr;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* The custom actions a query should offer, from the entity type's full set.
|
|
179
|
+
*
|
|
180
|
+
* `showedOn` must include the query side. The accepted values are `"detail"`, `"query"`
|
|
181
|
+
* and `"both"` — as the server model and the custom-actions guide have always
|
|
182
|
+
* documented. Both grids previously tested for `"list"`, a value nothing emits, so an
|
|
183
|
+
* action authored per the documentation rendered nowhere at all.
|
|
184
|
+
*
|
|
185
|
+
* ⚠️ This narrows what is DISPLAYED. It is NOT an authorization boundary: the grant
|
|
186
|
+
* is, and it is enforced independently in `ExecuteCustomAction` regardless of which
|
|
187
|
+
* query the caller clicked from — a caller can always POST directly.
|
|
188
|
+
*/
|
|
189
|
+
function filterQueryActions(actions) {
|
|
190
|
+
return actions.filter(a => a.showedOn === 'query' || a.showedOn === 'both');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Parses a custom action's `selectionRule` — a cardinality expression over the number
|
|
195
|
+
* of selected rows — into a predicate.
|
|
196
|
+
*
|
|
197
|
+
* A port of the server's `SelectionRuleParser`, and the two MUST agree: they are tested
|
|
198
|
+
* against one shared fixture (`selection-rule.fixture.json`) for exactly this reason.
|
|
199
|
+
* Vidyano, where this grammar comes from, has the same algorithm in C# and JavaScript and
|
|
200
|
+
* the two have already drifted — one throws on a non-numeric operand where the other
|
|
201
|
+
* silently permits everything.
|
|
202
|
+
*
|
|
203
|
+
* Grammar: `X` is the count placeholder, whitespace is insignificant, terms split on `X`
|
|
204
|
+
* are AND-combined (`1<X<5` is a range), operators are `<= >= < > != =` matched in that
|
|
205
|
+
* order so `>=` is never read as `>`, and a number-first term is mirrored (`0<X` is `>0`).
|
|
206
|
+
*
|
|
207
|
+
* Client-side this only drives whether a button is disabled. The server enforces the same
|
|
208
|
+
* rule independently — and neither is an authorization boundary: the action's grant is.
|
|
209
|
+
*/
|
|
210
|
+
function parseSelectionRule(rule) {
|
|
211
|
+
if (!rule || !rule.trim())
|
|
212
|
+
return () => true;
|
|
213
|
+
try {
|
|
214
|
+
return compile(rule);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Unlike the server, which refuses to start on a malformed rule, the client cannot
|
|
218
|
+
// usefully fail: the rule arrived over the wire from a server that already validated
|
|
219
|
+
// it. Disabling the button is the safe direction — it never permits an action the
|
|
220
|
+
// server would refuse.
|
|
221
|
+
return () => false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const OPERATORS = ['<=', '>=', '<', '>', '!=', '='];
|
|
225
|
+
function compile(rule) {
|
|
226
|
+
const normalized = rule.replace(/ /g, '').toUpperCase();
|
|
227
|
+
const terms = normalized.split('X').filter(t => t.length > 0);
|
|
228
|
+
if (terms.length === 0)
|
|
229
|
+
throw new Error(`Selection rule '${rule}' has no condition.`);
|
|
230
|
+
const numberFirst = !normalized.startsWith('X') && normalized.includes('X');
|
|
231
|
+
const predicates = terms.map((term, i) => compileTerm(term, rule, numberFirst && i === 0));
|
|
232
|
+
return count => predicates.every(p => p(count));
|
|
233
|
+
}
|
|
234
|
+
function compileTerm(term, rule, mirrored) {
|
|
235
|
+
const op = OPERATORS.find(o => (mirrored ? term.endsWith(o) : term.startsWith(o)));
|
|
236
|
+
if (!op)
|
|
237
|
+
throw new Error(`Selection rule '${rule}' has no recognised operator in '${term}'.`);
|
|
238
|
+
const numberPart = mirrored ? term.slice(0, term.length - op.length) : term.slice(op.length);
|
|
239
|
+
// Number(' ') is 0 and Number('1.5') is 1.5 — neither is a valid operand here.
|
|
240
|
+
if (!/^-?\d+$/.test(numberPart)) {
|
|
241
|
+
throw new Error(`Selection rule '${rule}' has a non-numeric operand in '${term}'.`);
|
|
242
|
+
}
|
|
243
|
+
const value = Number(numberPart);
|
|
244
|
+
const effective = mirrored ? mirror(op) : op;
|
|
245
|
+
switch (effective) {
|
|
246
|
+
case '<=': return count => count <= value;
|
|
247
|
+
case '>=': return count => count >= value;
|
|
248
|
+
case '<': return count => count < value;
|
|
249
|
+
case '>': return count => count > value;
|
|
250
|
+
case '!=': return count => count !== value;
|
|
251
|
+
case '=': return count => count === value;
|
|
252
|
+
default: throw new Error(`Selection rule '${rule}' has an unsupported operator '${effective}'.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function mirror(op) {
|
|
256
|
+
switch (op) {
|
|
257
|
+
case '<': return '>';
|
|
258
|
+
case '>': return '<';
|
|
259
|
+
case '<=': return '>=';
|
|
260
|
+
case '>=': return '<=';
|
|
261
|
+
default: return op;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The selection mode a grid needs in order to satisfy the actions offered on it.
|
|
267
|
+
*
|
|
268
|
+
* Derived rather than configured, so a grid gains a checkbox column exactly when an
|
|
269
|
+
* action needs one and is otherwise pixel-identical to a grid with no selection at all.
|
|
270
|
+
* Vidyano's query grid does the same thing — it renders the checkbox column only if some
|
|
271
|
+
* action is selection-gated.
|
|
272
|
+
*
|
|
273
|
+
* `'single'` when every gated action is satisfied by one row and refused by two; anything
|
|
274
|
+
* else that cares about the count gets `'multiple'`.
|
|
275
|
+
*/
|
|
276
|
+
function selectionModeFor(actions) {
|
|
277
|
+
// An action with no rule is not selection-gated: it acts on the query, not on rows.
|
|
278
|
+
const gated = actions.filter(a => !!a.selectionRule?.trim());
|
|
279
|
+
if (gated.length === 0)
|
|
280
|
+
return 'none';
|
|
281
|
+
const everyRuleWantsExactlyOne = gated.every(a => {
|
|
282
|
+
const rule = parseSelectionRule(a.selectionRule);
|
|
283
|
+
return rule(1) && !rule(2);
|
|
284
|
+
});
|
|
285
|
+
return everyRuleWantsExactlyOne ? 'single' : 'multiple';
|
|
286
|
+
}
|
|
287
|
+
|
|
177
288
|
/**
|
|
178
289
|
* Generated bundle index. Do not edit.
|
|
179
290
|
*/
|
|
180
291
|
|
|
181
|
-
export { AS_DETAIL_BREADCRUMBS_KEY, ELookupDisplayType, EReferenceDisplayType, ShowedOn, currentLanguage, dictToNestedPo, hasShowedOnFlag, nestedPoToDict, nestedPoToDisplayRow, resolveTranslation };
|
|
292
|
+
export { AS_DETAIL_BREADCRUMBS_KEY, ELookupDisplayType, EReferenceDisplayType, ShowedOn, currentLanguage, dictToNestedPo, filterQueryActions, hasShowedOnFlag, nestedPoToDict, nestedPoToDisplayRow, parseSelectionRule, resolveTranslation, selectionModeFor };
|
|
182
293
|
//# sourceMappingURL=mintplayer-ng-spark-models.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-models.mjs","sources":["../../models/src/translated-string.ts","../../models/src/showed-on.ts","../../models/src/entity-type.ts","../../models/src/lookup-reference.ts","../../models/src/as-detail-conversions.ts","../../models/mintplayer-ng-spark-models.ts"],"sourcesContent":["import { signal, type WritableSignal } from '@angular/core';\n\nexport type TranslatedString = Record<string, string>;\n\n/** Global reactive language state — shared across library boundaries via globalThis */\nexport const currentLanguage: WritableSignal<string> =\n ((globalThis as any).__sparkCurrentLanguage ??= signal('en'));\n\nexport function resolveTranslation(ts: TranslatedString | undefined, lang?: string): string {\n if (!ts) return '';\n const language = lang ?? currentLanguage();\n return ts[language] ?? ts['en'] ?? Object.values(ts)[0] ?? '';\n}\n","/**\n * Flags enum controlling on which pages an attribute should be displayed.\n * Values can be combined: ShowedOn.Query | ShowedOn.PersistentObject\n */\nexport enum ShowedOn {\n Query = 1,\n PersistentObject = 2,\n}\n\n/**\n * Helper function to check if a ShowedOn value includes a specific flag.\n */\nexport function hasShowedOnFlag(value: ShowedOn | string | undefined, flag: ShowedOn): boolean {\n if (value === undefined) return true; // Default: show on all pages\n\n // Handle string values from JSON (e.g., \"Query, PersistentObject\")\n if (typeof value === 'string') {\n const parts = value.split(',').map(s => s.trim());\n const flagName = ShowedOn[flag];\n return parts.includes(flagName);\n }\n\n // Handle numeric flag values\n return (value & flag) === flag;\n}\n","import { ShowedOn } from './showed-on';\nimport { TranslatedString } from './translated-string';\nimport { ValidationRule } from './validation-rule';\n\n/**\n * Controls how a Reference attribute is picked in the PO-edit form.\n * Serialized as a string by the server (mirrors the .NET EReferenceDisplayType).\n */\nexport enum EReferenceDisplayType {\n /** Renders as a `<bs-select>` listing every referenced item. */\n Dropdown = 'Dropdown',\n /** Renders a readonly textbox + \"…\" button that opens a searchable modal grid picker. */\n Modal = 'Modal',\n}\n\nexport interface EntityAttributeDefinition {\n id: string;\n name: string;\n label?: TranslatedString;\n dataType: string;\n isRequired: boolean;\n isVisible: boolean;\n isReadOnly: boolean;\n order: number;\n query?: string;\n /** For reference attributes, specifies the target entity type's CLR type name */\n referenceType?: string;\n /** For AsDetail attributes, specifies the nested entity type's CLR type name */\n asDetailType?: string;\n /** When true, the attribute represents an array/collection of AsDetail objects */\n isArray?: boolean;\n /** For array AsDetail attributes: \"modal\" (default) or \"inline\" */\n editMode?: 'inline' | 'modal';\n /**\n * For Reference attributes: 'Modal' renders the \"…\" + modal query-grid picker;\n * 'Dropdown'/absent (default) renders a `<bs-select>`. Hand-set in the model JSON.\n */\n referenceDisplayType?: EReferenceDisplayType;\n /** For array AsDetail attributes: when true, rows can be drag-reordered (order = array position) */\n isSortable?: boolean;\n /** For LookupReference attributes, specifies the lookup reference type name */\n lookupReferenceType?: string;\n /**\n * Controls on which pages the attribute should be displayed.\n * Query = shown in list views, PersistentObject = shown in detail/edit views.\n * Can be a numeric flag value or a string like \"Query, PersistentObject\".\n */\n showedOn?: ShowedOn | string;\n rules: ValidationRule[];\n /** References an AttributeGroup.id to assign this attribute to a group */\n group?: string;\n /** Number of grid columns this attribute spans within a tab's column layout */\n columnSpan?: number;\n /** Renderer component name for custom display in detail/list views */\n renderer?: string;\n /** Options passed to the renderer component */\n rendererOptions?: Record<string, any>;\n}\n\nexport interface AttributeTab {\n id: string;\n name: string;\n label?: TranslatedString;\n order: number;\n /** Number of columns for the grid layout within this tab */\n columnCount?: number;\n}\n\nexport interface AttributeGroup {\n id: string;\n name: string;\n label?: TranslatedString;\n /** References an AttributeTab.id to assign this group to a tab */\n tab?: string;\n order: number;\n}\n\nexport interface EntityType {\n id: string;\n name: string;\n description?: TranslatedString;\n clrType: string;\n alias?: string;\n /**\n * Breadcrumb template: literal text plus `{AttributeName}` placeholders. A scalar placeholder\n * renders its value; a reference placeholder renders the referenced entity's breadcrumb.\n * The server resolves this — clients only read the resulting strings. Example: \"{Street}, {City}\".\n */\n breadcrumb?: string;\n /**\n * When false, the breadcrumb needs the collection document (a placeholder field is not on the\n * projection). null/absent means renderable from the projection. Informational on the client.\n */\n breadcrumbProjectionSatisfiable?: boolean;\n tabs?: AttributeTab[];\n groups?: AttributeGroup[];\n attributes: EntityAttributeDefinition[];\n /** Query aliases or IDs to display as related query tables on the detail page. */\n queries?: string[];\n}\n","import { TranslatedString } from './translated-string';\n\nexport enum ELookupDisplayType {\n Dropdown = 0,\n Modal = 1\n}\n\nexport interface LookupReferenceListItem {\n name: string;\n isTransient: boolean;\n valueCount: number;\n displayType: ELookupDisplayType;\n}\n\nexport interface LookupReference {\n name: string;\n isTransient: boolean;\n displayType: ELookupDisplayType;\n values: LookupReferenceValue[];\n}\n\nexport interface LookupReferenceValue {\n key: string;\n values: TranslatedString;\n isActive: boolean;\n extra?: Record<string, unknown>;\n}\n","import { EntityAttributeDefinition } from './entity-type';\nimport { EntityType } from './entity-type';\nimport { PersistentObject } from './persistent-object';\nimport { PersistentObjectAttribute } from './persistent-object-attribute';\n\n/**\n * Resolves an `EntityType` by its CLR type name (e.g. `\"HR.Entities.Address\"`).\n * Callers typically close over `sparkService.getEntityTypes()`'s cached list.\n */\nexport type EntityTypeResolver = (clrTypeName: string) => EntityType | undefined;\n\n/**\n * Flattens a nested `PersistentObject` into the plain `Record<string, any>` shape the\n * form state uses throughout ng-spark. Primitive / reference attributes contribute their\n * `value`; nested AsDetail attributes recurse — single becomes an inner dict, array\n * becomes an array of inner dicts. Returns `{}` for `null` / `undefined` input.\n *\n * This is the ONE place that reads the server's new AsDetail wire shape and collapses it\n * back to the flat dict the form components already handle.\n */\nexport function nestedPoToDict(po: PersistentObject | null | undefined): Record<string, any> {\n if (!po) return {};\n const dict: Record<string, any> = {};\n for (const attr of po.attributes ?? []) {\n dict[attr.name] = attributeValueForForm(attr);\n }\n return dict;\n}\n\nfunction attributeValueForForm(attr: PersistentObjectAttribute): any {\n if (attr.dataType === 'AsDetail') {\n if (attr.isArray) return (attr.objects ?? []).map(po => nestedPoToDict(po));\n return attr.object ? nestedPoToDict(attr.object) : null;\n }\n return attr.value;\n}\n\n/**\n * Reserved key under which {@link nestedPoToDisplayRow} stashes the server-resolved breadcrumb of\n * each reference attribute (keyed by attribute name). Lets an AsDetail reference cell render the\n * label the server already resolved by id — page-independent — instead of guessing from a single\n * reference-query options page. Prefixed to avoid colliding with a real attribute name.\n */\nexport const AS_DETAIL_BREADCRUMBS_KEY = '__sparkBreadcrumbs';\n\n/**\n * Like {@link nestedPoToDict}, but for the read-only detail display path. In addition to each\n * attribute's value it preserves the server-resolved per-reference `breadcrumb` under\n * {@link AS_DETAIL_BREADCRUMBS_KEY}, so an AsDetail reference cell can render the label by id\n * regardless of whether the referenced document fits on the reference query's first options page.\n * The form/edit path keeps using {@link nestedPoToDict}, which never carries breadcrumbs.\n */\nexport function nestedPoToDisplayRow(po: PersistentObject | null | undefined): Record<string, any> {\n if (!po) return {};\n const dict: Record<string, any> = {};\n let breadcrumbs: Record<string, string> | undefined;\n for (const attr of po.attributes ?? []) {\n dict[attr.name] = displayValueForAttribute(attr);\n if (attr.dataType === 'Reference' && !attr.isArray && typeof attr.breadcrumb === 'string' && attr.breadcrumb !== '') {\n (breadcrumbs ??= {})[attr.name] = attr.breadcrumb;\n }\n }\n // Only attach the side channel when something resolved — keeps reference-free rows (the common\n // case) byte-for-byte identical to the plain flat dict.\n if (breadcrumbs) dict[AS_DETAIL_BREADCRUMBS_KEY] = breadcrumbs;\n return dict;\n}\n\nfunction displayValueForAttribute(attr: PersistentObjectAttribute): any {\n if (attr.dataType === 'AsDetail') {\n if (attr.isArray) return (attr.objects ?? []).map(po => nestedPoToDisplayRow(po));\n return attr.object ? nestedPoToDisplayRow(attr.object) : null;\n }\n return attr.value;\n}\n\n/**\n * Builds a nested `PersistentObject` from a flat dict against the schema in\n * <paramref name=\"entityType\"/>. Used when the form is about to save — AsDetail attributes\n * are no longer sent as flat dicts in `attribute.value`; the server now requires\n * `attribute.object` / `attribute.objects` with fully scaffolded nested POs.\n *\n * `resolve` walks through AsDetail types registered elsewhere (usually the full\n * `getEntityTypes()` list, keyed by CLR type name). Nested AsDetail inside AsDetail is\n * handled recursively.\n */\nexport function dictToNestedPo(\n dict: Record<string, any> | null | undefined,\n entityType: EntityType,\n resolve: EntityTypeResolver,\n): PersistentObject {\n const attributes: PersistentObjectAttribute[] = (entityType.attributes ?? [])\n .map(attrDef => buildAttribute(attrDef, dict?.[attrDef.name], resolve));\n\n return {\n id: (dict?.['Id'] as string) ?? (dict?.['id'] as string) ?? '',\n name: entityType.name,\n objectTypeId: entityType.id,\n attributes,\n };\n}\n\nfunction buildAttribute(\n attrDef: EntityAttributeDefinition,\n raw: any,\n resolve: EntityTypeResolver,\n): PersistentObjectAttribute {\n const attr: PersistentObjectAttribute = {\n id: attrDef.id,\n name: attrDef.name,\n label: attrDef.label,\n dataType: attrDef.dataType,\n isArray: attrDef.isArray,\n isRequired: attrDef.isRequired,\n isVisible: attrDef.isVisible,\n isReadOnly: attrDef.isReadOnly,\n order: attrDef.order,\n rules: attrDef.rules ?? [],\n isValueChanged: true,\n };\n\n if (attrDef.dataType === 'AsDetail') {\n // Server expects attr.value null for AsDetail; the nested PO carries the data.\n attr.value = null;\n attr.asDetailType = attrDef.asDetailType;\n\n const nestedType = attrDef.asDetailType ? resolve(attrDef.asDetailType) : undefined;\n if (!nestedType) {\n attr.object = null;\n attr.objects = attrDef.isArray ? [] : null;\n return attr;\n }\n\n if (attrDef.isArray) {\n const items: any[] = Array.isArray(raw) ? raw : [];\n attr.objects = items.map(item => dictToNestedPo((item as Record<string, any>) ?? {}, nestedType, resolve));\n } else {\n attr.object = raw ? dictToNestedPo(raw as Record<string, any>, nestedType, resolve) : null;\n }\n return attr;\n }\n\n attr.value = raw;\n return attr;\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-models.mjs","sources":["../../models/src/translated-string.ts","../../models/src/showed-on.ts","../../models/src/entity-type.ts","../../models/src/lookup-reference.ts","../../models/src/as-detail-conversions.ts","../../models/src/query-actions.ts","../../models/src/selection-rule.ts","../../models/src/selection-mode.ts","../../models/mintplayer-ng-spark-models.ts"],"sourcesContent":["import { signal, type WritableSignal } from '@angular/core';\n\nexport type TranslatedString = Record<string, string>;\n\n/** Global reactive language state — shared across library boundaries via globalThis */\nexport const currentLanguage: WritableSignal<string> =\n ((globalThis as any).__sparkCurrentLanguage ??= signal('en'));\n\nexport function resolveTranslation(ts: TranslatedString | undefined, lang?: string): string {\n if (!ts) return '';\n const language = lang ?? currentLanguage();\n return ts[language] ?? ts['en'] ?? Object.values(ts)[0] ?? '';\n}\n","/**\n * Flags enum controlling on which pages an attribute should be displayed.\n * Values can be combined: ShowedOn.Query | ShowedOn.PersistentObject\n */\nexport enum ShowedOn {\n Query = 1,\n PersistentObject = 2,\n}\n\n/**\n * Helper function to check if a ShowedOn value includes a specific flag.\n */\nexport function hasShowedOnFlag(value: ShowedOn | string | undefined, flag: ShowedOn): boolean {\n if (value === undefined) return true; // Default: show on all pages\n\n // Handle string values from JSON (e.g., \"Query, PersistentObject\")\n if (typeof value === 'string') {\n const parts = value.split(',').map(s => s.trim());\n const flagName = ShowedOn[flag];\n return parts.includes(flagName);\n }\n\n // Handle numeric flag values\n return (value & flag) === flag;\n}\n","import { ShowedOn } from './showed-on';\nimport { TranslatedString } from './translated-string';\nimport { ValidationRule } from './validation-rule';\n\n/**\n * Controls how a Reference attribute is picked in the PO-edit form.\n * Serialized as a string by the server (mirrors the .NET EReferenceDisplayType).\n */\nexport enum EReferenceDisplayType {\n /** Renders as a `<bs-select>` listing every referenced item. */\n Dropdown = 'Dropdown',\n /** Renders a readonly textbox + \"…\" button that opens a searchable modal grid picker. */\n Modal = 'Modal',\n}\n\nexport interface EntityAttributeDefinition {\n id: string;\n name: string;\n label?: TranslatedString;\n dataType: string;\n isRequired: boolean;\n isVisible: boolean;\n isReadOnly: boolean;\n order: number;\n query?: string;\n /** For reference attributes, specifies the target entity type's CLR type name */\n referenceType?: string;\n /** For AsDetail attributes, specifies the nested entity type's CLR type name */\n asDetailType?: string;\n /** When true, the attribute represents an array/collection of AsDetail objects */\n isArray?: boolean;\n /** For array AsDetail attributes: \"modal\" (default) or \"inline\" */\n editMode?: 'inline' | 'modal';\n /**\n * For Reference attributes: 'Modal' renders the \"…\" + modal query-grid picker;\n * 'Dropdown'/absent (default) renders a `<bs-select>`. Hand-set in the model JSON.\n */\n referenceDisplayType?: EReferenceDisplayType;\n /** For array AsDetail attributes: when true, rows can be drag-reordered (order = array position) */\n isSortable?: boolean;\n /** For LookupReference attributes, specifies the lookup reference type name */\n lookupReferenceType?: string;\n /**\n * Controls on which pages the attribute should be displayed.\n * Query = shown in list views, PersistentObject = shown in detail/edit views.\n * Can be a numeric flag value or a string like \"Query, PersistentObject\".\n */\n showedOn?: ShowedOn | string;\n rules: ValidationRule[];\n /** References an AttributeGroup.id to assign this attribute to a group */\n group?: string;\n /** Number of grid columns this attribute spans within a tab's column layout */\n columnSpan?: number;\n /** Renderer component name for custom display in detail/list views */\n renderer?: string;\n /** Options passed to the renderer component */\n rendererOptions?: Record<string, any>;\n}\n\nexport interface AttributeTab {\n id: string;\n name: string;\n label?: TranslatedString;\n order: number;\n /** Number of columns for the grid layout within this tab */\n columnCount?: number;\n}\n\nexport interface AttributeGroup {\n id: string;\n name: string;\n label?: TranslatedString;\n /** References an AttributeTab.id to assign this group to a tab */\n tab?: string;\n order: number;\n}\n\nexport interface EntityType {\n id: string;\n name: string;\n description?: TranslatedString;\n clrType: string;\n alias?: string;\n /**\n * Breadcrumb template: literal text plus `{AttributeName}` placeholders. A scalar placeholder\n * renders its value; a reference placeholder renders the referenced entity's breadcrumb.\n * The server resolves this — clients only read the resulting strings. Example: \"{Street}, {City}\".\n */\n breadcrumb?: string;\n /**\n * When false, the breadcrumb needs the collection document (a placeholder field is not on the\n * projection). null/absent means renderable from the projection. Informational on the client.\n */\n breadcrumbProjectionSatisfiable?: boolean;\n tabs?: AttributeTab[];\n groups?: AttributeGroup[];\n attributes: EntityAttributeDefinition[];\n /** Query aliases or IDs to display as related query tables on the detail page. */\n queries?: string[];\n}\n","import { TranslatedString } from './translated-string';\n\nexport enum ELookupDisplayType {\n Dropdown = 0,\n Modal = 1\n}\n\nexport interface LookupReferenceListItem {\n name: string;\n isTransient: boolean;\n valueCount: number;\n displayType: ELookupDisplayType;\n}\n\nexport interface LookupReference {\n name: string;\n isTransient: boolean;\n displayType: ELookupDisplayType;\n values: LookupReferenceValue[];\n}\n\nexport interface LookupReferenceValue {\n key: string;\n values: TranslatedString;\n isActive: boolean;\n extra?: Record<string, unknown>;\n}\n","import { EntityAttributeDefinition } from './entity-type';\nimport { EntityType } from './entity-type';\nimport { PersistentObject } from './persistent-object';\nimport { PersistentObjectAttribute } from './persistent-object-attribute';\n\n/**\n * Resolves an `EntityType` by its CLR type name (e.g. `\"HR.Entities.Address\"`).\n * Callers typically close over `sparkService.getEntityTypes()`'s cached list.\n */\nexport type EntityTypeResolver = (clrTypeName: string) => EntityType | undefined;\n\n/**\n * Flattens a nested `PersistentObject` into the plain `Record<string, any>` shape the\n * form state uses throughout ng-spark. Primitive / reference attributes contribute their\n * `value`; nested AsDetail attributes recurse — single becomes an inner dict, array\n * becomes an array of inner dicts. Returns `{}` for `null` / `undefined` input.\n *\n * This is the ONE place that reads the server's new AsDetail wire shape and collapses it\n * back to the flat dict the form components already handle.\n */\nexport function nestedPoToDict(po: PersistentObject | null | undefined): Record<string, any> {\n if (!po) return {};\n const dict: Record<string, any> = {};\n for (const attr of po.attributes ?? []) {\n dict[attr.name] = attributeValueForForm(attr);\n }\n return dict;\n}\n\nfunction attributeValueForForm(attr: PersistentObjectAttribute): any {\n if (attr.dataType === 'AsDetail') {\n if (attr.isArray) return (attr.objects ?? []).map(po => nestedPoToDict(po));\n return attr.object ? nestedPoToDict(attr.object) : null;\n }\n return attr.value;\n}\n\n/**\n * Reserved key under which {@link nestedPoToDisplayRow} stashes the server-resolved breadcrumb of\n * each reference attribute (keyed by attribute name). Lets an AsDetail reference cell render the\n * label the server already resolved by id — page-independent — instead of guessing from a single\n * reference-query options page. Prefixed to avoid colliding with a real attribute name.\n */\nexport const AS_DETAIL_BREADCRUMBS_KEY = '__sparkBreadcrumbs';\n\n/**\n * Like {@link nestedPoToDict}, but for the read-only detail display path. In addition to each\n * attribute's value it preserves the server-resolved per-reference `breadcrumb` under\n * {@link AS_DETAIL_BREADCRUMBS_KEY}, so an AsDetail reference cell can render the label by id\n * regardless of whether the referenced document fits on the reference query's first options page.\n * The form/edit path keeps using {@link nestedPoToDict}, which never carries breadcrumbs.\n */\nexport function nestedPoToDisplayRow(po: PersistentObject | null | undefined): Record<string, any> {\n if (!po) return {};\n const dict: Record<string, any> = {};\n let breadcrumbs: Record<string, string> | undefined;\n for (const attr of po.attributes ?? []) {\n dict[attr.name] = displayValueForAttribute(attr);\n if (attr.dataType === 'Reference' && !attr.isArray && typeof attr.breadcrumb === 'string' && attr.breadcrumb !== '') {\n (breadcrumbs ??= {})[attr.name] = attr.breadcrumb;\n }\n }\n // Only attach the side channel when something resolved — keeps reference-free rows (the common\n // case) byte-for-byte identical to the plain flat dict.\n if (breadcrumbs) dict[AS_DETAIL_BREADCRUMBS_KEY] = breadcrumbs;\n return dict;\n}\n\nfunction displayValueForAttribute(attr: PersistentObjectAttribute): any {\n if (attr.dataType === 'AsDetail') {\n if (attr.isArray) return (attr.objects ?? []).map(po => nestedPoToDisplayRow(po));\n return attr.object ? nestedPoToDisplayRow(attr.object) : null;\n }\n return attr.value;\n}\n\n/**\n * Builds a nested `PersistentObject` from a flat dict against the schema in\n * <paramref name=\"entityType\"/>. Used when the form is about to save — AsDetail attributes\n * are no longer sent as flat dicts in `attribute.value`; the server now requires\n * `attribute.object` / `attribute.objects` with fully scaffolded nested POs.\n *\n * `resolve` walks through AsDetail types registered elsewhere (usually the full\n * `getEntityTypes()` list, keyed by CLR type name). Nested AsDetail inside AsDetail is\n * handled recursively.\n */\nexport function dictToNestedPo(\n dict: Record<string, any> | null | undefined,\n entityType: EntityType,\n resolve: EntityTypeResolver,\n): PersistentObject {\n const attributes: PersistentObjectAttribute[] = (entityType.attributes ?? [])\n .map(attrDef => buildAttribute(attrDef, dict?.[attrDef.name], resolve));\n\n return {\n id: (dict?.['Id'] as string) ?? (dict?.['id'] as string) ?? '',\n name: entityType.name,\n objectTypeId: entityType.id,\n attributes,\n };\n}\n\nfunction buildAttribute(\n attrDef: EntityAttributeDefinition,\n raw: any,\n resolve: EntityTypeResolver,\n): PersistentObjectAttribute {\n const attr: PersistentObjectAttribute = {\n id: attrDef.id,\n name: attrDef.name,\n label: attrDef.label,\n dataType: attrDef.dataType,\n isArray: attrDef.isArray,\n isRequired: attrDef.isRequired,\n isVisible: attrDef.isVisible,\n isReadOnly: attrDef.isReadOnly,\n order: attrDef.order,\n rules: attrDef.rules ?? [],\n isValueChanged: true,\n };\n\n if (attrDef.dataType === 'AsDetail') {\n // Server expects attr.value null for AsDetail; the nested PO carries the data.\n attr.value = null;\n attr.asDetailType = attrDef.asDetailType;\n\n const nestedType = attrDef.asDetailType ? resolve(attrDef.asDetailType) : undefined;\n if (!nestedType) {\n attr.object = null;\n attr.objects = attrDef.isArray ? [] : null;\n return attr;\n }\n\n if (attrDef.isArray) {\n const items: any[] = Array.isArray(raw) ? raw : [];\n attr.objects = items.map(item => dictToNestedPo((item as Record<string, any>) ?? {}, nestedType, resolve));\n } else {\n attr.object = raw ? dictToNestedPo(raw as Record<string, any>, nestedType, resolve) : null;\n }\n return attr;\n }\n\n attr.value = raw;\n return attr;\n}\n","import { CustomActionDefinition } from './custom-action';\n\n/**\n * The custom actions a query should offer, from the entity type's full set.\n *\n * `showedOn` must include the query side. The accepted values are `\"detail\"`, `\"query\"`\n * and `\"both\"` — as the server model and the custom-actions guide have always\n * documented. Both grids previously tested for `\"list\"`, a value nothing emits, so an\n * action authored per the documentation rendered nowhere at all.\n *\n * ⚠️ This narrows what is DISPLAYED. It is NOT an authorization boundary: the grant\n * is, and it is enforced independently in `ExecuteCustomAction` regardless of which\n * query the caller clicked from — a caller can always POST directly.\n */\nexport function filterQueryActions(\n actions: CustomActionDefinition[],\n): CustomActionDefinition[] {\n return actions.filter(a => a.showedOn === 'query' || a.showedOn === 'both');\n}\n","/**\n * Parses a custom action's `selectionRule` — a cardinality expression over the number\n * of selected rows — into a predicate.\n *\n * A port of the server's `SelectionRuleParser`, and the two MUST agree: they are tested\n * against one shared fixture (`selection-rule.fixture.json`) for exactly this reason.\n * Vidyano, where this grammar comes from, has the same algorithm in C# and JavaScript and\n * the two have already drifted — one throws on a non-numeric operand where the other\n * silently permits everything.\n *\n * Grammar: `X` is the count placeholder, whitespace is insignificant, terms split on `X`\n * are AND-combined (`1<X<5` is a range), operators are `<= >= < > != =` matched in that\n * order so `>=` is never read as `>`, and a number-first term is mirrored (`0<X` is `>0`).\n *\n * Client-side this only drives whether a button is disabled. The server enforces the same\n * rule independently — and neither is an authorization boundary: the action's grant is.\n */\nexport function parseSelectionRule(rule?: string | null): (count: number) => boolean {\n if (!rule || !rule.trim()) return () => true;\n try {\n return compile(rule);\n } catch {\n // Unlike the server, which refuses to start on a malformed rule, the client cannot\n // usefully fail: the rule arrived over the wire from a server that already validated\n // it. Disabling the button is the safe direction — it never permits an action the\n // server would refuse.\n return () => false;\n }\n}\n\nconst OPERATORS = ['<=', '>=', '<', '>', '!=', '='] as const;\n\nfunction compile(rule: string): (count: number) => boolean {\n const normalized = rule.replace(/ /g, '').toUpperCase();\n const terms = normalized.split('X').filter(t => t.length > 0);\n if (terms.length === 0) throw new Error(`Selection rule '${rule}' has no condition.`);\n\n const numberFirst = !normalized.startsWith('X') && normalized.includes('X');\n const predicates = terms.map((term, i) => compileTerm(term, rule, numberFirst && i === 0));\n\n return count => predicates.every(p => p(count));\n}\n\nfunction compileTerm(term: string, rule: string, mirrored: boolean): (count: number) => boolean {\n const op = OPERATORS.find(o => (mirrored ? term.endsWith(o) : term.startsWith(o)));\n if (!op) throw new Error(`Selection rule '${rule}' has no recognised operator in '${term}'.`);\n\n const numberPart = mirrored ? term.slice(0, term.length - op.length) : term.slice(op.length);\n // Number(' ') is 0 and Number('1.5') is 1.5 — neither is a valid operand here.\n if (!/^-?\\d+$/.test(numberPart)) {\n throw new Error(`Selection rule '${rule}' has a non-numeric operand in '${term}'.`);\n }\n const value = Number(numberPart);\n const effective = mirrored ? mirror(op) : op;\n\n switch (effective) {\n case '<=': return count => count <= value;\n case '>=': return count => count >= value;\n case '<': return count => count < value;\n case '>': return count => count > value;\n case '!=': return count => count !== value;\n case '=': return count => count === value;\n default: throw new Error(`Selection rule '${rule}' has an unsupported operator '${effective}'.`);\n }\n}\n\nfunction mirror(op: string): string {\n switch (op) {\n case '<': return '>';\n case '>': return '<';\n case '<=': return '>=';\n case '>=': return '<=';\n default: return op;\n }\n}\n","import { CustomActionDefinition } from './custom-action';\nimport { parseSelectionRule } from './selection-rule';\n\nexport type SparkSelectionMode = 'none' | 'single' | 'multiple';\n\n/**\n * The selection mode a grid needs in order to satisfy the actions offered on it.\n *\n * Derived rather than configured, so a grid gains a checkbox column exactly when an\n * action needs one and is otherwise pixel-identical to a grid with no selection at all.\n * Vidyano's query grid does the same thing — it renders the checkbox column only if some\n * action is selection-gated.\n *\n * `'single'` when every gated action is satisfied by one row and refused by two; anything\n * else that cares about the count gets `'multiple'`.\n */\nexport function selectionModeFor(actions: CustomActionDefinition[]): SparkSelectionMode {\n // An action with no rule is not selection-gated: it acts on the query, not on rows.\n const gated = actions.filter(a => !!a.selectionRule?.trim());\n if (gated.length === 0) return 'none';\n\n const everyRuleWantsExactlyOne = gated.every(a => {\n const rule = parseSelectionRule(a.selectionRule);\n return rule(1) && !rule(2);\n });\n\n return everyRuleWantsExactlyOne ? 'single' : 'multiple';\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -139,11 +139,11 @@ class SparkPoCreateComponent {
|
|
|
139
139
|
window.history.back();
|
|
140
140
|
}
|
|
141
141
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkPoCreateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
142
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: SparkPoCreateComponent, isStandalone: true, selector: "spark-po-create", outputs: { saved: "saved", cancelled: "cancelled" }, ngImport: i0, template: "<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", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsContainerComponent, selector: "bs-container" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkPoFormComponent, selector: "spark-po-form", inputs: ["entityType", "formData", "validationErrors", "showButtons", "isSaving", "parentId", "parentType"], outputs: ["formDataChange", "save", "cancel"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
142
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: SparkPoCreateComponent, isStandalone: true, selector: "spark-po-create", outputs: { saved: "saved", cancelled: "cancelled" }, ngImport: i0, template: "<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", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsContainerComponent, selector: "bs-container" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkPoFormComponent, selector: "spark-po-form", inputs: ["entityType", "formData", "validationErrors", "showButtons", "isSaving", "parentId", "parentType"], outputs: ["formDataChange", "save", "cancel"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
143
143
|
}
|
|
144
144
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkPoCreateComponent, decorators: [{
|
|
145
145
|
type: Component,
|
|
146
|
-
args: [{ selector: 'spark-po-create', imports: [CommonModule, BsAlertComponent, BsContainerComponent, BsSpinnerComponent, SparkPoFormComponent, ResolveTranslationPipe, TranslateKeyPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<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" }]
|
|
146
|
+
args: [{ selector: 'spark-po-create', imports: [CommonModule, BsAlertComponent, BsContainerComponent, BsSpinnerComponent, SparkPoFormComponent, ResolveTranslationPipe, TranslateKeyPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<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" }]
|
|
147
147
|
}], ctorParameters: () => [], propDecorators: { saved: [{ type: i0.Output, args: ["saved"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }] } });
|
|
148
148
|
|
|
149
149
|
/**
|