@mintplayer/ng-spark 22.26.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.
@@ -1 +1 @@
1
- {"version":3,"file":"mintplayer-ng-spark-soft-delete.mjs","names":[],"sources":["../../soft-delete/src/spark-soft-delete.service.ts","../../soft-delete/src/spark-deleted-toggle.component.ts","../../soft-delete/src/spark-soft-delete-actions.component.ts","../../soft-delete/src/provide-spark-soft-delete.ts"],"sourcesContent":["import { inject, Injectable } from '@angular/core';\nimport { PersistentObject } from '@mintplayer/ng-spark/models';\nimport { SparkService } from '@mintplayer/ng-spark/services';\n\n/**\n * Client for the SoftDelete package's endpoints (`MintPlayer.Spark.SoftDelete`, #460 T1). Ids travel\n * in the body — the route table is literal and Raven ids contain slashes.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkSoftDeleteService {\n private readonly spark = inject(SparkService);\n\n /**\n * `POST /spark/po/restore` → the restored (live) object. Requires `Restore/T` and a deleted row;\n * anything else is 404 (or 401 anonymous), a restore withheld by `OnDisableActionsAsync` is 403.\n */\n restore(type: string, id: string): Promise<PersistentObject> {\n return this.spark.postEnvelope<PersistentObject>('/po/restore', { objectTypeId: type, id });\n }\n\n /**\n * `POST /spark/po/purge` → 204. Requires `Purge/T` and a deleted row. Removes the document and\n * every revision of it; it cannot be undone.\n */\n async purge(type: string, id: string): Promise<void> {\n await this.spark.postEnvelope<void>('/po/purge', { objectTypeId: type, id });\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkQueryListContext } from '@mintplayer/ng-spark/panels';\n\n/**\n * The query page's \"Deleted\" toggle (#460): switches the list into the recycle bin\n * (`deleted: only`) and back. Rendered only for holders of `ViewDeleted/T` — for anyone else the\n * server would ignore the widening anyway, so offering it would show the same rows under a\n * misleading label.\n *\n * Registered by {@link provideSparkSoftDelete} as a `SPARK_QUERY_LIST_ACTIONS` entry; usable on its\n * own in a custom page with any {@link SparkQueryListContext}.\n */\n@Component({\n selector: 'spark-deleted-toggle',\n imports: [TranslateKeyPipe, SparkIconComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (visible()) {\n <button type=\"button\"\n class=\"btn spark-deleted-toggle\"\n [class.btn-secondary]=\"active()\"\n [class.btn-outline-secondary]=\"!active()\"\n [attr.aria-pressed]=\"active()\"\n (click)=\"toggle()\">\n <spark-icon name=\"trash\" /> {{ 'softDelete.showDeleted' | t }}\n </button>\n }\n `,\n})\nexport class SparkDeletedToggleComponent {\n context = input.required<SparkQueryListContext>();\n\n protected readonly visible = computed(() =>\n !!this.context().entityType && this.context().permissions?.canViewDeleted === true);\n\n protected readonly active = computed(() => this.context().deleted === 'only');\n\n toggle(): void {\n this.context().setDeleted(this.active() ? 'exclude' : 'only');\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SPARK_RETURN_URL_STATE_KEY, SparkLanguageService, SparkReturnNavigationService } from '@mintplayer/ng-spark/services';\nimport { SparkDetailContext } from '@mintplayer/ng-spark/panels';\nimport { SparkSoftDeleteService } from './spark-soft-delete.service';\n\n/**\n * Restore and Purge on the detail page of a row opened from the recycle bin (#460).\n *\n * Shown only when the page loaded the row with `?deleted=only` (every row in the recycle bin is\n * deleted) and the caller holds `Restore/T` / `Purge/T`. The server re-checks both — the row must be\n * deleted and the row gate must pass — so a refusal here is the 404/403 it answers, surfaced inline.\n *\n * Registered by {@link provideSparkSoftDelete} as a `SPARK_DETAIL_ACTIONS` entry.\n */\n@Component({\n selector: 'spark-soft-delete-actions',\n imports: [TranslateKeyPipe, SparkIconComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (deletedView()) {\n <span class=\"d-inline-flex gap-2 align-items-center\">\n @if (canRestore()) {\n <button type=\"button\" class=\"btn btn-success spark-restore\" [disabled]=\"busy()\" (click)=\"restore()\">\n {{ 'softDelete.restore' | t }}\n </button>\n }\n @if (canPurge()) {\n <button type=\"button\" class=\"btn btn-danger spark-purge\" [disabled]=\"busy()\" (click)=\"purge()\">\n <spark-icon name=\"trash\" /> {{ 'softDelete.purge' | t }}\n </button>\n }\n @if (error(); as message) {\n <span class=\"text-danger small spark-soft-delete-error\" role=\"alert\">{{ message }}</span>\n }\n </span>\n }\n `,\n})\nexport class SparkSoftDeleteActionsComponent {\n private readonly softDelete = inject(SparkSoftDeleteService);\n private readonly router = inject(Router);\n private readonly route = inject(ActivatedRoute);\n private readonly returnNavigation = inject(SparkReturnNavigationService);\n private readonly lang = inject(SparkLanguageService);\n\n context = input.required<SparkDetailContext>();\n\n protected readonly deletedView = computed(() => this.context().deleted === 'only');\n protected readonly canRestore = computed(() => this.context().permissions?.canRestore === true);\n protected readonly canPurge = computed(() => this.context().permissions?.canPurge === true);\n\n protected readonly busy = signal(false);\n protected readonly error = signal<string | null>(null);\n\n async restore(): Promise<void> {\n if (this.busy()) return;\n const ctx = this.context();\n await this.run(async () => {\n await this.softDelete.restore(ctx.type, ctx.id);\n // Same page, live mode: dropping `?deleted` makes the detail page re-read the row normally.\n // The list this row was opened from travels on: a later Delete still returns there.\n const returnUrl = this.returnNavigation.recordedReturnUrl();\n await this.router.navigate([], {\n relativeTo: this.route,\n queryParams: { deleted: null },\n queryParamsHandling: 'merge',\n replaceUrl: true,\n ...(returnUrl ? { state: { [SPARK_RETURN_URL_STATE_KEY]: returnUrl } } : {}),\n });\n });\n }\n\n async purge(): Promise<void> {\n if (this.busy()) return;\n if (!confirm(this.lang.t('softDelete.confirmPurge'))) return;\n const ctx = this.context();\n await this.run(async () => {\n await this.softDelete.purge(ctx.type, ctx.id);\n // The row no longer exists: back to the recycle bin it was opened from — the recorded list, else\n // the type's list in `?deleted=only`. Never \"the previous page\", which may be anything.\n await this.returnNavigation.returnToList(ctx.entityType?.name, { deletedView: true });\n });\n }\n\n private async run(work: () => Promise<void>): Promise<void> {\n this.busy.set(true);\n this.error.set(null);\n try {\n await work();\n } catch (e) {\n const err = e as HttpErrorResponse;\n this.error.set(err?.status === 404\n ? this.lang.t('softDelete.unavailable')\n : (err?.error?.error || err?.message || this.lang.t('common.actionFailed')));\n } finally {\n this.busy.set(false);\n }\n }\n}\n","import { Provider } from '@angular/core';\nimport { provideSparkDetailActions, provideSparkQueryListActions } from '@mintplayer/ng-spark/panels';\nimport { SparkDeletedToggleComponent } from './spark-deleted-toggle.component';\nimport { SparkSoftDeleteActionsComponent } from './spark-soft-delete-actions.component';\n\n/**\n * Adds the SoftDelete UI (#460) to the pages `sparkRoutes()` routes to:\n *\n * - the query page gets a **Deleted** toggle (holders of `ViewDeleted/T`) that lists the recycle bin\n * (`?deleted=only`); its rows open with the same mode;\n * - the detail page of such a row gets **Restore** (`Restore/T`) and **Purge** (`Purge/T`).\n *\n * Needs the server package `MintPlayer.Spark.SoftDelete`. Add to the app's providers:\n * `providers: [provideSpark(...), provideSparkSoftDelete()]`.\n */\nexport function provideSparkSoftDelete(): Provider[] {\n return [\n ...provideSparkQueryListActions({ id: 'spark-soft-delete-toggle', component: SparkDeletedToggleComponent, priority: 55 }),\n ...provideSparkDetailActions({ id: 'spark-soft-delete-actions', component: SparkSoftDeleteActionsComponent, priority: 4 }),\n ];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mintplayer-ng-spark-soft-delete.mjs","names":[],"sources":["../../soft-delete/src/spark-soft-delete.service.ts","../../soft-delete/src/spark-deleted-toggle.component.ts","../../soft-delete/src/spark-soft-delete-actions.component.ts","../../soft-delete/src/provide-spark-soft-delete.ts"],"sourcesContent":["import { inject, Injectable } from '@angular/core';\nimport { PersistentObject } from '@mintplayer/ng-spark/models';\nimport { SparkService } from '@mintplayer/ng-spark/services';\n\n/**\n * Client for the SoftDelete package's endpoints (`MintPlayer.Spark.SoftDelete`, #460 T1). Ids travel\n * in the body — the route table is literal and Raven ids contain slashes.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkSoftDeleteService {\n private readonly spark = inject(SparkService);\n\n /**\n * `POST /spark/po/restore` → the restored (live) object. Requires `Restore/T` and a deleted row;\n * anything else is 404 (or 401 anonymous), a restore withheld by `OnDisableActionsAsync` is 403.\n */\n restore(type: string, id: string): Promise<PersistentObject> {\n return this.spark.postEnvelope<PersistentObject>('/po/restore', { objectTypeId: type, id });\n }\n\n /**\n * `POST /spark/po/purge` → 204. Requires `Purge/T` and a deleted row. Removes the document and\n * every revision of it; it cannot be undone.\n */\n async purge(type: string, id: string, etag: string): Promise<void> {\n await this.spark.postEnvelope<void>('/po/purge', { objectTypeId: type, id, etag });\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkQueryListContext } from '@mintplayer/ng-spark/panels';\n\n/**\n * The query page's \"Deleted\" toggle (#460): switches the list into the recycle bin\n * (`deleted: only`) and back. Rendered only for holders of `ViewDeleted/T` — for anyone else the\n * server would ignore the widening anyway, so offering it would show the same rows under a\n * misleading label.\n *\n * Registered by {@link provideSparkSoftDelete} as a `SPARK_QUERY_LIST_ACTIONS` entry; usable on its\n * own in a custom page with any {@link SparkQueryListContext}.\n */\n@Component({\n selector: 'spark-deleted-toggle',\n imports: [TranslateKeyPipe, SparkIconComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (visible()) {\n <button type=\"button\"\n class=\"btn spark-deleted-toggle\"\n [class.btn-secondary]=\"active()\"\n [class.btn-outline-secondary]=\"!active()\"\n [attr.aria-pressed]=\"active()\"\n (click)=\"toggle()\">\n <spark-icon name=\"trash\" /> {{ 'softDelete.showDeleted' | t }}\n </button>\n }\n `,\n})\nexport class SparkDeletedToggleComponent {\n context = input.required<SparkQueryListContext>();\n\n protected readonly visible = computed(() =>\n !!this.context().entityType && this.context().permissions?.canViewDeleted === true);\n\n protected readonly active = computed(() => this.context().deleted === 'only');\n\n toggle(): void {\n this.context().setDeleted(this.active() ? 'exclude' : 'only');\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SPARK_RETURN_URL_STATE_KEY, SparkLanguageService, SparkReturnNavigationService } from '@mintplayer/ng-spark/services';\nimport { SparkDetailContext } from '@mintplayer/ng-spark/panels';\nimport { SparkSoftDeleteService } from './spark-soft-delete.service';\n\n/**\n * Restore and Purge on the detail page of a row opened from the recycle bin (#460).\n *\n * Shown only when the page loaded the row with `?deleted=only` (every row in the recycle bin is\n * deleted) and the caller holds `Restore/T` / `Purge/T`. The server re-checks both — the row must be\n * deleted and the row gate must pass — so a refusal here is the 404/403 it answers, surfaced inline.\n *\n * Registered by {@link provideSparkSoftDelete} as a `SPARK_DETAIL_ACTIONS` entry.\n */\n@Component({\n selector: 'spark-soft-delete-actions',\n imports: [TranslateKeyPipe, SparkIconComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (deletedView()) {\n <span class=\"d-inline-flex gap-2 align-items-center\">\n @if (canRestore()) {\n <button type=\"button\" class=\"btn btn-success spark-restore\" [disabled]=\"busy()\" (click)=\"restore()\">\n {{ 'softDelete.restore' | t }}\n </button>\n }\n @if (canPurge()) {\n <button type=\"button\" class=\"btn btn-danger spark-purge\" [disabled]=\"busy()\" (click)=\"purge()\">\n <spark-icon name=\"trash\" /> {{ 'softDelete.purge' | t }}\n </button>\n }\n @if (error(); as message) {\n <span class=\"text-danger small spark-soft-delete-error\" role=\"alert\">{{ message }}</span>\n }\n </span>\n }\n `,\n})\nexport class SparkSoftDeleteActionsComponent {\n private readonly softDelete = inject(SparkSoftDeleteService);\n private readonly router = inject(Router);\n private readonly route = inject(ActivatedRoute);\n private readonly returnNavigation = inject(SparkReturnNavigationService);\n private readonly lang = inject(SparkLanguageService);\n\n context = input.required<SparkDetailContext>();\n\n protected readonly deletedView = computed(() => this.context().deleted === 'only');\n protected readonly canRestore = computed(() => this.context().permissions?.canRestore === true);\n protected readonly canPurge = computed(() => this.context().permissions?.canPurge === true);\n\n protected readonly busy = signal(false);\n protected readonly error = signal<string | null>(null);\n\n async restore(): Promise<void> {\n if (this.busy()) return;\n const ctx = this.context();\n await this.run(async () => {\n await this.softDelete.restore(ctx.type, ctx.id);\n // Same page, live mode: dropping `?deleted` makes the detail page re-read the row normally.\n // The list this row was opened from travels on: a later Delete still returns there.\n const returnUrl = this.returnNavigation.recordedReturnUrl();\n await this.router.navigate([], {\n relativeTo: this.route,\n queryParams: { deleted: null },\n queryParamsHandling: 'merge',\n replaceUrl: true,\n ...(returnUrl ? { state: { [SPARK_RETURN_URL_STATE_KEY]: returnUrl } } : {}),\n });\n });\n }\n\n async purge(): Promise<void> {\n if (this.busy()) return;\n if (!confirm(this.lang.t('softDelete.confirmPurge'))) return;\n const ctx = this.context();\n await this.run(async () => {\n await this.softDelete.purge(ctx.type, ctx.id, ctx.item.etag ?? '');\n // The row no longer exists: back to the recycle bin it was opened from — the recorded list, else\n // the type's list in `?deleted=only`. Never \"the previous page\", which may be anything.\n await this.returnNavigation.returnToList(ctx.entityType?.name, { deletedView: true });\n });\n }\n\n private async run(work: () => Promise<void>): Promise<void> {\n this.busy.set(true);\n this.error.set(null);\n try {\n await work();\n } catch (e) {\n const err = e as HttpErrorResponse;\n this.error.set(err?.status === 404\n ? this.lang.t('softDelete.unavailable')\n : (err?.error?.error || err?.message || this.lang.t('common.actionFailed')));\n } finally {\n this.busy.set(false);\n }\n }\n}\n","import { Provider } from '@angular/core';\nimport { provideSparkDetailActions, provideSparkQueryListActions } from '@mintplayer/ng-spark/panels';\nimport { SparkDeletedToggleComponent } from './spark-deleted-toggle.component';\nimport { SparkSoftDeleteActionsComponent } from './spark-soft-delete-actions.component';\n\n/**\n * Adds the SoftDelete UI (#460) to the pages `sparkRoutes()` routes to:\n *\n * - the query page gets a **Deleted** toggle (holders of `ViewDeleted/T`) that lists the recycle bin\n * (`?deleted=only`); its rows open with the same mode;\n * - the detail page of such a row gets **Restore** (`Restore/T`) and **Purge** (`Purge/T`).\n *\n * Needs the server package `MintPlayer.Spark.SoftDelete`. Add to the app's providers:\n * `providers: [provideSpark(...), provideSparkSoftDelete()]`.\n */\nexport function provideSparkSoftDelete(): Provider[] {\n return [\n ...provideSparkQueryListActions({ id: 'spark-soft-delete-toggle', component: SparkDeletedToggleComponent, priority: 55 }),\n ...provideSparkDetailActions({ id: 'spark-soft-delete-actions', component: SparkSoftDeleteActionsComponent, priority: 4 }),\n ];\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mintplayer/ng-spark",
3
3
  "private": false,
4
- "version": "22.26.0",
4
+ "version": "22.27.0",
5
5
  "description": "Angular component library for MintPlayer.Spark CRUD applications",
6
6
  "repository": {
7
7
  "type": "git",
@@ -14,8 +14,8 @@
14
14
  "@angular/common": "^22.0.0",
15
15
  "@angular/router": "^22.0.0",
16
16
  "@angular/forms": "^22.0.0",
17
- "@mintplayer/ng-bootstrap": "^22.20.0",
18
- "@mintplayer/web-components": "^2.17.0",
17
+ "@mintplayer/ng-bootstrap": "^22.21.0",
18
+ "@mintplayer/web-components": "^2.18.0",
19
19
  "@mintplayer/pagination": "*",
20
20
  "rxjs": "~7.8.0"
21
21
  },
@@ -200,21 +200,21 @@ export declare class SparkQueryActionsContext {
200
200
  query: SparkQuery | null;
201
201
  }
202
202
  /**
203
- * One button of a query's toolbar (#460, M15): the built-in New or Delete, or a custom action.
203
+ * One button of a query's toolbar (#460 M15, #467 R3): the built-in New, Edit or Delete, or a custom action.
204
204
  *
205
205
  * Built by `SparkQueryGridComponent.toolbarActions()` and rendered by every host of the grid — the
206
206
  * query card's header and the query-list page's action bar — so both surfaces offer the same
207
207
  * actions, enabled by the same rules.
208
208
  */
209
209
  export interface SparkQueryToolbarAction {
210
- kind: 'new' | 'delete' | 'custom' | 'addon';
210
+ kind: 'new' | 'edit' | 'delete' | 'custom' | 'addon';
211
211
  /** For `addon` (row menu only): the add-on's entry (`SPARK_QUERY_ROW_ACTIONS`). */
212
212
  addon?: SparkQueryRowAction;
213
213
  /** The action's name, unique within the toolbar. */
214
214
  name: string;
215
215
  /** Its catalogue entry: label, icon, `selectionRule`, `variant`, confirmation. */
216
216
  definition: CustomActionDefinition;
217
- /** `*bsPriorityNavItem` priority: New first, then Delete, then custom actions by offset. */
217
+ /** `*bsPriorityNavItem` priority: New first, then Edit, then Delete, then custom actions by offset. */
218
218
  priority: number;
219
219
  }
220
220
  /**
@@ -357,14 +357,13 @@ export declare class SparkQueryGridComponent {
357
357
  }>;
358
358
  /**
359
359
  * A click anywhere on a row opens it, like its first-column link — the link used to be the only
360
- * target, so a row whose first column was empty ("—") could not be opened at all. While rows are
361
- * selectable a click selects instead, and a double-click opens. Clicks on the link itself or on an
362
- * interactive element inside the row (a vote button, a checkbox) are theirs, not the row's; a
363
- * modified click (Ctrl, ⌘, Shift, middle) is left to the browser.
360
+ * target, so a row whose first column was empty ("—") could not be opened at all. This holds on
361
+ * every list, selectable or not (#467, D9): only the checkbox cell selects, which the datatable's
362
+ * `'checkbox'` mode guarantees. Clicks on the link itself or on an interactive element inside the
363
+ * row (a vote button, a checkbox) are theirs, not the row's; a modified click (Ctrl, ⌘, Shift,
364
+ * middle) is left to the browser.
364
365
  */
365
366
  protected onRowClick(event: BsDatatableRowEvent<unknown>): void;
366
- protected onRowDblClick(event: BsDatatableRowEvent<unknown>): void;
367
- private openRow;
368
367
  /**
369
368
  * Paging and sorting. Two-way, so a host driving `data` can sort those rows by whatever the
370
369
  * user actually clicked — the datatable writes the new sort back through here.
@@ -449,45 +448,60 @@ export declare class SparkQueryGridComponent {
449
448
  */
450
449
  errorMessage: import("@angular/core").WritableSignal<string | null>;
451
450
  /**
452
- * The rendered selection mode (#460, D17): the host's override (a sub-query entry's
453
- * `selectionMode`), else the query's own, else `'auto'` — derived from the CUSTOM actions offered,
454
- * exactly as before the setting existed, so no existing grid changes behaviour. The default Delete
455
- * does not widen `'auto'`: selection is opt-in, and without it Delete stays reachable per row
456
- * through the row menu.
451
+ * The built-in actions this result offers (#467, R1(1–3)), each with its toolbar kind: listed for
452
+ * the caller (the right), on the query side (`defaultQueryActions` already filtered `showedOn`),
453
+ * not withheld by this result's `disabledActions` — Edit also not when `Save` is — and nothing in
454
+ * the recycle bin. New additionally needs the type's create right ({@link offersCreate}).
455
+ */
456
+ readonly offeredDefaultActions: import("@angular/core").Signal<SparkQueryToolbarAction[]>;
457
+ /**
458
+ * The rendered selection mode (#467, R1): the host's override (a sub-query entry's
459
+ * `selectionMode`), else the query's own, else `'auto'` — derived from the EFFECTIVE action list
460
+ * (built-ins and custom actions alike), so a list is selectable exactly when the caller can use an
461
+ * action on ticked rows here. Recomputed whenever `disabledActions` or the deleted mode changes;
462
+ * the selection is cleared when the mode becomes `'none'`.
457
463
  */
458
464
  selectionMode: import("@angular/core").Signal<import("@mintplayer/ng-spark/models").SparkSelectionMode>;
459
465
  /**
460
- * The actions a host renders in its toolbar, in one list (#460, M15): New, Delete and the custom
461
- * actions, each with the rule that enables it. The query card and the query-list page render this
462
- * same list, so the two surfaces cannot drift.
466
+ * The actions a host renders in its toolbar, in one list (#460 M15, #467 R3): New, Edit, Delete and
467
+ * the custom actions, each with the rule that enables it. The query card and the query-list page
468
+ * render this same list, so the two surfaces cannot drift. The same effective list decides
469
+ * {@link selectionMode}.
463
470
  *
464
- * - New: the default entry's `showedOn` includes the query, the caller holds `New/T`, and the
465
- * result does not withhold `New`/`Save`.
466
- * - Delete: likewise with `Delete/T` and `Delete`, and only while rows can be selected (otherwise
467
- * it could never be enabled — the row menu offers it per row instead).
471
+ * - New, Edit, Delete: {@link offeredDefaultActions}. Edit and Delete act on ticked rows, so a list
472
+ * declared `'none'` leaves them to the row menu, where they would otherwise never be enabled.
468
473
  * - Custom: `visibleCustomActions()`.
469
- * - In the recycle bin ({@link recycleBin}) none of the three: the list is empty.
474
+ * - In the recycle bin ({@link recycleBin}) none: the list is empty.
470
475
  */
471
476
  toolbarActions: import("@angular/core").Signal<SparkQueryToolbarAction[]>;
472
477
  /**
473
- * The per-row `⋮` menu: every offered action whose rule accepts exactly one row — Delete included,
474
- * whatever the selection mode. An action without a rule acts on the query, not on a row, and is
475
- * left to the toolbar.
478
+ * The per-row `⋮` menu: every offered action whose rule accepts exactly one row — Edit and Delete
479
+ * included, whatever the selection mode. An action without a rule acts on the query, not on a row,
480
+ * and is left to the toolbar.
476
481
  */
477
482
  rowActions: import("@angular/core").Signal<SparkQueryToolbarAction[]>;
478
483
  /** What an add-on row action is asked whether it is offered; null until the type resolved. */
479
484
  private readonly rowActionScope;
480
- /** The default Delete: not withheld by the result, and never in the recycle bin. */
481
- private offersDelete;
482
485
  /** Whether a toolbar action can run with the current selection. The server checks again. */
483
486
  isToolbarActionEnabled(action: SparkQueryToolbarAction): boolean;
484
487
  /** Runs a toolbar action on the current selection. */
485
488
  runToolbarAction(action: SparkQueryToolbarAction): Promise<void>;
486
489
  /**
487
- * The chip's ⊗, offered only for single selection: there the datatable renders no checkbox
488
- * column, so it has no header deselect-all, and a row click never deselects.
490
+ * The built-in Edit (#467, R2): it only navigates, to the row's edit page with this list as the
491
+ * return state. Get and Update enforce everything (row gate, `can`, etag, write shield); the `=1`
492
+ * rule is client-side only, since there is no bulk edit.
489
493
  */
494
+ editRow(row: QueryResultItem | undefined): void;
495
+ /** The chip's ⊗: drops the whole selection, ticked rows on other pages included. */
490
496
  clearSelection(): void;
497
+ /** The ids of the rows on the page currently shown; the chip counts the ticked rows outside it. */
498
+ private readonly loadedIds;
499
+ /**
500
+ * Ticked rows that are not on the page shown (#467, D19): the selection survives paging, so the
501
+ * chip says how many of the rows an action will act on cannot be seen ("7 selected, 4 on other
502
+ * pages"). Actions always receive the whole `selection()`, which is what the chip counts.
503
+ */
504
+ readonly offPageCount: import("@angular/core").Signal<number>;
491
505
  /** The id of the row whose `⋮` menu is open; at most one is. */
492
506
  readonly openRowMenu: import("@angular/core").WritableSignal<string | null>;
493
507
  /** Below the toggle, right-aligned; above it when there is no room below. */
@@ -520,10 +534,11 @@ export declare class SparkQueryGridComponent {
520
534
  */
521
535
  startNew(): void;
522
536
  /**
523
- * The default Delete on `ids`: one request, all rows or none (#460, D18). Asks first — with the
524
- * entry's `confirmationMessageKey` — then clears the selection and refreshes.
537
+ * The default Delete on `rows`: one request, all rows or none (#460, D18). Asks first — with the
538
+ * entry's `confirmation` (`{count}` substituted) — then clears the selection and refreshes. Each row
539
+ * goes with the `etag` it was listed with (#467, D14), so a row changed since is refused, not lost.
525
540
  */
526
- deleteRows(ids: string[], definition?: CustomActionDefinition): Promise<void>;
541
+ deleteRows(rows: QueryResultItem[], definition?: CustomActionDefinition): Promise<void>;
527
542
  isVirtualScrolling: import("@angular/core").Signal<boolean>;
528
543
  /**
529
544
  * Columns as sent with the most recent fetched page.
@@ -128,51 +128,71 @@ export interface PersistentObjectAttribute {
128
128
  */
129
129
  asDetailType?: string;
130
130
  }
131
+ /**
132
+ * One action of the composed catalogue (#467, D7), as `/spark/actions/list` reports it for a type:
133
+ * a built-in (New, Edit, Delete) or a custom action, already narrowed to what the caller may run.
134
+ * Text arrives resolved by the server (D26).
135
+ */
131
136
  export interface CustomActionDefinition {
132
137
  name: string;
133
- displayName: TranslatedString;
138
+ /** `actions.{name}.label`, or the humanized name when untranslated. */
139
+ label: TranslatedString;
134
140
  icon?: string;
135
- description?: string;
141
+ /** `actions.{name}.description`; absent when untranslated. */
142
+ description?: TranslatedString;
136
143
  showedOn: string;
137
144
  selectionRule?: string;
138
145
  refreshOnCompleted: boolean;
139
- confirmationMessageKey?: string;
146
+ /**
147
+ * The confirmation to ask before running, `actions.{name}.confirmation` or the explicit key the
148
+ * catalogue names. May contain `{count}`, the number of rows about to be acted on. Absent: no prompt.
149
+ */
150
+ confirmation?: TranslatedString;
140
151
  /**
141
152
  * How prominently, and how warily, to present the action: 'primary', 'secondary', 'danger',
142
153
  * 'warning'. Undefined renders the neutral default.
143
154
  *
144
155
  * Presentation only — the server decides who may see and run an action. A 'danger' variant just
145
- * asks the button to look like what it does; pair it with `confirmationMessageKey` for anything
156
+ * asks the button to look like what it does; pair it with a `confirmation` for anything
146
157
  * irreversible, because the colour warns and the prompt is what actually prevents the accident.
147
158
  */
148
159
  variant?: string;
149
160
  offset: number;
150
161
  /**
151
- * True for the framework's built-in `New` and `Delete` (#460, D18): catalogue entries with a
152
- * `showedOn` and a `selectionRule` like any custom action (defaults: New has no rule, Delete is
153
- * `>0`; an app overrides them in `customActions.json`), but run through `/po/new` and
154
- * `/po/delete-many`, never `/actions/execute`. Absent for a custom action.
162
+ * True for the framework's built-in `New`, `Edit` and `Delete` (#460 D18, #467 D7): catalogue
163
+ * entries with a `showedOn` and a `selectionRule` like any custom action (the core's
164
+ * `actions.json` ships them; an app overrides or removes them in its own), but run through
165
+ * `/po/new`, the edit page and `/po/delete-many`, never `/actions/execute`. Absent for a custom action.
155
166
  */
156
167
  isDefault?: boolean;
157
168
  }
158
- /** How many rows a grid lets the user select — what `<bs-datatable>` renders. */
159
- export type SparkSelectionMode = 'none' | 'single' | 'multiple';
160
169
  /**
161
- * What a query definition (or a sub-query entry) declares (#460, D17): a fixed mode, or `'auto'` —
170
+ * Whether a grid lets the user tick rows — what `<bs-datatable>` renders. There is no single
171
+ * selection (#467, D10): a selectable list is a checkbox list, and actions whose rule does not
172
+ * match the count are disabled rather than the selection trimmed.
173
+ */
174
+ export type SparkSelectionMode = 'none' | 'multiple';
175
+ /**
176
+ * What a query definition (or a sub-query entry) declares (#460 D17): a fixed mode, or `'auto'` —
162
177
  * the default, and what an absent field means — to derive it from the actions offered.
163
178
  */
164
179
  export type SparkSelectionModeSetting = 'auto' | SparkSelectionMode;
180
+ /** The largest selection the server accepts in one request (`SparkDefaultActions.MaxSelectedItems`). */
181
+ export declare const SPARK_MAX_SELECTED_ITEMS = 200;
165
182
  /**
166
- * The selection mode a grid renders.
167
- *
168
- * A declared `'none'`, `'single'` or `'multiple'` wins outright. `'auto'` (or nothing) derives it,
169
- * so a grid gains a checkbox column exactly when an action needs one and is otherwise
170
- * pixel-identical to a grid with no selection at all — as Vidyano's query grid does. The offered
171
- * actions include the default Delete (`>0`) when the caller may delete, so such a grid is
172
- * multi-select.
183
+ * Whether an action needs ticked rows (#467, R1(4)): it has a rule, and the rule accepts at least
184
+ * one row. `=0` accepts none, so an action that runs only without a selection never makes a list
185
+ * selectable; an action without a rule acts on the query, not on rows.
186
+ */
187
+ export declare function needsSelection(action: CustomActionDefinition): boolean;
188
+ /**
189
+ * The selection mode a grid renders (#467, R1).
173
190
  *
174
- * Derived: `'single'` when every gated action is satisfied by one row and refused by two; anything
175
- * else that cares about the count gets `'multiple'`.
191
+ * A declared `'none'` or `'multiple'` wins outright. `'auto'` (or nothing) derives it from
192
+ * `actions`, which must already be the **effective** list for this result — listed for the caller
193
+ * (the right), shown on queries, not withheld by `disabledActions` and not hidden by the recycle
194
+ * bin. Built-in Edit and Delete count like any other action. `'multiple'` when at least one of them
195
+ * {@link needsSelection}, else `'none'`, so a read-only user sees a list with no checkbox column.
176
196
  */
177
197
  export declare function selectionModeFor(actions: CustomActionDefinition[], declared?: SparkSelectionModeSetting | null): SparkSelectionMode;
178
198
  /**
@@ -293,7 +313,8 @@ export interface AttributeGroup {
293
313
  export interface EntityType {
294
314
  id: string;
295
315
  name: string;
296
- description?: TranslatedString;
316
+ /** The type's display name, resolved by the server (#467, D6; formerly `description`). */
317
+ label?: TranslatedString;
297
318
  /**
298
319
  * The backing CLR type, or absent for a JSON-only virtual type (#325).
299
320
  *
@@ -369,7 +390,8 @@ export interface SparkQuerySortColumn {
369
390
  export interface SparkQuery {
370
391
  id: string;
371
392
  name: string;
372
- description?: TranslatedString;
393
+ /** The query's display name, resolved by the server (#467; formerly `description`). */
394
+ label?: TranslatedString;
373
395
  source: string;
374
396
  alias?: string;
375
397
  sortColumns: SparkQuerySortColumn[];
@@ -479,14 +501,20 @@ export interface QueryColumn extends SparkCellColumn {
479
501
  /**
480
502
  * One row: an id, a display string, and a value per column.
481
503
  *
482
- * Deliberately too weak to act on — no attribute metadata, no `can` block, no etag, because none
483
- * of those can be trusted from a projection.
504
+ * Deliberately too weak to act on — no attribute metadata, no `can` block, because neither can be
505
+ * trusted from a projection. It does carry its document's `etag` (#467, D14), a version rather than
506
+ * a judgement, so a delete from the list says which version it removes.
484
507
  */
485
508
  export interface QueryResultItem {
486
509
  /** Never null and unique within a result; the server refuses anything else. */
487
510
  id: string;
488
511
  /** What to show when the row is named rather than tabulated (a reference picker's value). */
489
512
  breadcrumb?: string;
513
+ /**
514
+ * The document's change vector when the list loaded (#467, D14). A delete sends it back; a row
515
+ * changed since is a 409. Absent for a row with no document behind it.
516
+ */
517
+ etag?: string;
490
518
  values: QueryResultItemValue[];
491
519
  /** Presentation hints for the whole row. Keys arrive lower-cased. */
492
520
  typeHints?: Record<string, string>;
@@ -926,6 +954,13 @@ export declare function comparableValue(attribute: PersistentObjectAttribute | u
926
954
  export declare function comparableFormValue(value: unknown): string;
927
955
  /** Whether two form-dict values hold the same content (see {@link comparableFormValue}). */
928
956
  export declare function formValuesEqual(a: unknown, b: unknown): boolean;
957
+ /**
958
+ * The prompt to show before running `action` on `count` rows, with `{count}` substituted, or null
959
+ * when the action asks for no confirmation (#467, D7).
960
+ */
961
+ export declare function confirmationText(action: CustomActionDefinition | undefined | null, count: number, lang?: string): string | null;
962
+ /** The built-in action named `name` (`New`, `Edit`, `Delete`) in `actions`, when the server listed it. */
963
+ export declare function builtInAction(actions: CustomActionDefinition[], name: 'New' | 'Edit' | 'Delete'): CustomActionDefinition | undefined;
929
964
  /** Whether `showedOn` offers the action on a query: `"query"` or `"both"`, in any casing. */
930
965
  export declare function isShowedOnQuery(showedOn: string | undefined | null): boolean;
931
966
  /** Whether `showedOn` offers the action on a detail page: `"detail"` or `"both"`, in any casing. */
@@ -937,7 +972,7 @@ export declare function isShowedOnDetail(showedOn: string | undefined | null): b
937
972
  * and `"both"` — as the server model and the custom-actions guide have always
938
973
  * documented — compared case-insensitively (#460 M15: `"Both"` used to render nowhere).
939
974
  *
940
- * The built-in New and Delete (`isDefault`, #460 D18) are left out: they are not executed as
975
+ * The built-in New, Edit and Delete (`isDefault`, #460 D18, #467 D7) are left out: they are not executed as
941
976
  * custom actions, and the grid offers them itself (see {@link defaultQueryActions}).
942
977
  *
943
978
  * ⚠️ This narrows what is DISPLAYED. It is NOT an authorization boundary: the grant
@@ -948,8 +983,8 @@ export declare function filterQueryActions(actions: CustomActionDefinition[]): C
948
983
  /** The custom actions a detail page should offer — the detail-side counterpart of {@link filterQueryActions}. */
949
984
  export declare function filterDetailActions(actions: CustomActionDefinition[]): CustomActionDefinition[];
950
985
  /**
951
- * The built-in New and Delete the server listed for this caller (#460, D18), when their `showedOn`
952
- * includes the query side. Present only when the caller holds `New/T` or `Delete/T`.
986
+ * The built-in New, Edit and Delete the server listed for this caller (#460 D18, #467 D7), when their `showedOn`
987
+ * includes the query side. Present only when the caller holds `New/T`, `Edit/T` or `Delete/T`.
953
988
  */
954
989
  export declare function defaultQueryActions(actions: CustomActionDefinition[]): CustomActionDefinition[];
955
990
  /**
@@ -60,8 +60,12 @@ export declare class SparkPoDetailComponent {
60
60
  asDetailReferenceOptions: import("@angular/core").WritableSignal<Record<string, Record<string, QueryResultItem[]>>>;
61
61
  type: string;
62
62
  id: string;
63
- canEdit: import("@angular/core").WritableSignal<boolean>;
64
- canDelete: import("@angular/core").WritableSignal<boolean>;
63
+ /** The catalogue's Edit (#467, D8) when this page offers it: listed for the caller, on the detail side, and allowed on this row. */
64
+ editAction: import("@angular/core").WritableSignal<CustomActionDefinition | null>;
65
+ /** The catalogue's Delete, on the same terms as {@link editAction}. */
66
+ deleteAction: import("@angular/core").WritableSignal<CustomActionDefinition | null>;
67
+ canEdit: import("@angular/core").Signal<boolean>;
68
+ canDelete: import("@angular/core").Signal<boolean>;
65
69
  customActions: import("@angular/core").WritableSignal<CustomActionDefinition[]>;
66
70
  /** Type-level rights, kept for the add-on context; null until loaded. */
67
71
  permissions: import("@angular/core").WritableSignal<EntityPermissions | null>;
@@ -125,6 +129,8 @@ export declare class SparkPoDetailComponent {
125
129
  * a louder style. Restricted to a known set rather than interpolated, because the value arrives
126
130
  * from a JSON file and must not be able to put arbitrary classes on the button.
127
131
  */
132
+ /** Edit's and Delete's button classes: the catalogue's variant, else the look they always had. */
133
+ protected builtInActionClass(action: CustomActionDefinition, fallback: 'primary' | 'danger'): string;
128
134
  protected customActionClass(action: CustomActionDefinition): string;
129
135
  /**
130
136
  * The action currently running, or null. Drives the disabled state on every custom-action
@@ -3,7 +3,7 @@ import { TemplateRef } from "@angular/core";
3
3
  import { Color } from "@mintplayer/ng-bootstrap";
4
4
  import { SparkLanguageService } from "@mintplayer/ng-spark/services";
5
5
  import { SparkQueryToolbarAction } from "@mintplayer/ng-spark/grid";
6
- import { CustomActionDefinition, QueryColumn, QueryColumnFilter, QueryResultItem, SparkDeletedFilter } from "@mintplayer/ng-spark/models";
6
+ import { CustomActionDefinition, QueryColumn, QueryColumnFilter, QueryResultItem, SparkDeletedFilter, SparkSelectionModeSetting } from "@mintplayer/ng-spark/models";
7
7
  import { SparkQueryListContext } from "@mintplayer/ng-spark/panels";
8
8
  /**
9
9
  * The routed query page: chrome around one {@link SparkQueryGridComponent}.
@@ -31,6 +31,11 @@ export declare class SparkQueryListComponent {
31
31
  protected readonly lang: SparkLanguageService;
32
32
  extraActionsTemplate: import("@angular/core").InputSignal<TemplateRef<void> | null>;
33
33
  showCustomActions: import("@angular/core").InputSignal<boolean>;
34
+ /**
35
+ * Overrides the query's own `selectionMode` (#467, R3), as the sub-query card's input does: for a
36
+ * host that embeds this page and wants, say, `'none'` on a list the actions would make selectable.
37
+ */
38
+ selectionMode: import("@angular/core").InputSignal<SparkSelectionModeSetting | null>;
34
39
  /**
35
40
  * Forwarded to the grid, so a query PAGE can replace its row links — the same reason the card
36
41
  * forwards it. Without this the escape hatch existed only for a directly-embedded grid.
@@ -147,6 +152,11 @@ export declare class SparkQueryListComponent {
147
152
  "required": false;
148
153
  "isSignal": true;
149
154
  };
155
+ "selectionMode": {
156
+ "alias": "selectionMode";
157
+ "required": false;
158
+ "isSignal": true;
159
+ };
150
160
  "rowRoute": {
151
161
  "alias": "rowRoute";
152
162
  "required": false;
@@ -62,11 +62,22 @@ export interface NewObjectOptions {
62
62
  }
63
63
  /** Where a bulk delete was started from; see {@link SparkService.deleteMany}. */
64
64
  export interface DeleteManyOptions {
65
- /** The query the rows were selected in, for the server's `OnDisableActionsAsync`. */
66
- queryId?: string;
65
+ /**
66
+ * The query the rows were selected in. Required (#467, D12): the server fetches the rows through it
67
+ * and applies its `OnDisableActionsAsync` decision; without it the call is a 400.
68
+ */
69
+ queryId: string;
67
70
  /** The sub-query's container, when deleting from a sub-query. */
68
71
  parentId?: string;
69
72
  parentType?: string;
73
+ /** One reason for the whole batch, recorded on every soft-deleted row (#467, D20). */
74
+ reason?: string;
75
+ }
76
+ /** A row and the version of it the caller saw (#467, D14): what a bulk delete names. */
77
+ export interface SparkRowVersion {
78
+ id: string;
79
+ /** The row's `etag` from the query result. */
80
+ etag: string;
70
81
  }
71
82
  /** Context for {@link SparkService.deleteRow}. Every field is required — see `DeleteRow.cs`. */
72
83
  export interface DeleteRowOptions {
@@ -155,12 +166,20 @@ export declare class SparkService {
155
166
  * `parent` is the sub-query the New was started from — the same `parentId`/`parentType`/`queryId`
156
167
  * `newObject` was given. The server resolves and authorizes it again and hands it to the save hooks
157
168
  * as `PersistentObject.Parent`.
169
+ *
170
+ * Resolves to `null` when a server interceptor cancelled the create (#482: the user answered a prompt with
171
+ * Cancel) — the server answers 204 and nothing was created.
158
172
  */
159
173
  create(type: string, data: Partial<PersistentObject>, parent?: {
160
174
  parentId: string;
161
175
  parentType: string;
162
176
  queryId: string;
163
- }): Promise<PersistentObject>;
177
+ }): Promise<PersistentObject | null>;
178
+ /**
179
+ * Saves an edit. `data.etag` — the etag the object was loaded with — is required (#467, D16): it is
180
+ * the version being edited. A 409 means the object changed since (`reason: 'changed'`) or was
181
+ * deleted since (`reason: 'deleted'`).
182
+ */
164
183
  update(type: string, id: string, data: Partial<PersistentObject>): Promise<PersistentObject>;
165
184
  /**
166
185
  * Asks the server to reshape an in-progress object after `triggeredBy`'s value changed.
@@ -199,14 +218,20 @@ export declare class SparkService {
199
218
  * of the route variables.
200
219
  */
201
220
  deleteRow(type: string, options: DeleteRowOptions): Promise<void>;
202
- delete(type: string, id: string): Promise<void>;
221
+ /**
222
+ * Deletes one object. `etag` is the version being deleted — the loaded object's `etag` — and is
223
+ * required (#467, D14): an object changed since is a 409.
224
+ */
225
+ delete(type: string, id: string, etag: string): Promise<void>;
203
226
  /**
204
227
  * Deletes several rows of one type in one request — the default Delete action on a selection
205
- * (`POST /spark/po/delete-many`, #460 D18). All or nothing: a 404 means some row is missing or not
206
- * yours to delete, a 403 that the server's `OnDisableActionsAsync` withholds Delete on the query or
207
- * on one of the rows, a 400 that the selection breaks the rule or the 200-row cap.
228
+ * (`POST /spark/po/delete-many`, #460 D18). Each row carries the `etag` its query result row had
229
+ * (#467, D14). All or nothing: a 404 means some row is missing or not yours to delete, a 403 that
230
+ * the server's `OnDisableActionsAsync` withholds Delete on the query or on one of the rows, a 409
231
+ * that rows changed since the list loaded (the body's `message` names them), a 400 that the
232
+ * selection breaks the rule or the 200-row cap.
208
233
  */
209
- deleteMany(type: string, ids: string[], options?: DeleteManyOptions): Promise<void>;
234
+ deleteMany(type: string, items: SparkRowVersion[], options: DeleteManyOptions): Promise<void>;
210
235
  getCustomActions(objectTypeId: string): Promise<CustomActionDefinition[]>;
211
236
  /**
212
237
  * @param parent The object of THIS type the action is operating on — the detail-page invocation.
@@ -17,7 +17,7 @@ export declare class SparkSoftDeleteService {
17
17
  * `POST /spark/po/purge` → 204. Requires `Purge/T` and a deleted row. Removes the document and
18
18
  * every revision of it; it cannot be undone.
19
19
  */
20
- purge(type: string, id: string): Promise<void>;
20
+ purge(type: string, id: string, etag: string): Promise<void>;
21
21
  static ɵfac: i0.ɵɵFactoryDeclaration<SparkSoftDeleteService, never>;
22
22
  static ɵprov: i0.ɵɵInjectableDeclaration<any>;
23
23
  }