@vc-shell/vc-app-skill 2.4.0 → 2.5.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/package.json +1 -1
- package/runtime/VERSION +1 -1
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/composables/useAsync/useAsync.docs.md +1 -0
- package/runtime/knowledge/docs/core/composables/useLatestRequest/useLatestRequest.docs.md +149 -0
- package/runtime/knowledge/docs/core/composables/usePopup/usePopup.docs.md +32 -7
- package/runtime/knowledge/docs/core/plugins/ai-agent/ai-agent.docs.md +20 -0
- package/runtime/knowledge/docs/shell/dashboard/draggable-dashboard/draggable-dashboard.docs.md +10 -8
- package/runtime/knowledge/docs/ui/components/organisms/vc-scheduler/vc-scheduler.docs.md +13 -11
- package/runtime/knowledge/docs/ui/composables/useDataTablePagination.docs.md +15 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vc-shell/vc-app-skill",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "AI coding skill for scaffolding and generating VirtoCommerce Shell applications. Works with Claude Code, OpenCode, Gemini, Codex, Cursor.",
|
|
5
5
|
"bin": "./bin/install.cjs",
|
|
6
6
|
"files": [
|
package/runtime/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.5.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Synced from framework at commit
|
|
1
|
+
Synced from framework at commit 2964ccbe3 on 2026-08-19T10:23:33.758Z
|
|
@@ -422,6 +422,7 @@ const { action: save, loading: saveLoading } = useAsync(async () => saveData());
|
|
|
422
422
|
- Toast notifications are deferred with `setTimeout(0)` and registered via `setPendingErrorNotification`. The `ErrorInterceptor` (blade-level `onErrorCaptured`) can call `cancelPendingErrorNotification` to suppress the toast when a blade error banner is shown instead.
|
|
423
423
|
- The notification module is lazy-imported to avoid circular dependencies with `@core/composables`.
|
|
424
424
|
- `isSessionExpired()` from `@core/utilities/sessionExpiration` gates the notification. The flag is set by the fetch interceptor on the 401 that kills the session and cleared by `useUser.signIn`. It is imported directly, not through the `@core/utilities` barrel, for the same circular-dependency reason as `pendingErrorNotifications`.
|
|
425
|
+
- On platforms that redirect an unauthenticated API call to the login page instead of answering 401, the interceptor rejects the request with a `SessionExpiredError` rather than handing the page's HTML back. Without that, every caller parsed a document as data and raised its own `Unexpected token '<'` error while the app was already redirecting to login. Actions failing that way carry one identical message, so this suppression and the `notificationId` de-duplication both collapse them to a single toast at most.
|
|
425
426
|
|
|
426
427
|
<!-- internal:end -->
|
|
427
428
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useLatestRequest
|
|
3
|
+
category: composables
|
|
4
|
+
group: utilities
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# useLatestRequest
|
|
8
|
+
|
|
9
|
+
Latest-wins sequencing for overlapping async work: lets a caller drop a response
|
|
10
|
+
that a newer request already superseded.
|
|
11
|
+
|
|
12
|
+
`useAsync` covers loading state, error state and error notifications, but it has
|
|
13
|
+
no notion of a superseded call — a slow earlier response can overwrite a newer
|
|
14
|
+
one. This composable fills that gap.
|
|
15
|
+
|
|
16
|
+
## When to Use
|
|
17
|
+
|
|
18
|
+
- A search field that fires a request per keystroke, where the slowest response must not win
|
|
19
|
+
- A master/detail pair, where clicking row A then row B must not leave A's details on screen
|
|
20
|
+
- Any load that can still be in flight when its blade closes
|
|
21
|
+
- When NOT to use: for a single request with no concurrent sibling — plain `useAsync` is enough
|
|
22
|
+
|
|
23
|
+
!!! note "Discards, does not cancel"
|
|
24
|
+
The superseded request still completes; its result is thrown away. The generated
|
|
25
|
+
API clients build their own `RequestInit` and accept no `AbortSignal`, so there
|
|
26
|
+
is nothing to cancel through. Aborting would additionally save the round trip and
|
|
27
|
+
belongs with a client that takes a signal.
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { useLatestRequest } from "@vc-shell/framework";
|
|
33
|
+
|
|
34
|
+
const search = useLatestRequest();
|
|
35
|
+
const items = ref([]);
|
|
36
|
+
|
|
37
|
+
async function load(criteria) {
|
|
38
|
+
const request = search.begin();
|
|
39
|
+
try {
|
|
40
|
+
const result = await client.search(criteria);
|
|
41
|
+
if (!request.isCurrent()) return; // a newer search already won
|
|
42
|
+
items.value = result;
|
|
43
|
+
} finally {
|
|
44
|
+
request.complete();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Two rules make it correct:
|
|
50
|
+
|
|
51
|
+
1. Check `isCurrent()` **after** the await and before writing any state.
|
|
52
|
+
2. Call `complete()` in a `finally`, so a throwing request still releases `pending`.
|
|
53
|
+
|
|
54
|
+
## API Reference
|
|
55
|
+
|
|
56
|
+
### Returns
|
|
57
|
+
|
|
58
|
+
| Member | Type | Description |
|
|
59
|
+
| ------------ | ------------------------ | --------------------------------------------------------------------------------- |
|
|
60
|
+
| `begin` | `() => LatestRequest` | Starts a request and supersedes any earlier one |
|
|
61
|
+
| `invalidate` | `() => void` | Supersedes the in-flight request without starting a new one |
|
|
62
|
+
| `dispose` | `() => void` | Permanently supersedes everything. Runs automatically when the owning scope stops |
|
|
63
|
+
| `pending` | `Readonly<Ref<boolean>>` | `true` while the newest request is still running |
|
|
64
|
+
|
|
65
|
+
### `LatestRequest`
|
|
66
|
+
|
|
67
|
+
| Member | Type | Description |
|
|
68
|
+
| ----------- | --------------- | ---------------------------------------------------------------------------------- |
|
|
69
|
+
| `isCurrent` | `() => boolean` | `false` once a newer request started, or after `invalidate()` / `dispose()` |
|
|
70
|
+
| `complete` | `() => void` | Marks this request finished. Idempotent; only the current request clears `pending` |
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
### `pending` tracks the newest request only
|
|
75
|
+
|
|
76
|
+
A superseded request finishing does **not** clear `pending` — the newer one is
|
|
77
|
+
still running, and clearing there would hide the spinner while the screen is
|
|
78
|
+
still waiting for data.
|
|
79
|
+
|
|
80
|
+
### Automatic disposal
|
|
81
|
+
|
|
82
|
+
When called inside a component or effect scope, the tracker disposes itself when
|
|
83
|
+
that scope stops, so a response landing after its blade closed can never write
|
|
84
|
+
into a dead scope. Call `dispose()` by hand only outside a scope.
|
|
85
|
+
|
|
86
|
+
## Recipes
|
|
87
|
+
|
|
88
|
+
### Driving a spinner
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const details = useLatestRequest();
|
|
92
|
+
// `pending` is a ref, so watch it, render it, or hand it to useLoading.
|
|
93
|
+
const loading = details.pending;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Dropping a request on selection change
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
watch(selectedId, () => {
|
|
100
|
+
// Nothing new starts yet; whatever is in flight stops being current.
|
|
101
|
+
details.invalidate();
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Common Mistakes
|
|
106
|
+
|
|
107
|
+
**Wrong: checking before the await**
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const request = search.begin();
|
|
111
|
+
if (!request.isCurrent()) return; // always true here — nothing has superseded it yet
|
|
112
|
+
const result = await client.search(criteria);
|
|
113
|
+
items.value = result; // still overwrites newer data
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Right: checking after**
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const request = search.begin();
|
|
120
|
+
const result = await client.search(criteria);
|
|
121
|
+
if (!request.isCurrent()) return;
|
|
122
|
+
items.value = result;
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Wrong: completing only on success**
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const request = search.begin();
|
|
129
|
+
const result = await client.search(criteria); // throws → pending stays true forever
|
|
130
|
+
request.complete();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Right: completing in `finally`**
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const request = search.begin();
|
|
137
|
+
try {
|
|
138
|
+
const result = await client.search(criteria);
|
|
139
|
+
if (!request.isCurrent()) return;
|
|
140
|
+
items.value = result;
|
|
141
|
+
} finally {
|
|
142
|
+
request.complete();
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Related
|
|
147
|
+
|
|
148
|
+
- [useAsync](./useAsync.md) — loading state, error state and error notifications for a single call
|
|
149
|
+
- [useLoading](./useLoading.md) — aggregates several loading flags
|
|
@@ -69,13 +69,38 @@ async function deleteProduct(id: string) {
|
|
|
69
69
|
|
|
70
70
|
### Returns (`IUsePopup`)
|
|
71
71
|
|
|
72
|
-
| Method | Signature
|
|
73
|
-
| ------------------ |
|
|
74
|
-
| `open` | `() => void`
|
|
75
|
-
| `close` | `() => void`
|
|
76
|
-
| `showConfirmation` | `(message: string \| Ref<string
|
|
77
|
-
| `showError` | `(message: string \| Ref<string
|
|
78
|
-
| `showInfo` | `(message: string \| Ref<string
|
|
72
|
+
| Method | Signature | Description |
|
|
73
|
+
| ------------------ | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `open` | `() => void` | Push the popup onto the stack and render it |
|
|
75
|
+
| `close` | `() => void` | Remove the popup from the stack |
|
|
76
|
+
| `showConfirmation` | `(message: string \| Ref<string>, options?: PopupMessageOptions) => Promise<boolean>` | Warning dialog with Confirm/Cancel buttons. Resolves `true` on confirm, `false` on cancel or close. |
|
|
77
|
+
| `showError` | `(message: string \| Ref<string>, options?: PopupMessageOptions) => void` | Error-styled popup with a close button |
|
|
78
|
+
| `showInfo` | `(message: string \| Ref<string>, options?: PopupMessageOptions) => void` | Info-styled popup with a close button |
|
|
79
|
+
|
|
80
|
+
### `PopupMessageOptions`
|
|
81
|
+
|
|
82
|
+
| Option | Type | Default | Description |
|
|
83
|
+
| ------ | --------- | ------- | ------------------------------------------------- |
|
|
84
|
+
| `html` | `boolean` | `false` | Render the message as HTML instead of plain text. |
|
|
85
|
+
|
|
86
|
+
Messages are rendered as **text** by default. These dialogs are usually built by
|
|
87
|
+
interpolating server data into a translation, and as markup an entity name can
|
|
88
|
+
restyle the dialog or add a working link to one the user is meant to trust:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// A product literally named "<h1>Clearance</h1>" changes the dialog's layout
|
|
92
|
+
// when rendered as HTML. As text it simply reads back the name.
|
|
93
|
+
await showConfirmation(t("PRODUCTS.ALERTS.DELETE", { name: product.name }));
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Opt in only for markup you author yourself, never for interpolated data:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
showError(t("TEAM.ERRORS.USER_EXIST", { email }), { html: true });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A slot passed through `usePopup({ slots: { default: "<p>…</p>" } })` is
|
|
103
|
+
unaffected — explicit string slots are still rendered as sanitized HTML.
|
|
79
104
|
|
|
80
105
|
## How It Works
|
|
81
106
|
|
|
@@ -121,6 +121,26 @@ The composable returns `void`. It wires the watcher and the unmount cleanup; not
|
|
|
121
121
|
- `NAVIGATE_TO_APP` -- Open a specific blade (driven by markdown action links in assistant messages)
|
|
122
122
|
- `EXPAND_IN_CHAT` -- Expand an item inline in the chat (markdown action link)
|
|
123
123
|
- `SHOW_MORE` -- Request the next page of a result category (markdown action link)
|
|
124
|
+
- `CLOSE_PANEL` -- Close the panel. No payload.
|
|
125
|
+
|
|
126
|
+
### Closing the panel from the keyboard -- the chatbot has to help
|
|
127
|
+
|
|
128
|
+
The panel closes on `Escape` and toggles on `Ctrl/Cmd+I`, but those handlers listen on the **host** document. A keystroke is delivered to the document that owns the focused element, so once the chatbot takes focus -- most chat UIs autofocus their input on load -- the host never sees it. Nothing the shell can do changes that: the panel is a cross-origin iframe, so its key events are not observable from here.
|
|
129
|
+
|
|
130
|
+
**An embedded chatbot must therefore relay its own dismiss keys:**
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
window.addEventListener("keydown", (event) => {
|
|
134
|
+
if (event.key === "Escape" || ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "i")) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
window.parent.postMessage({ type: "CLOSE_PANEL" }, "https://your-shell-origin.example.com");
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Without that relay the only keyboard route out is `Shift+Tab` from the first focusable element in the chat, which lands on the panel header's Close button. That is a route, not a usable one -- relay the keys.
|
|
142
|
+
|
|
143
|
+
Closing a panel exposes nothing, and the sender's origin is validated against `allowedOrigins` before the message is acted on, so this carries no additional risk beyond the messages already accepted.
|
|
124
144
|
|
|
125
145
|
## Usage
|
|
126
146
|
|
package/runtime/knowledge/docs/shell/dashboard/draggable-dashboard/draggable-dashboard.docs.md
CHANGED
|
@@ -125,7 +125,7 @@ function resetLayout() {
|
|
|
125
125
|
## Details
|
|
126
126
|
|
|
127
127
|
- **Grid system**: Uses Gridstack.js under the hood, which provides a 12-column responsive grid. Widget `size.width` is in grid columns (1-12), `size.height` is in grid rows.
|
|
128
|
-
- **Layout persistence**: When the user rearranges or resizes widgets, the new layout is automatically saved to localStorage. On next visit, the persisted layout is restored.
|
|
128
|
+
- **Layout persistence**: When the user rearranges or resizes widgets, the new layout — position **and** size — is automatically saved to localStorage. On next visit, the persisted layout is restored. A widget's declared `size` is only the default: once the grid reports a live size, that is what is stored and restored.
|
|
129
129
|
- **Widget registration**: Widgets must be registered via `useDashboard().registerWidget()` before the dashboard mounts. The component reads the widget registry and creates grid items for each.
|
|
130
130
|
- **markRaw requirement**: Widget components must be wrapped in `markRaw()` when registering to prevent Vue from making them reactive (which would cause performance issues with the grid system).
|
|
131
131
|
- **Responsive behavior**: On mobile viewports, widgets stack vertically in a single column. Drag-and-drop is disabled on touch devices for better usability.
|
|
@@ -144,16 +144,18 @@ function resetLayout() {
|
|
|
144
144
|
|
|
145
145
|
Widgets can be rearranged without a pointer, which WCAG 2.5.7 Dragging Movements requires:
|
|
146
146
|
|
|
147
|
-
| Key | Action
|
|
148
|
-
| ------------------- |
|
|
149
|
-
| `Tab` | Move focus between widgets (each one is in the tab order)
|
|
150
|
-
| `Enter` / `Space` | Pick the focused widget up, and drop it again
|
|
151
|
-
| Arrow keys | While picked up, move the widget one grid cell
|
|
152
|
-
| `Shift` + arrow key | While picked up, resize by one cell (needs `resizable`)
|
|
153
|
-
| `Escape` | Cancel the move and
|
|
147
|
+
| Key | Action |
|
|
148
|
+
| ------------------- | ------------------------------------------------------------------------- |
|
|
149
|
+
| `Tab` | Move focus between widgets (each one is in the tab order) |
|
|
150
|
+
| `Enter` / `Space` | Pick the focused widget up, and drop it again |
|
|
151
|
+
| Arrow keys | While picked up, move the widget one grid cell |
|
|
152
|
+
| `Shift` + arrow key | While picked up, resize by one cell (needs `resizable`) |
|
|
153
|
+
| `Escape` | Cancel the move and restore the position **and size** it was picked up at |
|
|
154
154
|
|
|
155
155
|
Every step is announced through the component's `aria-live` region, and the picked-up widget is outlined so the state is visible to sighted keyboard users. Moves are clamped at the grid edges and at the 2×2 minimum widget size, and the layout is persisted when the widget is dropped — the same as after a mouse drag.
|
|
156
156
|
|
|
157
|
+
Announcements report where the widget actually ended up rather than the cell it was asked to move to: Gridstack compacts rows, so the two can differ. Repeated resizes accumulate (6 → 7 → 8 cells), and a keypress the minimum span refuses is not announced as a change.
|
|
158
|
+
|
|
157
159
|
!!! note "The widget itself is the control"
|
|
158
160
|
There is no separate "move" button. Gridstack only implements pointer dragging, so the widget is focusable and handles the keys directly. If you render your own interactive elements inside a widget, they keep working — the arrow keys only act while the widget has been explicitly picked up.
|
|
159
161
|
|
|
@@ -264,7 +264,7 @@ A host persisting events must apply these the same way the master/override model
|
|
|
264
264
|
|
|
265
265
|
## Overlapping Events and Overflow
|
|
266
266
|
|
|
267
|
-
All-day events that overlap in time on the same days are packed into separate stacked lanes automatically -- no configuration needed. When a day would need more lanes than fit, the extra events collapse into a "+N more"
|
|
267
|
+
All-day events that overlap in time on the same days are packed into separate stacked lanes automatically -- no configuration needed. When a day would need more lanes than fit, the extra events collapse into a "+N more" button; activating it -- by click or by keyboard -- opens a popover listing every all-day event on that date.
|
|
268
268
|
|
|
269
269
|
## Custom Slots
|
|
270
270
|
|
|
@@ -348,12 +348,13 @@ See the [Timeline recipe](#timeline-business-hours-review) below for a full exam
|
|
|
348
348
|
|
|
349
349
|
## CSS Custom Properties
|
|
350
350
|
|
|
351
|
-
| Property
|
|
352
|
-
|
|
|
353
|
-
| `--scheduler-border-color`
|
|
354
|
-
| `--
|
|
355
|
-
| `--
|
|
356
|
-
| `--z-
|
|
351
|
+
| Property | Default | Description |
|
|
352
|
+
| --------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
353
|
+
| `--scheduler-border-color` | `var(--neutrals-200)` | Border color for the grid, header, and row separators. |
|
|
354
|
+
| `--scheduler-surface-color` | `var(--additional-50)` | Surface the whole component paints, toolbar and weekday header included. It owns its background because it draws its own border and radius -- do not rely on the host's backdrop showing through. |
|
|
355
|
+
| `--vc-scheduler-event-ink` | `#fff` | Text color on Month-view event bars/chips. |
|
|
356
|
+
| `--z-critical-popup` | (theme z-index scale) | Stacking context for the "+N more" overflow popover. |
|
|
357
|
+
| `--z-local-sticky` | (theme z-index scale) | Timeline view. Stacking context for the sticky two-tier header. |
|
|
357
358
|
|
|
358
359
|
Event fill color comes from `color` (any CSS color or `var(...)` reference) and defaults to `var(--primary-500)`. Event label text is white by default -- see [Common Mistakes](#common-mistakes).
|
|
359
360
|
|
|
@@ -495,10 +496,11 @@ const events = [{ id: "a", title: "Promo", start, end, allDay: true, color: "#a2
|
|
|
495
496
|
## Accessibility
|
|
496
497
|
|
|
497
498
|
- The Month grid uses `role="grid"` / `role="row"` / `role="gridcell"` / `role="columnheader"` for the weekday header and day cells, each `gridcell` carrying a full formatted-date `aria-label`.
|
|
498
|
-
- Each event bar and timed chip is a focusable `role="button"` element (`tabindex="0"`) with an `aria-label` built from its title and formatted start/end dates -- activate with click or `Enter`.
|
|
499
|
-
- The "+N more" overflow
|
|
500
|
-
- The
|
|
501
|
-
- The This event/All events scope dialog
|
|
499
|
+
- Each event bar and timed chip is a focusable `role="button"` element (`tabindex="0"`) with an `aria-label` built from its title and formatted start/end dates -- activate with click or `Enter`. For an all-day event the announced end is the **last inclusive day**, not the exclusive `end` you pass: an event with `end` at midnight runs through the previous day, so a one-day event is announced as a single date rather than a two-day range. Timed events are announced with their real times.
|
|
500
|
+
- The "+N more" overflow control is a `<button>` whose visible text is its accessible name; the date it belongs to comes from the enclosing `gridcell`. Activating it moves focus to the first event in the popover, and closing the popover (Escape, or the close button) returns focus to the "+N more" button. Each event inside the popover is a `<button>` too, so a day's 4th and later events are reachable and operable by keyboard.
|
|
501
|
+
- The overflow popover's panel is a `VcPopover`, which supplies a titled header (the formatted date) and Escape-to-close. It does **not** apply `role="dialog"` or trap focus -- `VcPopover` has no focus management of its own, so any popover-based surface that needs focus moved into it does that itself, as the overflow popover above does.
|
|
502
|
+
- The editor modal and the This event/All events scope dialog are `VcPopup`, which is a Headless UI `Dialog` -- those do get accessible naming, a focus trap, focus restore and `inert` on the background from the library. The built-in quick-create popover is a `VcPopover` and focuses its title field on open.
|
|
503
|
+
- The recurrence editor's weekday toggle buttons expose `aria-pressed` (selected state) and an `aria-label` with the full weekday name, since their visible narrow labels ("S", "M", "T", ...) collide (Sun/Sat, Tue/Thu). The "↻" recurring-occurrence marker is `aria-hidden` -- purely visual, redundant with the event's own accessible name.
|
|
502
504
|
- The toolbar's prev/next buttons carry an explicit `aria-label` ("Previous" / "Next") since they are icon-only; the today/view-switch buttons show text labels.
|
|
503
505
|
- Event move/resize (Month view's `editable`) is a pointer-drag interaction with no keyboard equivalent yet; use a form outside VcScheduler for keyboard-only date edits. Timeline view is read-only (click still opens the quick-info popover).
|
|
504
506
|
- Respects `prefers-reduced-motion` -- event/bar transitions and chip hover transitions are disabled.
|
|
@@ -50,16 +50,17 @@ const pagination = useDataTablePagination({
|
|
|
50
50
|
|
|
51
51
|
### Returns (`reactive()` object)
|
|
52
52
|
|
|
53
|
-
| Property
|
|
54
|
-
|
|
|
55
|
-
| `currentPage`
|
|
56
|
-
| `pages`
|
|
57
|
-
| `skip`
|
|
58
|
-
| `pageSize`
|
|
59
|
-
| `totalCount`
|
|
60
|
-
| `goToPage`
|
|
61
|
-
| `setPage`
|
|
62
|
-
| `reset`
|
|
53
|
+
| Property | Type | Description |
|
|
54
|
+
| -------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
|
55
|
+
| `currentPage` | `number` | Current 1-based page number. Reading is fully supported; **assigning is deprecated** — see below |
|
|
56
|
+
| `pages` | `number` (readonly) | Total number of pages |
|
|
57
|
+
| `skip` | `number` (readonly) | Current skip offset for API calls |
|
|
58
|
+
| `pageSize` | `number` (readonly) | Resolved page size |
|
|
59
|
+
| `totalCount` | `number` (readonly) | Resolved total item count |
|
|
60
|
+
| `goToPage` | `(page: number) => void` | Navigate to page; fires `onPageChange` |
|
|
61
|
+
| `setPage` | `(page: number) => void` | Set the page without firing `onPageChange` (used to seed from a URL restore) |
|
|
62
|
+
| `reset` | `() => void` | Reset to page 1; does NOT fire `onPageChange` |
|
|
63
|
+
| `restoredPage` | `number \| undefined` (readonly, optional) | The page seeded from the blade URL at setup, or `undefined` if none. See the warning below |
|
|
63
64
|
|
|
64
65
|
All properties are auto-unwrapped by `reactive()` — no `.value` access needed in script or template.
|
|
65
66
|
|
|
@@ -166,6 +167,10 @@ Blade then simply binds:
|
|
|
166
167
|
- **Pure without callback**: Omit `onPageChange` and the composable works as pure state -- useful for unit tests or when the consumer prefers to watch properties reactively.
|
|
167
168
|
- **Why `reactive()` and not `ref()`**: Pagination is a cohesive group of properties always used together (`pagination.xxx`). `reactive()` is the Vue-idiomatic choice for such objects. `useDataTableSort` returns `ref()`s because its properties are destructured and used with `v-model` individually.
|
|
168
169
|
- **URL state (stateKey)**: When `stateKey` is provided, the composable reads the current page from the blade URL query on creation (via `setPage`, which does not fire `onPageChange`) and persists it on every `goToPage` call. Without `stateKey`, behavior is unchanged.
|
|
170
|
+
- **With `stateKey`, the first load MUST start at `pagination.skip`** -- not at a hardcoded `skip: 0`. The restore deliberately does not fire `onPageChange` (that would load twice), so a load issued with default criteria fetches page 1 while the paginator already shows page N. There is no error and no warning, and clicking page N does nothing because it is already the active page. Use `restoredPage` if the code needs to branch on whether a restore happened, but passing `skip` unconditionally is correct either way.
|
|
171
|
+
- **Do not assign to `currentPage` (deprecated)**: there are three ways to change the page and they behave differently. `goToPage(page)` changes it and fires `onPageChange`. `setPage(page)` changes it silently, for seeding. Assigning `pagination.currentPage = page` is a silent `setPage` -- the URL slice is still written, but `onPageChange` never fires, so the paginator moves to page N while the table keeps page 1's rows. It now logs a deprecation warning. It is deliberately **not** readonly: a production Vue build drops a write to a readonly reactive property silently, so making it readonly would leave existing consumers unable to change the page with no error at all. That tightening belongs in a major version.
|
|
172
|
+
- **`restoredPage` is optional in the interface**: consumers build `UseDataTablePaginationReturn` by hand to re-expose a nested pagination (a facade over two views, for example). A required property would break every such facade at compile time; the composable itself always provides it.
|
|
173
|
+
- **A missing provider is now loud**: `stateKey` only works inside a blade, where `TableQueryState` is provided. Outside one -- and in unit tests -- the feature used to disappear silently, indistinguishable from "the URL had no page". It now logs a warning naming the key.
|
|
169
174
|
|
|
170
175
|
## Tips
|
|
171
176
|
|