@blueprint-ts/core 4.0.0-beta.1 → 4.0.0-beta.10
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/CHANGELOG.md +74 -0
- package/docs/.vitepress/config.ts +1 -0
- package/docs/services/pagination/index.md +1 -1
- package/docs/services/pagination/infinite-scroller.md +4 -3
- package/docs/services/pagination/page-aware.md +5 -0
- package/docs/services/pagination/updating-rows.md +16 -0
- package/docs/services/requests/abort-requests.md +4 -0
- package/docs/services/requests/concurrency.md +58 -0
- package/docs/services/requests/error-handling.md +16 -0
- package/docs/upgrading/v3-to-v4.md +43 -0
- package/docs/vue/forms/arrays.md +57 -0
- package/package.json +4 -4
- package/src/pagination/BasePaginator.ts +24 -1
- package/src/pagination/PageAwarePaginator.ts +15 -5
- package/src/pagination/index.ts +0 -2
- package/src/requests/BaseRequest.ts +87 -2
- package/src/requests/RequestConcurrencyMode.enum.ts +6 -0
- package/src/requests/contracts/BaseRequestContract.ts +3 -0
- package/src/requests/exceptions/InvalidJsonException.ts +1 -1
- package/src/requests/exceptions/StaleResponseException.ts +13 -0
- package/src/requests/factories/FormDataFactory.ts +5 -2
- package/src/requests/index.ts +7 -1
- package/src/requests/types/RequestConcurrencyOptions.ts +6 -0
- package/src/vue/forms/BaseForm.ts +26 -11
- package/src/vue/forms/PropertyAwareArray.ts +4 -0
- package/src/vue/requests/factories/VueRequestLoaderFactory.ts +1 -1
- package/src/pagination/Paginator.ts +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
+
## v4.0.0-beta.10 - 2026-02-28 (beta)
|
|
2
|
+
|
|
3
|
+
# [4.0.0-beta.10](/compare/v4.0.0-beta.9...v4.0.0-beta.10) (2026-02-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **pagination:** support local row removal without reload 3a66689
|
|
9
|
+
## v4.0.0-beta.9 - 2026-02-28 (beta)
|
|
10
|
+
|
|
11
|
+
# [4.0.0-beta.9](/compare/v4.0.0-beta.8...v4.0.0-beta.9) (2026-02-28)
|
|
12
|
+
## v4.0.0-beta.8 - 2026-02-28 (beta)
|
|
13
|
+
|
|
14
|
+
# [4.0.0-beta.8](/compare/v4.0.0-beta.7...v4.0.0-beta.8) (2026-02-28)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **requests:** add concurrency policy, stale-response handling, and docs 5feb49d
|
|
20
|
+
## v4.0.0-beta.7 - 2026-02-27 (beta)
|
|
21
|
+
|
|
22
|
+
# [4.0.0-beta.7](/compare/v4.0.0-beta.6...v4.0.0-beta.7) (2026-02-27)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* Make cause public in InvalidJsonException 3aab192
|
|
28
|
+
## v4.0.0-beta.6 - 2026-02-27 (beta)
|
|
29
|
+
|
|
30
|
+
# [4.0.0-beta.6](/compare/v4.0.0-beta.5...v4.0.0-beta.6) (2026-02-27)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Bug Fixes
|
|
34
|
+
|
|
35
|
+
* bind BaseForm getters to preserve this 5e44296
|
|
36
|
+
* Type propertyAwareToRaw properly 1fe2f28
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Features
|
|
40
|
+
|
|
41
|
+
* Make state protected readonly in BaseForm e6e2f81
|
|
42
|
+
* Removed deprecated Paginator alias 7f07617
|
|
43
|
+
## v4.0.0-beta.5 - 2026-02-27 (beta)
|
|
44
|
+
|
|
45
|
+
# [4.0.0-beta.5](/compare/v4.0.0-beta.4...v4.0.0-beta.5) (2026-02-27)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Bug Fixes
|
|
49
|
+
|
|
50
|
+
* satisfy paginator updater typing and type-only import in VueRequestLoaderFactory 6468368
|
|
51
|
+
## v4.0.0-beta.4 - 2026-02-26 (beta)
|
|
52
|
+
|
|
53
|
+
# [4.0.0-beta.4](/compare/v4.0.0-beta.3...v4.0.0-beta.4) (2026-02-26)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
### Bug Fixes
|
|
57
|
+
|
|
58
|
+
* Use WritableComputedRef for field models 3cdb7dc
|
|
59
|
+
## v4.0.0-beta.3 - 2026-02-26 (beta)
|
|
60
|
+
|
|
61
|
+
# [4.0.0-beta.3](/compare/v4.0.0-beta.2...v4.0.0-beta.3) (2026-02-26)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
### Bug Fixes
|
|
65
|
+
|
|
66
|
+
* Make PropertyAwareArray nominal to avoid array type confusion b68b6fe
|
|
67
|
+
## v4.0.0-beta.2 - 2026-02-26 (beta)
|
|
68
|
+
|
|
69
|
+
# [4.0.0-beta.2](/compare/v4.0.0-beta.1...v4.0.0-beta.2) (2026-02-26)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
### Bug Fixes
|
|
73
|
+
|
|
74
|
+
* Loosen typeing in FormDataFactory.ts fed4475
|
|
1
75
|
## v4.0.0-beta.1 - 2026-02-26 (beta)
|
|
2
76
|
|
|
3
77
|
# [4.0.0-beta.1](/compare/v3.0.1...v4.0.0-beta.1) (2026-02-26)
|
|
@@ -25,6 +25,7 @@ export default defineConfig({
|
|
|
25
25
|
{ text: 'Responses', link: '/services/requests/responses' },
|
|
26
26
|
{ text: 'Request Bodies', link: '/services/requests/request-bodies' },
|
|
27
27
|
{ text: 'Headers', link: '/services/requests/headers' },
|
|
28
|
+
{ text: 'Concurrency', link: '/services/requests/concurrency' },
|
|
28
29
|
{ text: 'Aborting Requests', link: '/services/requests/abort-requests' },
|
|
29
30
|
{ text: 'Events', link: '/services/requests/events' },
|
|
30
31
|
{ text: 'Bulk Requests', link: '/services/requests/bulk-requests' },
|
|
@@ -38,7 +38,7 @@ If you are using Vue, the library provides `VuePaginationDriverFactory` and `Vue
|
|
|
38
38
|
- `flush`: clears existing data before applying the next page
|
|
39
39
|
- `replace`: replaces existing data instead of appending (useful for infinite scroll)
|
|
40
40
|
|
|
41
|
-
`updateRows`
|
|
41
|
+
`updateRows` and `removeRows` are available on all paginators. See [Updating Rows](./updating-rows).
|
|
42
42
|
|
|
43
43
|
## Using Laravel Pagination
|
|
44
44
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Infinite Scroll
|
|
2
2
|
|
|
3
|
-
Use `InfiniteScroller` when you want to append pages to the existing list
|
|
3
|
+
Use `InfiniteScroller` when you want to append pages to the existing list. It extends `PageAwarePaginator`, so all
|
|
4
|
+
page-aware features and helpers are available.
|
|
4
5
|
|
|
5
6
|
```typescript
|
|
6
7
|
import { InfiniteScroller } from '@blueprint-ts/core/pagination'
|
|
@@ -11,8 +12,8 @@ await scroller.load()
|
|
|
11
12
|
await scroller.toNextPage()
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
`{ replace: true }` to
|
|
15
|
+
You can pass `{ flush: true }` to clear existing data before loading a page. In addition, `InfiniteScroller` supports
|
|
16
|
+
`{ replace: true }` to replace the current list instead of appending.
|
|
16
17
|
|
|
17
18
|
## Scroll Detection Helper
|
|
18
19
|
|
|
@@ -40,6 +40,11 @@ await paginator.setPageSize(25).load()
|
|
|
40
40
|
Page navigation helpers (`toNextPage`, `toPreviousPage`, `toFirstPage`, `toLastPage`) update the page number and load
|
|
41
41
|
the new page in one call.
|
|
42
42
|
|
|
43
|
+
## Concurrency
|
|
44
|
+
|
|
45
|
+
If the underlying request uses concurrency mode `LATEST` or `REPLACE_LATEST`, stale responses are ignored. In that case,
|
|
46
|
+
`load()` resolves with the current page data without updating the view, so older responses cannot overwrite newer ones.
|
|
47
|
+
|
|
43
48
|
|
|
44
49
|
## Updating Rows
|
|
45
50
|
|
|
@@ -34,3 +34,19 @@ const updated = paginator.updateRows(
|
|
|
34
34
|
(row) => ({ ...row, updatedAt: new Date().toISOString() })
|
|
35
35
|
)
|
|
36
36
|
```
|
|
37
|
+
|
|
38
|
+
## Removing Rows
|
|
39
|
+
|
|
40
|
+
Use `removeRows` to delete items from the current page data without reloading. By default it also decrements `total`
|
|
41
|
+
by the number of removed rows. Set `adjustTotal: false` to skip that behavior.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Remove a single item
|
|
45
|
+
const removed = paginator.removeRows((row) => row.id === targetId)
|
|
46
|
+
|
|
47
|
+
// Remove all drafts without adjusting total
|
|
48
|
+
const removedDrafts = paginator.removeRows(
|
|
49
|
+
(row) => row.status === 'draft',
|
|
50
|
+
{ adjustTotal: false }
|
|
51
|
+
)
|
|
52
|
+
```
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Requests can be aborted by passing an `AbortSignal` to the request.
|
|
4
4
|
|
|
5
|
+
If you want the request library to abort previous in-flight requests automatically, see [Concurrency](/services/requests/concurrency).
|
|
6
|
+
|
|
5
7
|
## Using AbortController
|
|
6
8
|
|
|
7
9
|
```typescript
|
|
@@ -16,6 +18,8 @@ const promise = request.send()
|
|
|
16
18
|
controller.abort()
|
|
17
19
|
```
|
|
18
20
|
|
|
21
|
+
Note: If you enable request concurrency with `REPLACE` or `REPLACE_LATEST`, the request will assign its own abort signal and override this one. See [Concurrency](/services/requests/concurrency) for details.
|
|
22
|
+
|
|
19
23
|
## Bulk Requests
|
|
20
24
|
|
|
21
25
|
`BulkRequestSender` internally manages an `AbortController` for its requests. You can abort the entire bulk operation:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Request Concurrency
|
|
2
|
+
|
|
3
|
+
Concurrent requests can cause two common problems:
|
|
4
|
+
|
|
5
|
+
1. A slower, older response overwrites newer data ("stale results").
|
|
6
|
+
2. Loading state flickers because earlier requests finish after later ones.
|
|
7
|
+
|
|
8
|
+
To solve this, `BaseRequest` supports an optional concurrency policy that can abort older requests and/or ignore stale responses.
|
|
9
|
+
|
|
10
|
+
## Basic Usage
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { RequestConcurrencyMode } from '@blueprint-ts/core/requests'
|
|
14
|
+
|
|
15
|
+
const request = new ExpenseIndexRequest()
|
|
16
|
+
|
|
17
|
+
request.setConcurrency({
|
|
18
|
+
mode: RequestConcurrencyMode.REPLACE_LATEST,
|
|
19
|
+
key: 'expense-search'
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
request.send()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Modes
|
|
26
|
+
|
|
27
|
+
- `ALLOW` (default): no aborts and no stale-response filtering.
|
|
28
|
+
- `REPLACE`: aborts any in-flight request with the same key.
|
|
29
|
+
- `LATEST`: ignores stale responses; only the most recent response is applied.
|
|
30
|
+
- `REPLACE_LATEST`: aborts older requests and ignores stale responses.
|
|
31
|
+
|
|
32
|
+
## Abort Signals
|
|
33
|
+
|
|
34
|
+
When using `REPLACE` or `REPLACE_LATEST`, the request creates and assigns its own `AbortController` for the concurrency key. This replaces any previously configured abort signal on that request instance. If you need to preserve a custom abort signal, apply it per request without using replace modes.
|
|
35
|
+
|
|
36
|
+
## Keys
|
|
37
|
+
|
|
38
|
+
The `key` lets you coordinate concurrency across multiple request instances. If you omit it, the request instance ID is used.
|
|
39
|
+
|
|
40
|
+
Use a shared key when multiple instances represent the same logical request stream (for example, a search box that creates new request objects).
|
|
41
|
+
|
|
42
|
+
## Stale Responses
|
|
43
|
+
|
|
44
|
+
When `LATEST` or `REPLACE_LATEST` is used, stale responses raise a `StaleResponseException` so the caller can ignore them safely.
|
|
45
|
+
|
|
46
|
+
If you don't want to handle it explicitly, catch and ignore it:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { StaleResponseException } from '@blueprint-ts/core/requests'
|
|
50
|
+
|
|
51
|
+
request.send().catch((error) => {
|
|
52
|
+
if (error instanceof StaleResponseException) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw error
|
|
57
|
+
})
|
|
58
|
+
```
|
|
@@ -13,6 +13,22 @@ When a request fails, `BaseRequest.send()` routes error responses through the re
|
|
|
13
13
|
|
|
14
14
|
## Catching Errors
|
|
15
15
|
|
|
16
|
+
When using request concurrency (see [Concurrency](/services/requests/concurrency)), `BaseRequest` can throw a `StaleResponseException` for outdated responses. These should usually be ignored:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { StaleResponseException } from '@blueprint-ts/core/requests'
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await request.send()
|
|
23
|
+
} catch (error: unknown) {
|
|
24
|
+
if (error instanceof StaleResponseException) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw error
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
16
32
|
- `400` -> `BadRequestException`
|
|
17
33
|
- `401` -> `UnauthorizedException`
|
|
18
34
|
- `403` -> `ForbiddenException`
|
|
@@ -78,6 +78,24 @@ Page navigation helpers (`toNextPage`, `toPreviousPage`, `toFirstPage`, `toLastP
|
|
|
78
78
|
`PaginationParamsContract` was removed from the Laravel pagination exports because it was unused. Define your own params
|
|
79
79
|
interface in your app instead.
|
|
80
80
|
|
|
81
|
+
## Deprecated `Paginator` alias removed
|
|
82
|
+
|
|
83
|
+
`Paginator` (and the related `PaginatorOptions` alias) was an old alias for `PageAwarePaginator`. The alias file has been removed, so importing `Paginator` or `PaginatorOptions` now results in a build error.
|
|
84
|
+
|
|
85
|
+
### How to Fix
|
|
86
|
+
|
|
87
|
+
- Replace `import { Paginator } from '@blueprint-ts/core/pagination'` with:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { PageAwarePaginator } from '@blueprint-ts/core/pagination'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- Replace `import { type PaginatorOptions } from '@blueprint-ts/core/pagination'` with:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { type PageAwarePaginatorOptions } from '@blueprint-ts/core/pagination'
|
|
97
|
+
```
|
|
98
|
+
|
|
81
99
|
## Core Modules Moved out of `service/`
|
|
82
100
|
|
|
83
101
|
Core (non-framework) modules moved from `@blueprint-ts/core/service/*` to `@blueprint-ts/core/*`.
|
|
@@ -152,6 +170,22 @@ export class MyConfirmOptions implements ConfirmDialogOptions {
|
|
|
152
170
|
}
|
|
153
171
|
```
|
|
154
172
|
|
|
173
|
+
## BaseRequestContract Requires `setConcurrency`
|
|
174
|
+
|
|
175
|
+
`BaseRequestContract` now includes `setConcurrency(...)` so requests can opt into concurrency handling (e.g., latest-wins or replace-latest).
|
|
176
|
+
If you implemented `BaseRequestContract` directly (instead of extending `BaseRequest`), TypeScript will now require this method.
|
|
177
|
+
|
|
178
|
+
### How to Fix
|
|
179
|
+
|
|
180
|
+
Add the method to your custom request implementation:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
setConcurrency(options?: RequestConcurrencyOptions): this {
|
|
184
|
+
// no-op by default; use options if your implementation needs them
|
|
185
|
+
return this
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
155
189
|
## Laravel Packages Moved
|
|
156
190
|
|
|
157
191
|
Laravel modules moved from `@blueprint-ts/core/service/laravel/*` to `@blueprint-ts/core/laravel/*`.
|
|
@@ -169,3 +203,12 @@ with:
|
|
|
169
203
|
```typescript
|
|
170
204
|
import { JsonBaseRequest } from '@blueprint-ts/core/laravel/requests'
|
|
171
205
|
```
|
|
206
|
+
|
|
207
|
+
## BaseForm `state` is now protected
|
|
208
|
+
|
|
209
|
+
`BaseForm.state` was public in previous versions, but it is now `protected readonly`. That means external consumers can no longer import form instances and access `form.state`. Subclasses can still read/write state internally, so any code that depended on reading it from outside the form must switch to supported accessors such as `form.properties`.
|
|
210
|
+
|
|
211
|
+
### How to Fix
|
|
212
|
+
|
|
213
|
+
- Replace direct `form.state` usage with the public helpers (`form.properties`, getters, or explicit payload builders).
|
|
214
|
+
- If you were creating helpers that accessed `state` from outside the form, move those helpers inside the form class so they can rely on the protected field.
|
package/docs/vue/forms/arrays.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Arrays
|
|
2
2
|
|
|
3
|
+
## PropertyAwareArray
|
|
4
|
+
|
|
3
5
|
Use `PropertyAwareArray` for arrays with per-item `v-model`, errors, and dirty state when your array contains objects.
|
|
4
6
|
|
|
5
7
|
```ts
|
|
@@ -43,3 +45,58 @@ Example component usage:
|
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
Nested error keys like `positions.0.value` map into `position.value.errors`.
|
|
48
|
+
|
|
49
|
+
## propertyAwareToRaw
|
|
50
|
+
|
|
51
|
+
`propertyAwareToRaw` converts a property-aware object (each field wrapped in `{ model: { value } }`) into a plain object by unwrapping every field's `model.value`. It is purely a transformation: metadata such as `errors`, `dirty`, and `touched` is dropped, while arrays and nested objects are processed recursively. Keys starting with `_` are omitted from the result, and arrays are mapped element-by-element via the same unwrapping.
|
|
52
|
+
|
|
53
|
+
### Input shape (property-aware)
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const propertyAwarePosition = {
|
|
57
|
+
id: {
|
|
58
|
+
model: { value: 'pos-1' },
|
|
59
|
+
errors: [],
|
|
60
|
+
dirty: false,
|
|
61
|
+
touched: false
|
|
62
|
+
},
|
|
63
|
+
sort_order: {
|
|
64
|
+
model: { value: 10 },
|
|
65
|
+
errors: [],
|
|
66
|
+
dirty: false,
|
|
67
|
+
touched: false
|
|
68
|
+
},
|
|
69
|
+
description: {
|
|
70
|
+
model: { value: 'Service' },
|
|
71
|
+
errors: [],
|
|
72
|
+
dirty: false,
|
|
73
|
+
touched: false
|
|
74
|
+
},
|
|
75
|
+
net_amount: {
|
|
76
|
+
model: { value: 100 },
|
|
77
|
+
errors: [],
|
|
78
|
+
dirty: false,
|
|
79
|
+
touched: false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Note that this is only an example of how the input shape looks When used in a form the model is a WritableComputedRef.
|
|
85
|
+
|
|
86
|
+
### Output shape (raw)
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const rawPosition = {
|
|
90
|
+
id: 'pos-1',
|
|
91
|
+
sort_order: 10,
|
|
92
|
+
description: 'Service',
|
|
93
|
+
net_amount: 100
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Notes
|
|
98
|
+
|
|
99
|
+
- Arrays are mapped element-by-element.
|
|
100
|
+
- Nested objects are unwrapped recursively.
|
|
101
|
+
- Any keys starting with `_` are ignored.
|
|
102
|
+
- Only the `model.value` remains; form metadata is dropped.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blueprint-ts/core",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.10",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"vue-router": "^4.3.2"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"lodash-es": "^4.17.
|
|
78
|
-
"qs": "^6.
|
|
79
|
-
"uuid": "^
|
|
77
|
+
"lodash-es": "^4.17.23",
|
|
78
|
+
"qs": "^6.15.0",
|
|
79
|
+
"uuid": "^13.0.0"
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -42,7 +42,7 @@ export abstract class BasePaginator<ResourceInterface, ViewDriver extends BaseVi
|
|
|
42
42
|
const result = updater(row, i, data)
|
|
43
43
|
|
|
44
44
|
if (result !== undefined) {
|
|
45
|
-
data[i] = result
|
|
45
|
+
data[i] = result as ResourceInterface
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
updated++
|
|
@@ -55,6 +55,25 @@ export abstract class BasePaginator<ResourceInterface, ViewDriver extends BaseVi
|
|
|
55
55
|
return updated
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
public removeRows(
|
|
59
|
+
predicate: (row: ResourceInterface, index: number, data: ResourceInterface[]) => boolean,
|
|
60
|
+
options?: { adjustTotal?: boolean }
|
|
61
|
+
): number {
|
|
62
|
+
const data = this.viewDriver.getData()
|
|
63
|
+
const next = data.filter((row, index) => !predicate(row, index, data))
|
|
64
|
+
const removed = data.length - next.length
|
|
65
|
+
|
|
66
|
+
if (removed > 0) {
|
|
67
|
+
this.viewDriver.setData(next)
|
|
68
|
+
|
|
69
|
+
if (options?.adjustTotal ?? true) {
|
|
70
|
+
this.viewDriver.setTotal(Math.max(0, this.viewDriver.getTotal() - removed))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return removed
|
|
75
|
+
}
|
|
76
|
+
|
|
58
77
|
public getTotal(): number {
|
|
59
78
|
return this.viewDriver.getTotal()
|
|
60
79
|
}
|
|
@@ -68,4 +87,8 @@ export abstract class BasePaginator<ResourceInterface, ViewDriver extends BaseVi
|
|
|
68
87
|
this.viewDriver.setTotal(dto.getTotal())
|
|
69
88
|
this.initialized = true
|
|
70
89
|
}
|
|
90
|
+
|
|
91
|
+
protected handleStaleResponse(): PaginationDataDto<ResourceInterface[]> {
|
|
92
|
+
return new PaginationDataDto<ResourceInterface[]>(this.viewDriver.getData(), this.viewDriver.getTotal())
|
|
93
|
+
}
|
|
71
94
|
}
|
|
@@ -4,6 +4,7 @@ import { type ViewDriverFactoryContract } from './contracts/ViewDriverFactoryCon
|
|
|
4
4
|
import { type PaginatorLoadDataOptions } from './contracts/PaginatorLoadDataOptions'
|
|
5
5
|
import { type PaginationDataDriverContract } from './contracts/PaginationDataDriverContract'
|
|
6
6
|
import { BasePaginator } from './BasePaginator'
|
|
7
|
+
import { StaleResponseException } from '../requests/exceptions/StaleResponseException'
|
|
7
8
|
|
|
8
9
|
export interface PageAwarePaginatorOptions {
|
|
9
10
|
viewDriverFactory?: ViewDriverFactoryContract
|
|
@@ -113,10 +114,19 @@ export class PageAwarePaginator<ResourceInterface> extends BasePaginator<Resourc
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
protected loadData(pageNumber: number, pageSize: number, options?: PaginatorLoadDataOptions): Promise<PaginationDataDto<ResourceInterface[]>> {
|
|
116
|
-
return this.dataDriver
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
return this.dataDriver
|
|
118
|
+
.get(pageNumber, pageSize)
|
|
119
|
+
.then((value: PaginationDataDto<ResourceInterface[]>) => {
|
|
120
|
+
this.passDataToViewDriver(value, options)
|
|
121
|
+
|
|
122
|
+
return value
|
|
123
|
+
})
|
|
124
|
+
.catch((error) => {
|
|
125
|
+
if (error instanceof StaleResponseException) {
|
|
126
|
+
return this.handleStaleResponse()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw error
|
|
130
|
+
})
|
|
121
131
|
}
|
|
122
132
|
}
|
package/src/pagination/index.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { PaginationDataDto } from './dtos/PaginationDataDto'
|
|
|
2
2
|
import { StatePaginationDataDto } from './dtos/StatePaginationDataDto'
|
|
3
3
|
import { VuePaginationDriver } from './frontendDrivers/VuePaginationDriver'
|
|
4
4
|
import { VueBaseViewDriver } from './frontendDrivers/VueBaseViewDriver'
|
|
5
|
-
import { Paginator } from './Paginator'
|
|
6
5
|
import { BasePaginator } from './BasePaginator'
|
|
7
6
|
import { PageAwarePaginator } from './PageAwarePaginator'
|
|
8
7
|
import { StatePaginator } from './StatePaginator'
|
|
@@ -25,7 +24,6 @@ export {
|
|
|
25
24
|
StatePaginationDataDto,
|
|
26
25
|
VuePaginationDriver,
|
|
27
26
|
VueBaseViewDriver,
|
|
28
|
-
Paginator,
|
|
29
27
|
BasePaginator,
|
|
30
28
|
PageAwarePaginator,
|
|
31
29
|
StatePaginator,
|
|
@@ -4,6 +4,7 @@ import { RequestEvents } from './RequestEvents.enum'
|
|
|
4
4
|
import { RequestMethodEnum } from './RequestMethod.enum'
|
|
5
5
|
import { BaseResponse } from './responses/BaseResponse'
|
|
6
6
|
import { ResponseException } from './exceptions/ResponseException'
|
|
7
|
+
import { StaleResponseException } from './exceptions/StaleResponseException'
|
|
7
8
|
import { type DriverConfigContract } from './contracts/DriverConfigContract'
|
|
8
9
|
import { type BodyFactoryContract } from './contracts/BodyFactoryContract'
|
|
9
10
|
import { type RequestLoaderContract } from './contracts/RequestLoaderContract'
|
|
@@ -13,6 +14,8 @@ import { type BaseRequestContract, type EventHandlerCallback } from './contracts
|
|
|
13
14
|
import { type HeadersContract } from './contracts/HeadersContract'
|
|
14
15
|
import { type ResponseHandlerContract } from './drivers/contracts/ResponseHandlerContract'
|
|
15
16
|
import { type ResponseContract } from './contracts/ResponseContract'
|
|
17
|
+
import { type RequestConcurrencyOptions } from './types/RequestConcurrencyOptions'
|
|
18
|
+
import { RequestConcurrencyMode } from './RequestConcurrencyMode.enum'
|
|
16
19
|
import { mergeDeep } from '../support/helpers'
|
|
17
20
|
import { v4 as uuidv4 } from 'uuid'
|
|
18
21
|
|
|
@@ -29,6 +32,7 @@ export abstract class BaseRequest<
|
|
|
29
32
|
protected requestBody: RequestBodyInterface | undefined = undefined
|
|
30
33
|
protected requestLoader: RequestLoaderContract<RequestLoaderLoadingType> | undefined = undefined
|
|
31
34
|
protected abortSignal: AbortSignal | undefined = undefined
|
|
35
|
+
protected concurrencyOptions: RequestConcurrencyOptions | undefined = undefined
|
|
32
36
|
/* @ts-expect-error Ignore generics */
|
|
33
37
|
protected events: { [key in RequestEvents]?: EventHandlerCallback[] } = {}
|
|
34
38
|
|
|
@@ -36,6 +40,9 @@ export abstract class BaseRequest<
|
|
|
36
40
|
|
|
37
41
|
protected static requestDriver: RequestDriverContract
|
|
38
42
|
protected static requestLoaderFactory: RequestLoaderFactoryContract<unknown>
|
|
43
|
+
protected static concurrencySequenceByKey: Map<string, number> = new Map()
|
|
44
|
+
protected static concurrencyAbortControllerByKey: Map<string, AbortController> = new Map()
|
|
45
|
+
protected static concurrencyInFlightByKey: Map<string, number> = new Map()
|
|
39
46
|
|
|
40
47
|
public constructor() {
|
|
41
48
|
if (BaseRequest.requestLoaderFactory !== undefined) {
|
|
@@ -61,6 +68,12 @@ export abstract class BaseRequest<
|
|
|
61
68
|
return this
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
public setConcurrency(options?: RequestConcurrencyOptions): this {
|
|
72
|
+
this.concurrencyOptions = options
|
|
73
|
+
|
|
74
|
+
return this
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
public getRequestId(): string {
|
|
65
78
|
return this.requestId
|
|
66
79
|
}
|
|
@@ -124,6 +137,25 @@ export abstract class BaseRequest<
|
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
public async send(): Promise<ResponseClass> {
|
|
140
|
+
const concurrencyMode: RequestConcurrencyMode = this.concurrencyOptions?.mode ?? RequestConcurrencyMode.ALLOW
|
|
141
|
+
const concurrencyKey = this.concurrencyOptions?.key ?? this.requestId
|
|
142
|
+
const useReplace = concurrencyMode === RequestConcurrencyMode.REPLACE || concurrencyMode === RequestConcurrencyMode.REPLACE_LATEST
|
|
143
|
+
const useLatest = concurrencyMode === RequestConcurrencyMode.LATEST || concurrencyMode === RequestConcurrencyMode.REPLACE_LATEST
|
|
144
|
+
const sequence = this.bumpConcurrencySequence(concurrencyKey)
|
|
145
|
+
this.incrementConcurrencyInFlight(concurrencyKey)
|
|
146
|
+
|
|
147
|
+
if (useReplace) {
|
|
148
|
+
const previousController = BaseRequest.concurrencyAbortControllerByKey.get(concurrencyKey)
|
|
149
|
+
|
|
150
|
+
if (previousController) {
|
|
151
|
+
previousController.abort()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const controller = new AbortController()
|
|
155
|
+
BaseRequest.concurrencyAbortControllerByKey.set(concurrencyKey, controller)
|
|
156
|
+
this.setAbortSignal(controller.signal)
|
|
157
|
+
}
|
|
158
|
+
|
|
127
159
|
this.dispatch<boolean>(RequestEvents.LOADING, true)
|
|
128
160
|
|
|
129
161
|
this.requestLoader?.setLoading(true)
|
|
@@ -144,11 +176,23 @@ export abstract class BaseRequest<
|
|
|
144
176
|
this.getConfig()
|
|
145
177
|
)
|
|
146
178
|
.then(async (responseHandler: ResponseHandlerContract) => {
|
|
179
|
+
if (useLatest && !this.isLatestSequence(concurrencyKey, sequence)) {
|
|
180
|
+
throw new StaleResponseException()
|
|
181
|
+
}
|
|
182
|
+
|
|
147
183
|
await responseSkeleton.setResponse(responseHandler)
|
|
148
184
|
|
|
149
185
|
return responseSkeleton
|
|
150
186
|
})
|
|
151
187
|
.catch(async (error) => {
|
|
188
|
+
if (useLatest && !this.isLatestSequence(concurrencyKey, sequence)) {
|
|
189
|
+
throw new StaleResponseException('Stale response ignored', error)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (error instanceof StaleResponseException) {
|
|
193
|
+
throw error
|
|
194
|
+
}
|
|
195
|
+
|
|
152
196
|
if (error instanceof ResponseException) {
|
|
153
197
|
const handler = new ErrorHandler<ResponseErrorBody>(error.getResponse())
|
|
154
198
|
|
|
@@ -160,8 +204,14 @@ export abstract class BaseRequest<
|
|
|
160
204
|
throw error
|
|
161
205
|
})
|
|
162
206
|
.finally(() => {
|
|
163
|
-
this.
|
|
164
|
-
|
|
207
|
+
const isStale = useLatest && !this.isLatestSequence(concurrencyKey, sequence)
|
|
208
|
+
|
|
209
|
+
if (!isStale) {
|
|
210
|
+
this.dispatch<boolean>(RequestEvents.LOADING, false)
|
|
211
|
+
this.requestLoader?.setLoading(false)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.decrementConcurrencyInFlight(concurrencyKey)
|
|
165
215
|
})
|
|
166
216
|
}
|
|
167
217
|
|
|
@@ -185,6 +235,41 @@ export abstract class BaseRequest<
|
|
|
185
235
|
return this
|
|
186
236
|
}
|
|
187
237
|
|
|
238
|
+
protected bumpConcurrencySequence(key: string): number {
|
|
239
|
+
const next = (BaseRequest.concurrencySequenceByKey.get(key) ?? 0) + 1
|
|
240
|
+
BaseRequest.concurrencySequenceByKey.set(key, next)
|
|
241
|
+
|
|
242
|
+
return next
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
protected isLatestSequence(key: string, sequence: number): boolean {
|
|
246
|
+
return (BaseRequest.concurrencySequenceByKey.get(key) ?? 0) === sequence
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
protected incrementConcurrencyInFlight(key: string): void {
|
|
250
|
+
const next = (BaseRequest.concurrencyInFlightByKey.get(key) ?? 0) + 1
|
|
251
|
+
BaseRequest.concurrencyInFlightByKey.set(key, next)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
protected decrementConcurrencyInFlight(key: string): void {
|
|
255
|
+
const current = BaseRequest.concurrencyInFlightByKey.get(key)
|
|
256
|
+
|
|
257
|
+
if (current === undefined) {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const next = current - 1
|
|
262
|
+
|
|
263
|
+
if (next <= 0) {
|
|
264
|
+
BaseRequest.concurrencyInFlightByKey.delete(key)
|
|
265
|
+
BaseRequest.concurrencySequenceByKey.delete(key)
|
|
266
|
+
BaseRequest.concurrencyAbortControllerByKey.delete(key)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
BaseRequest.concurrencyInFlightByKey.set(key, next)
|
|
271
|
+
}
|
|
272
|
+
|
|
188
273
|
protected baseUrl(): undefined {
|
|
189
274
|
return undefined
|
|
190
275
|
}
|
|
@@ -2,6 +2,7 @@ import { RequestMethodEnum } from '../RequestMethod.enum'
|
|
|
2
2
|
import { RequestEvents } from '../RequestEvents.enum'
|
|
3
3
|
import { type BodyFactoryContract } from './BodyFactoryContract'
|
|
4
4
|
import { type HeadersContract } from './HeadersContract'
|
|
5
|
+
import { type RequestConcurrencyOptions } from '../types/RequestConcurrencyOptions'
|
|
5
6
|
|
|
6
7
|
export type EventHandlerCallback<T> = (value: T) => void
|
|
7
8
|
|
|
@@ -33,4 +34,6 @@ export interface BaseRequestContract<RequestLoaderLoadingType, RequestBodyInterf
|
|
|
33
34
|
getResponse(): ResponseClass
|
|
34
35
|
|
|
35
36
|
setAbortSignal(signal: AbortSignal): this
|
|
37
|
+
|
|
38
|
+
setConcurrency(options?: RequestConcurrencyOptions): this
|
|
36
39
|
}
|
|
@@ -4,7 +4,7 @@ import { type ResponseHandlerContract } from '../drivers/contracts/ResponseHandl
|
|
|
4
4
|
export class InvalidJsonException extends ResponseException {
|
|
5
5
|
public constructor(
|
|
6
6
|
response: ResponseHandlerContract,
|
|
7
|
-
|
|
7
|
+
public cause: unknown
|
|
8
8
|
) {
|
|
9
9
|
super(response)
|
|
10
10
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class StaleResponseException extends Error {
|
|
2
|
+
public readonly cause: unknown
|
|
3
|
+
|
|
4
|
+
public constructor(message: string = 'Stale response ignored', cause?: unknown) {
|
|
5
|
+
super(message)
|
|
6
|
+
this.name = 'StaleResponseException'
|
|
7
|
+
this.cause = cause
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public getCause(): unknown {
|
|
11
|
+
return this.cause
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -3,10 +3,13 @@ import { type BodyFactoryContract } from '../contracts/BodyFactoryContract'
|
|
|
3
3
|
import { type BodyContract } from '../contracts/BodyContract'
|
|
4
4
|
|
|
5
5
|
type FormDataPrimitive = string | number | boolean | null | Date | Blob
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
export type FormDataValue = FormDataPrimitive | FormDataValue[] | { [key: string]: FormDataValue }
|
|
7
8
|
|
|
8
9
|
export class FormDataFactory<
|
|
9
|
-
RequestBodyInterface extends
|
|
10
|
+
RequestBodyInterface extends {
|
|
11
|
+
[K in keyof RequestBodyInterface]: FormDataValue | undefined
|
|
12
|
+
}
|
|
10
13
|
> implements BodyFactoryContract<RequestBodyInterface> {
|
|
11
14
|
public make(body: RequestBodyInterface): BodyContract {
|
|
12
15
|
return new FormDataBody<RequestBodyInterface>(body)
|
package/src/requests/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { ErrorHandler } from './ErrorHandler'
|
|
|
8
8
|
import { RequestErrorRouter } from './RequestErrorRouter'
|
|
9
9
|
import { RequestEvents } from './RequestEvents.enum'
|
|
10
10
|
import { RequestMethodEnum } from './RequestMethod.enum'
|
|
11
|
+
import { RequestConcurrencyMode } from './RequestConcurrencyMode.enum'
|
|
11
12
|
import { JsonBodyFactory } from './factories/JsonBodyFactory'
|
|
12
13
|
import { FormDataFactory } from './factories/FormDataFactory'
|
|
13
14
|
import { type BodyContract } from './contracts/BodyContract'
|
|
@@ -19,7 +20,9 @@ import { type BodyFactoryContract } from './contracts/BodyFactoryContract'
|
|
|
19
20
|
import { type ResponseHandlerContract } from './drivers/contracts/ResponseHandlerContract'
|
|
20
21
|
import { type BaseRequestContract } from './contracts/BaseRequestContract'
|
|
21
22
|
import { ResponseException } from './exceptions/ResponseException'
|
|
23
|
+
import { StaleResponseException } from './exceptions/StaleResponseException'
|
|
22
24
|
import { type HeadersContract } from './contracts/HeadersContract'
|
|
25
|
+
import { type RequestConcurrencyOptions } from './types/RequestConcurrencyOptions'
|
|
23
26
|
|
|
24
27
|
export {
|
|
25
28
|
FetchDriver,
|
|
@@ -32,7 +35,9 @@ export {
|
|
|
32
35
|
RequestErrorRouter,
|
|
33
36
|
RequestEvents,
|
|
34
37
|
RequestMethodEnum,
|
|
38
|
+
RequestConcurrencyMode,
|
|
35
39
|
ResponseException,
|
|
40
|
+
StaleResponseException,
|
|
36
41
|
JsonBodyFactory,
|
|
37
42
|
FormDataFactory
|
|
38
43
|
}
|
|
@@ -46,5 +51,6 @@ export type {
|
|
|
46
51
|
BodyFactoryContract,
|
|
47
52
|
ResponseHandlerContract,
|
|
48
53
|
BaseRequestContract,
|
|
49
|
-
HeadersContract
|
|
54
|
+
HeadersContract,
|
|
55
|
+
RequestConcurrencyOptions
|
|
50
56
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { reactive, computed, toRaw, type ComputedRef, watch } from 'vue'
|
|
1
|
+
import { reactive, computed, toRaw, type ComputedRef, type WritableComputedRef, watch } from 'vue'
|
|
2
2
|
import { camelCase, upperFirst, cloneDeep, isEqual } from 'lodash-es'
|
|
3
3
|
import isEqualWith from 'lodash-es/isEqualWith'
|
|
4
4
|
import { type PersistedForm } from './types/PersistedForm'
|
|
@@ -21,12 +21,21 @@ type FieldErrors = ErrorMessages | ErrorObject | ErrorArray
|
|
|
21
21
|
type ErrorBag = Record<string, FieldErrors>
|
|
22
22
|
|
|
23
23
|
type FieldProperty<T> = {
|
|
24
|
-
model:
|
|
24
|
+
model: WritableComputedRef<T>
|
|
25
25
|
errors: ErrorMessages
|
|
26
26
|
dirty: boolean
|
|
27
27
|
touched: boolean
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
type PropertyAwareToRaw<T> =
|
|
31
|
+
T extends Array<infer U>
|
|
32
|
+
? Array<PropertyAwareToRaw<U>>
|
|
33
|
+
: T extends { model: { value: infer V } }
|
|
34
|
+
? V
|
|
35
|
+
: T extends object
|
|
36
|
+
? { [K in keyof T]: PropertyAwareToRaw<T[K]> }
|
|
37
|
+
: T
|
|
38
|
+
|
|
30
39
|
type FormKey<FormBody extends object> = Extract<keyof FormBody, string>
|
|
31
40
|
type RequestKey<RequestBody extends object> = Extract<keyof RequestBody, string>
|
|
32
41
|
type ArrayItem<T> = T extends Array<infer Item> ? Item : never
|
|
@@ -54,13 +63,13 @@ function isErrorObject(value: unknown): value is ErrorObject {
|
|
|
54
63
|
return isRecord(value)
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
export function propertyAwareToRaw<T>(propertyAwareObject: T): T {
|
|
66
|
+
export function propertyAwareToRaw<T>(propertyAwareObject: T): PropertyAwareToRaw<T> {
|
|
58
67
|
if (Array.isArray(propertyAwareObject)) {
|
|
59
|
-
return propertyAwareObject.map((item) => propertyAwareToRaw(item)) as T
|
|
68
|
+
return propertyAwareObject.map((item) => propertyAwareToRaw(item)) as PropertyAwareToRaw<T>
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
if (!isRecord(propertyAwareObject)) {
|
|
63
|
-
return propertyAwareObject
|
|
72
|
+
return propertyAwareObject as PropertyAwareToRaw<T>
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
const result: Record<string, unknown> = {}
|
|
@@ -87,7 +96,7 @@ export function propertyAwareToRaw<T>(propertyAwareObject: T): T {
|
|
|
87
96
|
result[key] = value
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
return result as T
|
|
99
|
+
return result as PropertyAwareToRaw<T>
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
/** Helper: shallow-merge source object into target. */
|
|
@@ -160,11 +169,11 @@ function propertyAwareDeepEqual<T>(a: T, b: T): boolean {
|
|
|
160
169
|
* (We assume that for every key in RequestBody there is a corresponding key in FormBody.)
|
|
161
170
|
*/
|
|
162
171
|
export abstract class BaseForm<RequestBody extends object, FormBody extends object> {
|
|
163
|
-
|
|
172
|
+
protected readonly state: FormBody
|
|
164
173
|
private readonly dirty: DirtyMap<FormBody>
|
|
165
174
|
private readonly touched: Record<keyof FormBody, boolean>
|
|
166
175
|
private readonly original: FormBody
|
|
167
|
-
private readonly _model: { [K in keyof FormBody]:
|
|
176
|
+
private readonly _model: { [K in keyof FormBody]: WritableComputedRef<FormBody[K]> }
|
|
168
177
|
private _errors: ErrorBag = reactive<ErrorBag>({})
|
|
169
178
|
private _hasErrors: ComputedRef<boolean>
|
|
170
179
|
protected append: string[] = []
|
|
@@ -375,7 +384,7 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
|
|
|
375
384
|
this.buildFieldDependencies()
|
|
376
385
|
|
|
377
386
|
this.state = reactive(initialData) as FormBody
|
|
378
|
-
this._model = {} as { [K in keyof FormBody]:
|
|
387
|
+
this._model = {} as { [K in keyof FormBody]: WritableComputedRef<FormBody[K]> }
|
|
379
388
|
|
|
380
389
|
for (const key in this.state) {
|
|
381
390
|
const value = this.state[key]
|
|
@@ -795,12 +804,18 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
|
|
|
795
804
|
|
|
796
805
|
private getValueGetter(name: string): ((value: unknown) => unknown) | undefined {
|
|
797
806
|
const candidate = (this as Record<string, unknown>)[name]
|
|
798
|
-
|
|
807
|
+
if (typeof candidate !== 'function') {
|
|
808
|
+
return undefined
|
|
809
|
+
}
|
|
810
|
+
return (candidate as (value: unknown) => unknown).bind(this)
|
|
799
811
|
}
|
|
800
812
|
|
|
801
813
|
private getNoArgGetter(name: string): (() => unknown) | undefined {
|
|
802
814
|
const candidate = (this as Record<string, unknown>)[name]
|
|
803
|
-
|
|
815
|
+
if (typeof candidate !== 'function') {
|
|
816
|
+
return undefined
|
|
817
|
+
}
|
|
818
|
+
return (candidate as () => unknown).bind(this)
|
|
804
819
|
}
|
|
805
820
|
|
|
806
821
|
private transformValue(value: unknown, parentKey?: string): unknown {
|
|
@@ -23,12 +23,16 @@ export type PropertyAware<T> = {
|
|
|
23
23
|
* computed getters/setters, error tracking, and dirty flags.
|
|
24
24
|
*/
|
|
25
25
|
export class PropertyAwareArray<T = unknown> extends Array<T> {
|
|
26
|
+
// Private brand to prevent plain arrays from being assignable to PropertyAwareArray.
|
|
27
|
+
// This keeps conditional types from treating normal arrays as property-aware.
|
|
28
|
+
private readonly __propertyAwareArrayBrand!: void
|
|
26
29
|
/**
|
|
27
30
|
* Creates a new PropertyAwareArray instance
|
|
28
31
|
*/
|
|
29
32
|
public constructor(items: T[] = []) {
|
|
30
33
|
// Call Array constructor with array length
|
|
31
34
|
super()
|
|
35
|
+
void this.__propertyAwareArrayBrand
|
|
32
36
|
|
|
33
37
|
// Add items to the array
|
|
34
38
|
if (items && items.length) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { VueRequestLoader } from '../loaders/VueRequestLoader'
|
|
2
2
|
import { type RequestLoaderContract } from '../../../requests/contracts/RequestLoaderContract'
|
|
3
3
|
import { type Ref } from 'vue'
|
|
4
|
-
import { RequestLoaderFactoryContract } from '../../../requests'
|
|
4
|
+
import type { RequestLoaderFactoryContract } from '../../../requests'
|
|
5
5
|
|
|
6
6
|
export class VueRequestLoaderFactory implements RequestLoaderFactoryContract<Ref<boolean>> {
|
|
7
7
|
public make(): RequestLoaderContract<Ref<boolean>> {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { PageAwarePaginator, type PageAwarePaginatorOptions } from './PageAwarePaginator'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @deprecated Use PageAwarePaginator instead. This alias is kept for backward compatibility.
|
|
5
|
-
*/
|
|
6
|
-
export const Paginator = PageAwarePaginator
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @deprecated Use PageAwarePaginatorOptions instead. This alias is kept for backward compatibility.
|
|
10
|
-
*/
|
|
11
|
-
export type PaginatorOptions = PageAwarePaginatorOptions
|