@neovici/cosmoz-queue 1.6.1 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +509 -2
- package/dist/queue/use-queue.d.ts +19 -0
- package/dist/queue/use-queue.d.ts.map +1 -1
- package/dist/queue/use-queue.js +11 -0
- package/dist/util/fetch/fetch.d.ts +1 -1
- package/dist/util/{test/path.test.d.ts.map → path.test.d.ts.map} +1 -1
- package/dist/util/{test/path.test.js → path.test.js} +25 -30
- package/package.json +14 -11
- package/dist/queue/test/__snapshots__/render.test.snap.d.ts +0 -2
- package/dist/queue/test/__snapshots__/render.test.snap.d.ts.map +0 -1
- package/dist/queue/test/__snapshots__/render.test.snap.js +0 -64
- package/dist/queue/test/item-click.test.d.ts +0 -2
- package/dist/queue/test/item-click.test.d.ts.map +0 -1
- package/dist/queue/test/item-click.test.js +0 -28
- package/dist/queue/test/render.test.d.ts +0 -2
- package/dist/queue/test/render.test.d.ts.map +0 -1
- package/dist/queue/test/render.test.js +0 -27
- package/dist/queue/test/use-pref.test.d.ts +0 -2
- package/dist/queue/test/use-pref.test.d.ts.map +0 -1
- package/dist/queue/test/use-pref.test.js +0 -16
- /package/dist/util/{test/path.test.d.ts → path.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,3 +1,510 @@
|
|
|
1
|
-
# cosmoz-queue
|
|
1
|
+
# @neovici/cosmoz-queue
|
|
2
2
|
|
|
3
|
-
A pionjs
|
|
3
|
+
A hooks-based UI component library for building **master-detail views** with list, split, and queue navigation modes. Built on [`@pionjs/pion`](https://github.com/pionjs/pion) and [`lit-html`](https://lit.dev/docs/libraries/standalone-templates/).
|
|
4
|
+
|
|
5
|
+
> **What is a "queue"?** Not a data structure -- a UI pattern. Users work through a list of items one-by-one, like processing a queue of tasks. The component provides three view modes: a data table, a side-by-side split, and a full-screen sequential navigator.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @neovici/cosmoz-queue
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dependencies: `@pionjs/pion`, `lit-html`, `i18next`.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
The `queue()` factory is the recommended high-level API. It composes all the internal hooks and renders the complete queue UI -- tabs, navigation, list, and detail view.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { queue } from '@neovici/cosmoz-queue';
|
|
21
|
+
import { spread } from '@open-wc/lit-helpers';
|
|
22
|
+
import { component, useCallback } from '@pionjs/pion';
|
|
23
|
+
import { html } from 'lit-html';
|
|
24
|
+
import { ref } from 'lit-html/directives/ref.js';
|
|
25
|
+
import { fetchOrderDetails$ } from './api';
|
|
26
|
+
import type { OrderListItem } from './types';
|
|
27
|
+
|
|
28
|
+
const OrderListQueue = ({ heading }: { heading: string }) =>
|
|
29
|
+
queue<OrderListItem>({
|
|
30
|
+
// Title displayed above the list
|
|
31
|
+
heading,
|
|
32
|
+
|
|
33
|
+
// Unique key for persisting table column settings
|
|
34
|
+
settingsId: 'order-list',
|
|
35
|
+
|
|
36
|
+
// URL hash parameter names (enables deep-linking and back/forward)
|
|
37
|
+
idHashParam: 'id',
|
|
38
|
+
tabHashParam: 'qtab',
|
|
39
|
+
|
|
40
|
+
// Async function that fetches full details for a list item.
|
|
41
|
+
// Results are memoized per item identity.
|
|
42
|
+
details: useCallback(
|
|
43
|
+
(item: OrderListItem) => fetchOrderDetails$(item.id),
|
|
44
|
+
[],
|
|
45
|
+
),
|
|
46
|
+
|
|
47
|
+
// Render the list component. `thru` contains bindings that wire
|
|
48
|
+
// the omnitable events to queue state (items, selection, clicks).
|
|
49
|
+
// Spread them onto your list component.
|
|
50
|
+
list: (thru, { onRef }) =>
|
|
51
|
+
html`<order-list-core ${spread(thru)} ${ref(onRef)}></order-list-core>`,
|
|
52
|
+
|
|
53
|
+
// Render the detail view. `nav` contains prev/next buttons --
|
|
54
|
+
// place them in a slot or wherever navigation belongs in your view.
|
|
55
|
+
view: (thru, { nav }) =>
|
|
56
|
+
html`<order-view-core ${spread(thru)}>${nav}</order-view-core>`,
|
|
57
|
+
|
|
58
|
+
// Render a loading skeleton shown while `details` resolves.
|
|
59
|
+
loader: (thru, { nav }) =>
|
|
60
|
+
html`<order-view-skeleton ${spread(thru)}>${nav}</order-view-skeleton>`,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
customElements.define('order-list-queue', component(OrderListQueue));
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `queue()` props
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Default | Description |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| `heading` | `string` | -- | Title displayed above the tabs |
|
|
71
|
+
| `settingsId` | `string?` | -- | Key for persisting omnitable column settings |
|
|
72
|
+
| `idHashParam` | `string` | `'qid'` | URL hash param for the selected item ID |
|
|
73
|
+
| `tabHashParam` | `string` | `'qtab'` | URL hash param for the active tab |
|
|
74
|
+
| `details` | `(item: I) => PromiseLike<D>` | -- | Fetches full details for a list item |
|
|
75
|
+
| `api` | `(id: string, item: I) => string` | -- | Alternative to `details`: returns a URL, queue fetches JSON internally |
|
|
76
|
+
| `list` | `(thru, props) => TemplateResult` | -- | Renders the list component |
|
|
77
|
+
| `view` | `(thru, props) => TemplateResult` | -- | Renders the detail view |
|
|
78
|
+
| `loader` | `(thru, props) => TemplateResult` | -- | Renders the loading skeleton |
|
|
79
|
+
| `pagination` | `Pagination?` | -- | Server-side pagination config |
|
|
80
|
+
| `fallback` | `string?` | -- | Default tab when none is set in URL hash |
|
|
81
|
+
| `split` | `SplitOpts?` | -- | Split.js configuration (sizes, min sizes) |
|
|
82
|
+
| `afterHeading` | `unknown?` | -- | Extra content rendered after the heading |
|
|
83
|
+
|
|
84
|
+
Use **`details`** when you control the fetch (most common). Use **`api`** when you want the queue to handle fetching via URL.
|
|
85
|
+
|
|
86
|
+
### The `thru` bindings
|
|
87
|
+
|
|
88
|
+
The `list`, `view`, and `loader` render functions receive a `thru` object as their first argument. This object contains property and event bindings that wire the child component to queue state:
|
|
89
|
+
|
|
90
|
+
**For `list`:**
|
|
91
|
+
- `.settingsId`, `.exposedParts` -- table configuration
|
|
92
|
+
- `@visible-items-changed`, `@selected-items-changed`, `@total-available-changed` -- sync items/selection back to queue
|
|
93
|
+
- `@omnitable-item-click` -- item click handler
|
|
94
|
+
- `@async-simple-action` -- action completion handler
|
|
95
|
+
|
|
96
|
+
**For `view`:**
|
|
97
|
+
- `.item` -- the current detail item (list item or resolved detail)
|
|
98
|
+
- `.hideActions` -- whether to hide actions (true when items are selected in split mode)
|
|
99
|
+
- `@async-simple-action` -- action completion handler
|
|
100
|
+
|
|
101
|
+
Spread these onto your component with `${spread(thru)}` from `@open-wc/lit-helpers`.
|
|
102
|
+
|
|
103
|
+
## View modes
|
|
104
|
+
|
|
105
|
+
The queue provides three tab-based view modes:
|
|
106
|
+
|
|
107
|
+
| Mode | Tab name | Behavior |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| **List** | `overview` | Full-width data table (`cosmoz-omnitable`). The default view. |
|
|
110
|
+
| **Split** | `split` | Side-by-side: resizable list on the left, detail view on the right. Disabled on mobile. |
|
|
111
|
+
| **Queue** | `queue` | Full-screen detail view with prev/next navigation and keyboard arrow keys. |
|
|
112
|
+
|
|
113
|
+
The active tab is synced to the URL hash (e.g. `#qtab=split&id=abc-123`), enabling deep-linking and browser back/forward navigation.
|
|
114
|
+
|
|
115
|
+
On mobile viewports, the split tab is disabled and falls back to queue mode automatically.
|
|
116
|
+
|
|
117
|
+
## List module
|
|
118
|
+
|
|
119
|
+
The `@neovici/cosmoz-queue/list` entry point provides a managed `cosmoz-omnitable` with built-in state management, data fetching, pagination, and action rendering.
|
|
120
|
+
|
|
121
|
+
### `listCore()`
|
|
122
|
+
|
|
123
|
+
The high-level factory that combines `useListCore()` + `renderListCore()`:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { itemClick } from '@neovici/cosmoz-queue';
|
|
127
|
+
import { column, listCore, style } from '@neovici/cosmoz-queue/list';
|
|
128
|
+
import { component, html } from '@pionjs/pion';
|
|
129
|
+
import { t } from 'i18next';
|
|
130
|
+
import type { OrderListItem } from './types';
|
|
131
|
+
|
|
132
|
+
interface Props {
|
|
133
|
+
exposedParts: string;
|
|
134
|
+
api: (params: { pathLocator: string }) => Promise<{
|
|
135
|
+
items: OrderListItem[];
|
|
136
|
+
metaData: { totalAvailable: number };
|
|
137
|
+
}>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const OrderListCore = ({ exposedParts, api }: Props) => {
|
|
141
|
+
return listCore({
|
|
142
|
+
// Unique key for persisting column settings
|
|
143
|
+
settingsId: 'order-list-core',
|
|
144
|
+
|
|
145
|
+
// CSS parts to expose for external styling
|
|
146
|
+
exposedParts,
|
|
147
|
+
|
|
148
|
+
// When false, omnitable applies its own local filtering
|
|
149
|
+
noLocal: false,
|
|
150
|
+
|
|
151
|
+
// Column definitions. The tuple is [factory, deps] -- same pattern
|
|
152
|
+
// as useMemo. The factory returns an object where each key becomes
|
|
153
|
+
// a column name.
|
|
154
|
+
columns: [
|
|
155
|
+
() => ({
|
|
156
|
+
title: column({
|
|
157
|
+
render: ({ name }) =>
|
|
158
|
+
html`<cosmoz-omnitable-column
|
|
159
|
+
name="${name}"
|
|
160
|
+
title=${t('Title')}
|
|
161
|
+
flex="2"
|
|
162
|
+
.renderCell=${(
|
|
163
|
+
_col: unknown,
|
|
164
|
+
{ item, index }: { item: OrderListItem; index: number },
|
|
165
|
+
) =>
|
|
166
|
+
html`<a
|
|
167
|
+
@click="${itemClick({
|
|
168
|
+
index,
|
|
169
|
+
activate: ['split', 'queue'],
|
|
170
|
+
})}"
|
|
171
|
+
>${item.title}</a
|
|
172
|
+
>`}
|
|
173
|
+
></cosmoz-omnitable-column>`,
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
status: column({
|
|
177
|
+
render: ({ name }) =>
|
|
178
|
+
html`<cosmoz-omnitable-column-autocomplete
|
|
179
|
+
name="${name}"
|
|
180
|
+
title=${t('Status')}
|
|
181
|
+
value-path="status.name"
|
|
182
|
+
></cosmoz-omnitable-column-autocomplete>`,
|
|
183
|
+
}),
|
|
184
|
+
|
|
185
|
+
createdAt: column({
|
|
186
|
+
render: ({ name }) =>
|
|
187
|
+
html`<cosmoz-omnitable-column-date
|
|
188
|
+
name="${name}"
|
|
189
|
+
title=${t('Date created')}
|
|
190
|
+
></cosmoz-omnitable-column-date>`,
|
|
191
|
+
}),
|
|
192
|
+
}),
|
|
193
|
+
[], // dependency array (empty = compute once)
|
|
194
|
+
],
|
|
195
|
+
|
|
196
|
+
// Reactive query parameters. Recomputed when deps change,
|
|
197
|
+
// which triggers a new data fetch.
|
|
198
|
+
params: [() => ({ pathLocator: '/some/path' }), []],
|
|
199
|
+
|
|
200
|
+
// Data fetcher. Receives { params, page, pageSize }.
|
|
201
|
+
// Must return { items, total }.
|
|
202
|
+
list$: [
|
|
203
|
+
({ params }) =>
|
|
204
|
+
api(params).then((r) => ({
|
|
205
|
+
items: r.items ?? [],
|
|
206
|
+
total: r.metaData?.totalAvailable ?? 0,
|
|
207
|
+
})),
|
|
208
|
+
[api],
|
|
209
|
+
],
|
|
210
|
+
|
|
211
|
+
// Batch actions shown when items are selected
|
|
212
|
+
actions: [myAction()],
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
customElements.define(
|
|
217
|
+
'order-list-core',
|
|
218
|
+
component(OrderListCore, { styleSheets: [style] }),
|
|
219
|
+
);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `listCore()` props
|
|
223
|
+
|
|
224
|
+
| Prop | Type | Description |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `settingsId` | `string` | Persistence key for column settings |
|
|
227
|
+
| `exposedParts` | `string?` | CSS `exportparts` value |
|
|
228
|
+
| `noLocal` | `boolean` | Skip omnitable local filtering (default: `true`) |
|
|
229
|
+
| `columns` | `[() => Columns, deps[]]` | Column definitions (memoized) |
|
|
230
|
+
| `params` | `[(opts) => Params, deps[]]` | Query parameters (memoized). The `opts` include `{ filters, descending, sortOn, columns }` |
|
|
231
|
+
| `list$` | `[(props) => Promise<{ items, total }>, deps[]]` | Data fetcher. Receives `{ params, page, pageSize }` |
|
|
232
|
+
| `pageSize` | `number?` | Items per page for "load more" (default: `50`) |
|
|
233
|
+
| `actions` | `Action[]?` | Batch actions |
|
|
234
|
+
| `content` | `(opts) => Renderable` | Extra content inside the omnitable |
|
|
235
|
+
| `hashParam` | `string?` | URL hash param for omnitable state |
|
|
236
|
+
| `csvFilename` | `string?` | Filename for CSV export |
|
|
237
|
+
| `enabledColumns` | `string[]?` | Initially visible columns |
|
|
238
|
+
|
|
239
|
+
### `column()`
|
|
240
|
+
|
|
241
|
+
Type-safe column definition helper:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { column } from '@neovici/cosmoz-queue/list';
|
|
245
|
+
|
|
246
|
+
const myColumn = column({
|
|
247
|
+
// Optional ordering hint
|
|
248
|
+
order: 1,
|
|
249
|
+
|
|
250
|
+
// Optional sort key
|
|
251
|
+
sort: 'name',
|
|
252
|
+
|
|
253
|
+
// Optional filter transform: receives the raw filter value,
|
|
254
|
+
// returns the value sent to the API
|
|
255
|
+
filter: (value: string) => ({ name: value }),
|
|
256
|
+
|
|
257
|
+
// Render function -- receives { name } where name is the object key
|
|
258
|
+
render: ({ name }) =>
|
|
259
|
+
html`<cosmoz-omnitable-column
|
|
260
|
+
name="${name}"
|
|
261
|
+
title="Name"
|
|
262
|
+
></cosmoz-omnitable-column>`,
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### `useListCore()` / `useListCoreState()`
|
|
267
|
+
|
|
268
|
+
For cases where you need more control over the list without `renderListCore()`:
|
|
269
|
+
|
|
270
|
+
- **`useListCore(props)`** -- manages columns, params, data fetching, pagination, and form dialog state. Returns `{ data$, columns, loadMore, dialog, open, ...state }`.
|
|
271
|
+
- **`useListCoreState(defaults?)`** -- lower-level state: `filters`, `sortOn`, `descending`, `groupOn`, `selectedItems`, plus their setters and `setTotalAvailable`.
|
|
272
|
+
|
|
273
|
+
## Actions module
|
|
274
|
+
|
|
275
|
+
The `@neovici/cosmoz-queue/actions` entry point provides a declarative system for defining user actions that open form dialogs.
|
|
276
|
+
|
|
277
|
+
### Defining an action
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { action, Action, defaultButton } from '@neovici/cosmoz-queue/actions';
|
|
281
|
+
import { t } from 'i18next';
|
|
282
|
+
import { when } from 'lit-html/directives/when.js';
|
|
283
|
+
|
|
284
|
+
// action() is an identity function that provides type inference
|
|
285
|
+
export const approveOrder = action<OrderItem, ApproveFields>({
|
|
286
|
+
// Label shown on the button
|
|
287
|
+
title: () => t('Approve'),
|
|
288
|
+
|
|
289
|
+
// Optional: filter which items this action applies to.
|
|
290
|
+
// If provided, non-applicable items are excluded and the button
|
|
291
|
+
// shows a count badge like "Approve (3/5)".
|
|
292
|
+
applicable: (item) => item.status === 'pending',
|
|
293
|
+
|
|
294
|
+
// Optional: custom button renderer. Defaults to `defaultButton()`.
|
|
295
|
+
// Return `nothing` to hide the button conditionally.
|
|
296
|
+
button: (opts) =>
|
|
297
|
+
when(opts.items.length >= 1, () => defaultButton(opts)),
|
|
298
|
+
|
|
299
|
+
// Dialog configuration. Opens a `cosmoz-form` dialog.
|
|
300
|
+
// `items` contains only the applicable items.
|
|
301
|
+
dialog: ({ items, title }) => ({
|
|
302
|
+
heading: title,
|
|
303
|
+
description: t('Approve the selected orders'),
|
|
304
|
+
fields: [
|
|
305
|
+
// cosmoz-form field definitions
|
|
306
|
+
],
|
|
307
|
+
initial: {},
|
|
308
|
+
onSave: async (values) => {
|
|
309
|
+
await approveOrders(items.map((i) => i.id), values);
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Rendering actions
|
|
316
|
+
|
|
317
|
+
Actions are rendered automatically by `listCore()` when passed as the `actions` prop. For manual rendering (e.g. in a detail view's bottom bar):
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import { renderActions } from '@neovici/cosmoz-queue/actions';
|
|
321
|
+
import { approveOrder } from './actions';
|
|
322
|
+
|
|
323
|
+
// In a view component:
|
|
324
|
+
const bottomBar = renderActions({ items: [currentItem], open })([
|
|
325
|
+
approveOrder,
|
|
326
|
+
]);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Action types
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
interface Action<TItem, TDialog> {
|
|
333
|
+
title: () => string;
|
|
334
|
+
applicable?: (item: TItem) => boolean;
|
|
335
|
+
button?: (opts: Action & ActionOpts) => unknown;
|
|
336
|
+
dialog: (opts: DialogOpts) => Dialogable | Promise<Dialogable>;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
interface ActionOpts<TItem> {
|
|
340
|
+
items: TItem[];
|
|
341
|
+
open: (dialog: Dialogable) => void;
|
|
342
|
+
slot?: string;
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Advanced: composing with hooks
|
|
347
|
+
|
|
348
|
+
When `queue()` is too opinionated, use the individual hooks directly.
|
|
349
|
+
|
|
350
|
+
### `useQueue()` + `renderQueue()`
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { useQueue, renderQueue, renderNav } from '@neovici/cosmoz-queue';
|
|
354
|
+
|
|
355
|
+
const MyQueue = ({ heading }: { heading: string }) => {
|
|
356
|
+
const {
|
|
357
|
+
index, mobile, tabnav, items, setItems,
|
|
358
|
+
setSelected, setTotalAvailable, totalAvailable,
|
|
359
|
+
onItemClick, nav,
|
|
360
|
+
} = useQueue<MyItem>({
|
|
361
|
+
idHashParam: 'id',
|
|
362
|
+
tabHashParam: 'tab',
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
return renderQueue({
|
|
366
|
+
heading,
|
|
367
|
+
mobile,
|
|
368
|
+
index,
|
|
369
|
+
items,
|
|
370
|
+
tabnav,
|
|
371
|
+
totalAvailable,
|
|
372
|
+
nav,
|
|
373
|
+
list: html`<my-list
|
|
374
|
+
@visible-items-changed=${updateWith(setItems)}
|
|
375
|
+
@selected-items-changed=${updateWith(setSelected)}
|
|
376
|
+
@total-available-changed=${updateWith(setTotalAvailable)}
|
|
377
|
+
@omnitable-item-click=${onItemClick}
|
|
378
|
+
></my-list>`,
|
|
379
|
+
renderItem: ({ item, nav }) =>
|
|
380
|
+
html`<my-view .item=${item}>${nav}</my-view>`,
|
|
381
|
+
renderLoader: ({ item, nav }) =>
|
|
382
|
+
html`<my-skeleton .item=${item}>${nav}</my-skeleton>`,
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Individual hooks
|
|
388
|
+
|
|
389
|
+
| Hook | Import | Purpose |
|
|
390
|
+
|---|---|---|
|
|
391
|
+
| `useTabs({ items, hashParam, mobile, fallback })` | `@neovici/cosmoz-queue` | Manages overview/split/queue tabs. Returns `{ tabnav, activeTab }`. |
|
|
392
|
+
| `useDataNav(items, opts)` | `@neovici/cosmoz-queue` | Item navigation with prev/next, URL hash sync. Returns `{ item, setItem, next, prev, forward, index }`. |
|
|
393
|
+
| `useSplit({ activeTab, ...splitOpts })` | `@neovici/cosmoz-queue` | Initializes Split.js when in split mode. |
|
|
394
|
+
| `useListState()` | `@neovici/cosmoz-queue` | Creates `items`, `selected`, `totalAvailable` state with setters. |
|
|
395
|
+
| `useListSSE({ entity, params, list$ })` | `@neovici/cosmoz-queue` | Subscribes to Server-Sent Events (`cosmoz-${entity}-updated`) for real-time list updates. |
|
|
396
|
+
| `useFetchActions({ pathLocator, selected, api })` | `@neovici/cosmoz-queue` | Fetches available actions for selected items from an API. Returns `{ actions, actionRows, actionsFetching }`. |
|
|
397
|
+
| `useAsyncAction(nav)` | `@neovici/cosmoz-queue` | Handles async action completion with automatic item removal from the list. Returns `{ listRef, onAsyncSimpleAction }`. |
|
|
398
|
+
| `usePagination()` | `@neovici/cosmoz-queue` | URL hash-based page state. Returns `{ page, onPage }`. |
|
|
399
|
+
|
|
400
|
+
## Utilities
|
|
401
|
+
|
|
402
|
+
### Fetch helpers (`@neovici/cosmoz-queue/util/fetch`)
|
|
403
|
+
|
|
404
|
+
Pre-configured `fetch` wrapper with CORS and credentials:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
import {
|
|
408
|
+
fetch,
|
|
409
|
+
setBaseInit,
|
|
410
|
+
handleJSON,
|
|
411
|
+
RequestError,
|
|
412
|
+
} from '@neovici/cosmoz-queue/util/fetch';
|
|
413
|
+
|
|
414
|
+
// Configure default headers (call once at app startup)
|
|
415
|
+
setBaseInit({
|
|
416
|
+
headers: { 'X-Custom-Header': 'value' },
|
|
417
|
+
// Or use dynamic headers:
|
|
418
|
+
getHeaders: () => ({ Authorization: `Bearer ${getToken()}` }),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// fetch() includes mode: 'cors', credentials: 'include' by default
|
|
422
|
+
const response = await fetch('/api/orders');
|
|
423
|
+
const data = await handleJSON(response);
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`RequestError` extends `Error` with `.response` and `.data` properties for structured error handling.
|
|
427
|
+
|
|
428
|
+
### `itemClick()`
|
|
429
|
+
|
|
430
|
+
Makes list cells clickable, dispatching `omnitable-item-click` events that the queue listens for:
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
import { itemClick } from '@neovici/cosmoz-queue';
|
|
434
|
+
|
|
435
|
+
// In an omnitable cell renderer:
|
|
436
|
+
html`<a @click="${itemClick({ index, activate: ['split', 'queue'] })}">
|
|
437
|
+
${item.title}
|
|
438
|
+
</a>`;
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
The `activate` option specifies which tab to switch to. The queue picks the first non-disabled tab from the array.
|
|
442
|
+
|
|
443
|
+
### Other utilities
|
|
444
|
+
|
|
445
|
+
| Function | Description |
|
|
446
|
+
|---|---|
|
|
447
|
+
| `getItems(items, selected)` | Returns `selected` if non-empty, otherwise `items` |
|
|
448
|
+
| `touch(list, id)` | Forces an omnitable item refresh by replacing it with a shallow copy |
|
|
449
|
+
|
|
450
|
+
## Architecture
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
queue() factory
|
|
454
|
+
|
|
|
455
|
+
+---> useQueue() State orchestration
|
|
456
|
+
| |
|
|
457
|
+
| +---> useListState() items, selected, totalAvailable
|
|
458
|
+
| +---> useTabs() overview | split | queue
|
|
459
|
+
| +---> useDataNav() current item, prev/next, URL hash
|
|
460
|
+
| +---> useKeyNav() arrow key navigation
|
|
461
|
+
| +---> useSplit() Split.js initialization
|
|
462
|
+
| +---> useUpdates() list-item-remove events
|
|
463
|
+
|
|
|
464
|
+
+---> useAsyncAction() Post-action item removal
|
|
465
|
+
|
|
|
466
|
+
+---> renderQueue() Template composition
|
|
467
|
+
|
|
|
468
|
+
+---> cosmoz-tabs-next Tab bar (List / Split / Queue)
|
|
469
|
+
+---> renderStats() "3-5 of 120"
|
|
470
|
+
+---> renderPagination() Page prev/next
|
|
471
|
+
+---> <div.split>
|
|
472
|
+
+---> list cosmoz-omnitable (user-provided)
|
|
473
|
+
+---> cosmoz-slider Animated detail view
|
|
474
|
+
+---> renderSlide() --> renderView()
|
|
475
|
+
|
|
|
476
|
+
+---> details() Async fetch
|
|
477
|
+
+---> renderItem or renderLoader
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## Entry points
|
|
481
|
+
|
|
482
|
+
| Import path | Description |
|
|
483
|
+
|---|---|
|
|
484
|
+
| `@neovici/cosmoz-queue` | Main: `queue()`, `useQueue()`, `renderQueue()`, navigation hooks, SSE, utilities |
|
|
485
|
+
| `@neovici/cosmoz-queue/actions` | `action()`, `renderActions()`, `defaultButton()`, `actionCount()` |
|
|
486
|
+
| `@neovici/cosmoz-queue/list` | `listCore()`, `column()`, `useListCore()`, `useListCoreState()`, `renderListCore()` |
|
|
487
|
+
| `@neovici/cosmoz-queue/list/more` | `useMore()` -- progressive "load more" pagination |
|
|
488
|
+
| `@neovici/cosmoz-queue/list/more/render` | `renderLoadMore()` -- "Load more" button |
|
|
489
|
+
| `@neovici/cosmoz-queue/util/fetch` | `fetch()`, `setBaseInit()`, `handleJSON()`, `RequestError` |
|
|
490
|
+
|
|
491
|
+
## Deprecation notices
|
|
492
|
+
|
|
493
|
+
### `api` property → `details`
|
|
494
|
+
|
|
495
|
+
The `api` property on `useQueue()` / `queue()` is **deprecated** and will be
|
|
496
|
+
removed in v2.0.0. Use `details` instead:
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
// Before (deprecated)
|
|
500
|
+
queue({ api: (id, item) => apiUrl(`items/${id}`) })
|
|
501
|
+
|
|
502
|
+
// After
|
|
503
|
+
queue({ details: (item) => fetch(apiUrl(`items/${item.id}`)).then(r => r.json()) })
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
See [Migration guide](docs/migration-api-to-details.md) for more patterns.
|
|
507
|
+
|
|
508
|
+
## License
|
|
509
|
+
|
|
510
|
+
[Apache-2.0](LICENSE)
|
|
@@ -5,6 +5,25 @@ interface Opts<I> extends ReturnType<typeof useListState<I>>, Pick<UseTabsOption
|
|
|
5
5
|
tabHashParam?: string;
|
|
6
6
|
idHashParam?: string;
|
|
7
7
|
onActivate?: (name: string) => void;
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated Use the `details` property instead. The `api` property will be removed in v2.0.0.
|
|
10
|
+
*
|
|
11
|
+
* The `api` property uses deprecated utilities and is less flexible than `details`.
|
|
12
|
+
* With `details`, you have full control over the fetch operation and can return any Promise.
|
|
13
|
+
*
|
|
14
|
+
* Migration example:
|
|
15
|
+
* ```ts
|
|
16
|
+
* // Before:
|
|
17
|
+
* useQueue({
|
|
18
|
+
* api: (id, item) => apiUrl(`api/items/${id}`)
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* // After:
|
|
22
|
+
* useQueue({
|
|
23
|
+
* details: (item) => fetch(apiUrl(`api/items/${item.id}`)).then(res => res.json())
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
8
27
|
api?: (id: string, item: I) => string;
|
|
9
28
|
id?: (i: I) => string;
|
|
10
29
|
split?: SplitOpts;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-queue.d.ts","sourceRoot":"","sources":["../../src/queue/use-queue.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAiB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAClD,OAAgB,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"use-queue.d.ts","sourceRoot":"","sources":["../../src/queue/use-queue.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAiB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAClD,OAAgB,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,YAAY,CAAC;AAwDhE,UAAU,IAAI,CAAC,CAAC,CACf,SACC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,EAClC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;;;;;;;;;;;;;;;OAkBG;IACH,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IACtC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,QAAA,MAAM,QAAQ,GAAI,CAAC,EAAE,8EASlB,IAAI,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA2CH,KAAK;oBAtBA,MAAM;;;;;;;;;;;;;;;;;;CAkDjB,CAAC;AAEF,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,IAAI,CAC7B,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EACjC,MAAM,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,CACxC,CAAC;yBAEc,CAAC,EAAE,MAAM,QAAQ,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAnC7B,KAAK;oBAtBA,MAAM;;;;;;;;;;;;;;;;;;;AAyDlB,wBACgD"}
|
package/dist/queue/use-queue.js
CHANGED
|
@@ -12,6 +12,17 @@ import useUpdates from './use-updates';
|
|
|
12
12
|
import { getItems, normalizeHeaders } from './util';
|
|
13
13
|
const _id = (item) => item['id'];
|
|
14
14
|
const useQNav = ({ id, api, items, ...thru }) => {
|
|
15
|
+
// Deprecation warning
|
|
16
|
+
/* eslint-disable no-console, no-template-curly-in-string */
|
|
17
|
+
if (api && typeof console !== 'undefined' && console.warn) {
|
|
18
|
+
console.warn('[cosmoz-queue] DEPRECATED: The `api` property is deprecated and will be removed in v2.0.0. ' +
|
|
19
|
+
'Please migrate to the `details` property for better control and performance.\n\n' +
|
|
20
|
+
'Migration guide:\n' +
|
|
21
|
+
' Before: api: (id, item) => apiUrl(\'api/items/${id}\')\n' +
|
|
22
|
+
' After: details: (item) => fetch(apiUrl(\'api/items/${item.id}\')).then(res => res.json())\n\n' +
|
|
23
|
+
'See: https://github.com/Neovici/cosmoz-queue#migration-from-api-to-details');
|
|
24
|
+
}
|
|
25
|
+
/* eslint-enable no-console, no-template-curly-in-string */
|
|
15
26
|
const details = useMemo(() => api && memoize((item) => json(api(id(item), item))), [id, api]), pass = useDataNav(items, {
|
|
16
27
|
...thru,
|
|
17
28
|
id,
|
|
@@ -26,7 +26,7 @@ export declare const setBaseInit: <T extends BaseInitOptions>(init: T) => Partia
|
|
|
26
26
|
headers: {};
|
|
27
27
|
};
|
|
28
28
|
export declare const fetch: (url: string, opts?: RequestInit) => Promise<Response>;
|
|
29
|
-
export declare const handleJSON: (res: Response) => Promise<any
|
|
29
|
+
export declare const handleJSON: (res: Response) => "" | Promise<any>;
|
|
30
30
|
/**
|
|
31
31
|
* @deprecated
|
|
32
32
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path.test.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"path.test.d.ts","sourceRoot":"","sources":["../../src/util/path.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,84 +1,79 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { get, normalize, split } from '
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { get, normalize, split } from './path';
|
|
3
3
|
describe('path', () => {
|
|
4
4
|
describe('normalize', () => {
|
|
5
5
|
it('returns string path as-is', () => {
|
|
6
|
-
|
|
6
|
+
expect(normalize('foo.bar.0.baz')).toBe('foo.bar.0.baz');
|
|
7
7
|
});
|
|
8
8
|
it('returns empty string as-is', () => {
|
|
9
|
-
|
|
9
|
+
expect(normalize('')).toBe('');
|
|
10
10
|
});
|
|
11
11
|
it('converts array path to flattened string', () => {
|
|
12
|
-
|
|
12
|
+
expect(normalize(['foo.bar', 0, 'baz'])).toBe('foo.bar.0.baz');
|
|
13
13
|
});
|
|
14
14
|
it('handles array with single element', () => {
|
|
15
|
-
|
|
15
|
+
expect(normalize(['foo'])).toBe('foo');
|
|
16
16
|
});
|
|
17
17
|
it('handles array with dotted strings and numbers', () => {
|
|
18
|
-
|
|
18
|
+
expect(normalize(['a.b', 1, 'c.d', 2])).toBe('a.b.1.c.d.2');
|
|
19
19
|
});
|
|
20
20
|
it('handles empty array', () => {
|
|
21
|
-
|
|
21
|
+
expect(normalize([])).toBe('');
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
describe('split', () => {
|
|
25
25
|
it('splits string path into array', () => {
|
|
26
|
-
|
|
26
|
+
expect(split('foo.bar.0.baz')).toEqual(['foo', 'bar', '0', 'baz']);
|
|
27
27
|
});
|
|
28
28
|
it('splits array path into flat array', () => {
|
|
29
|
-
|
|
30
|
-
'foo',
|
|
31
|
-
'bar',
|
|
32
|
-
'0',
|
|
33
|
-
'baz',
|
|
34
|
-
]);
|
|
29
|
+
expect(split(['foo.bar', 0, 'baz'])).toEqual(['foo', 'bar', '0', 'baz']);
|
|
35
30
|
});
|
|
36
31
|
it('handles single segment string', () => {
|
|
37
|
-
|
|
32
|
+
expect(split('foo')).toEqual(['foo']);
|
|
38
33
|
});
|
|
39
34
|
it('handles array with single element', () => {
|
|
40
|
-
|
|
35
|
+
expect(split(['foo'])).toEqual(['foo']);
|
|
41
36
|
});
|
|
42
37
|
it('handles empty string', () => {
|
|
43
|
-
|
|
38
|
+
expect(split('')).toEqual(['']);
|
|
44
39
|
});
|
|
45
40
|
});
|
|
46
41
|
describe('get', () => {
|
|
47
42
|
it('retrieves nested value with string path', () => {
|
|
48
43
|
const obj = { foo: { bar: { baz: 'value' } } };
|
|
49
|
-
|
|
44
|
+
expect(get(obj, 'foo.bar.baz')).toBe('value');
|
|
50
45
|
});
|
|
51
46
|
it('retrieves nested value with array path', () => {
|
|
52
47
|
const obj = { foo: { bar: { baz: 'value' } } };
|
|
53
|
-
|
|
48
|
+
expect(get(obj, ['foo', 'bar', 'baz'])).toBe('value');
|
|
54
49
|
});
|
|
55
50
|
it('retrieves nested value with dotted string in array path', () => {
|
|
56
51
|
const obj = { foo: { bar: { baz: 'value' } } };
|
|
57
|
-
|
|
52
|
+
expect(get(obj, ['foo.bar', 'baz'])).toBe('value');
|
|
58
53
|
});
|
|
59
54
|
it('retrieves array element by index', () => {
|
|
60
55
|
const obj = { items: ['a', 'b', 'c'] };
|
|
61
|
-
|
|
56
|
+
expect(get(obj, 'items.1')).toBe('b');
|
|
62
57
|
});
|
|
63
58
|
it('retrieves array element with array path', () => {
|
|
64
59
|
const obj = { items: ['a', 'b', 'c'] };
|
|
65
|
-
|
|
60
|
+
expect(get(obj, ['items', 0])).toBe('a');
|
|
66
61
|
});
|
|
67
62
|
it('returns undefined for non-existent path', () => {
|
|
68
63
|
const obj = { foo: { bar: 'value' } };
|
|
69
|
-
|
|
64
|
+
expect(get(obj, 'foo.baz.qux')).toBeUndefined();
|
|
70
65
|
});
|
|
71
66
|
it('returns undefined when intermediate property is undefined', () => {
|
|
72
67
|
const obj = { foo: undefined };
|
|
73
|
-
|
|
68
|
+
expect(get(obj, 'foo.bar')).toBeUndefined();
|
|
74
69
|
});
|
|
75
70
|
it('returns undefined when intermediate property is null', () => {
|
|
76
71
|
const obj = { foo: null };
|
|
77
|
-
|
|
72
|
+
expect(get(obj, 'foo.bar')).toBeUndefined();
|
|
78
73
|
});
|
|
79
74
|
it('returns undefined with empty string path', () => {
|
|
80
75
|
const obj = { foo: 'bar' };
|
|
81
|
-
|
|
76
|
+
expect(get(obj, '')).toBeUndefined();
|
|
82
77
|
});
|
|
83
78
|
it('handles complex nested structure', () => {
|
|
84
79
|
const obj = {
|
|
@@ -87,13 +82,13 @@ describe('path', () => {
|
|
|
87
82
|
{ name: 'Bob', address: { city: 'LA' } },
|
|
88
83
|
],
|
|
89
84
|
};
|
|
90
|
-
|
|
85
|
+
expect(get(obj, 'users.1.address.city')).toBe('LA');
|
|
91
86
|
});
|
|
92
87
|
it('returns undefined for null root', () => {
|
|
93
|
-
|
|
88
|
+
expect(get(null, 'foo')).toBeUndefined();
|
|
94
89
|
});
|
|
95
90
|
it('returns undefined for undefined root', () => {
|
|
96
|
-
|
|
91
|
+
expect(get(undefined, 'foo')).toBeUndefined();
|
|
97
92
|
});
|
|
98
93
|
});
|
|
99
94
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neovici/cosmoz-queue",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "A reusable queue component for master-detail views with list, split, and queue modes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web-components",
|
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
"lint": "tsc && eslint --cache .",
|
|
30
30
|
"build": "tsc -p tsconfig.build.json",
|
|
31
31
|
"start": "wds",
|
|
32
|
-
"test": "
|
|
33
|
-
"test:
|
|
32
|
+
"test": "vitest --run",
|
|
33
|
+
"test:unit": "vitest --project=unit --run",
|
|
34
|
+
"test:storybook": "vitest --project=storybook --run",
|
|
35
|
+
"test:watch": "vitest",
|
|
34
36
|
"check:duplicates": "check-duplicate-components",
|
|
35
37
|
"dev": "npm run storybook:start",
|
|
36
38
|
"storybook:start": "storybook dev -p 8000",
|
|
@@ -91,26 +93,27 @@
|
|
|
91
93
|
"@commitlint/config-conventional": "^20.0.0",
|
|
92
94
|
"@eslint/eslintrc": "^2.0.0",
|
|
93
95
|
"@neovici/cfg": "^2.8.0",
|
|
94
|
-
"@neovici/testing": "^2.
|
|
95
|
-
"@open-wc/testing": "^4.0.0",
|
|
96
|
-
"@open-wc/testing-helpers": "^3.0.1",
|
|
96
|
+
"@neovici/testing": "^2.2.0",
|
|
97
97
|
"@semantic-release/changelog": "^6.0.0",
|
|
98
98
|
"@semantic-release/git": "^10.0.0",
|
|
99
99
|
"@storybook/addon-links": "^10.0.0",
|
|
100
|
+
"@storybook/addon-vitest": "^10.0.0",
|
|
100
101
|
"@storybook/web-components": "^10.0.0",
|
|
101
102
|
"@storybook/web-components-vite": "^10.0.0",
|
|
102
|
-
"@types/mocha": "^10.0.6",
|
|
103
103
|
"@types/node": "^22.10.2",
|
|
104
|
+
"@types/react": "^19.2.13",
|
|
104
105
|
"@types/split.js": "^1.6.0",
|
|
105
|
-
"@
|
|
106
|
-
"@
|
|
106
|
+
"@vitest/browser": "^4.0.0",
|
|
107
|
+
"@vitest/browser-playwright": "^4.0.0",
|
|
107
108
|
"esbuild": "^0.27.0",
|
|
108
109
|
"http-server": "^14.1.1",
|
|
109
110
|
"husky": "^9.0.11",
|
|
111
|
+
"jsdom": "^26.0.0",
|
|
112
|
+
"playwright": "^1.52.0",
|
|
110
113
|
"semantic-release": "^25.0.0",
|
|
111
|
-
"sinon": "^19.0.0",
|
|
112
114
|
"storybook": "^10.0.0",
|
|
113
|
-
"typescript": "^5.4.3"
|
|
115
|
+
"typescript": "^5.4.3",
|
|
116
|
+
"vitest": "^4.0.0"
|
|
114
117
|
},
|
|
115
118
|
"overrides": {
|
|
116
119
|
"conventional-changelog-conventionalcommits": ">= 8.0.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"render.test.snap.d.ts","sourceRoot":"","sources":["../../../../src/queue/test/__snapshots__/render.test.snap.js"],"names":[],"mappings":"AACA,yCAA4B"}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/* @web/test-runner snapshot v1 */
|
|
2
|
-
export const snapshots = {};
|
|
3
|
-
snapshots['queue > render renderNav'] = `<button
|
|
4
|
-
class="button-nav prev"
|
|
5
|
-
disabled=""
|
|
6
|
-
slot="extra"
|
|
7
|
-
>
|
|
8
|
-
</button>
|
|
9
|
-
`;
|
|
10
|
-
/* end snapshot queue > render renderNav */
|
|
11
|
-
snapshots['queue > render renderPagination'] = `<div class="tabn-pagination">
|
|
12
|
-
<button class="button-page page-prev">
|
|
13
|
-
</button>
|
|
14
|
-
<button class="button-page page-next">
|
|
15
|
-
</button>
|
|
16
|
-
</div>
|
|
17
|
-
`;
|
|
18
|
-
/* end snapshot queue > render renderPagination */
|
|
19
|
-
snapshots['queue > render renderNav'] = `<button
|
|
20
|
-
class="button-nav prev"
|
|
21
|
-
disabled=""
|
|
22
|
-
name="Previous item"
|
|
23
|
-
slot="extra"
|
|
24
|
-
>
|
|
25
|
-
</button>
|
|
26
|
-
`;
|
|
27
|
-
/* end snapshot queue > render renderNav */
|
|
28
|
-
snapshots['queue > render renderPagination'] = `<div class="tabn-pagination">
|
|
29
|
-
<button
|
|
30
|
-
class="button-page page-prev"
|
|
31
|
-
name="Previous page"
|
|
32
|
-
>
|
|
33
|
-
</button>
|
|
34
|
-
<button
|
|
35
|
-
class="button-page page-next"
|
|
36
|
-
name="Next page"
|
|
37
|
-
>
|
|
38
|
-
</button>
|
|
39
|
-
</div>
|
|
40
|
-
`;
|
|
41
|
-
/* end snapshot queue > render renderPagination */
|
|
42
|
-
snapshots['queue > render renderNav'] = `<button
|
|
43
|
-
class="button-nav prev"
|
|
44
|
-
disabled=""
|
|
45
|
-
slot="extra"
|
|
46
|
-
title=""
|
|
47
|
-
>
|
|
48
|
-
</button>
|
|
49
|
-
`;
|
|
50
|
-
/* end snapshot queue > render renderNav */
|
|
51
|
-
snapshots['queue > render renderPagination'] = `<div class="tabn-pagination">
|
|
52
|
-
<button
|
|
53
|
-
class="button-page page-prev"
|
|
54
|
-
title=""
|
|
55
|
-
>
|
|
56
|
-
</button>
|
|
57
|
-
<button
|
|
58
|
-
class="button-page page-next"
|
|
59
|
-
title=""
|
|
60
|
-
>
|
|
61
|
-
</button>
|
|
62
|
-
</div>
|
|
63
|
-
`;
|
|
64
|
-
/* end snapshot queue > render renderPagination */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"item-click.test.d.ts","sourceRoot":"","sources":["../../../src/queue/test/item-click.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { assert, oneEvent } from '@open-wc/testing';
|
|
2
|
-
import { oneDefaultPreventedEvent } from '@open-wc/testing-helpers';
|
|
3
|
-
import { itemClick } from '../item-click';
|
|
4
|
-
describe('item-click', () => {
|
|
5
|
-
it('fires event', async () => {
|
|
6
|
-
const el = document.createElement('div');
|
|
7
|
-
el.addEventListener('click', itemClick({ index: 2, activate: 'queue' }));
|
|
8
|
-
setTimeout(() => el.click());
|
|
9
|
-
const { detail } = await oneEvent(el, 'omnitable-item-click');
|
|
10
|
-
assert.equal(detail.index, 2);
|
|
11
|
-
assert.equal(detail.activate, 'queue');
|
|
12
|
-
});
|
|
13
|
-
it('prevents default', async () => {
|
|
14
|
-
const el = document.createElement('div');
|
|
15
|
-
const ev = new MouseEvent('click');
|
|
16
|
-
el.addEventListener('click', itemClick({ index: 3, activate: 'list' }));
|
|
17
|
-
setTimeout(() => el.dispatchEvent(ev));
|
|
18
|
-
const { detail } = await oneDefaultPreventedEvent(el, 'omnitable-item-click');
|
|
19
|
-
assert.equal(detail.index, 3);
|
|
20
|
-
assert.equal(detail.activate, 'list');
|
|
21
|
-
});
|
|
22
|
-
it('does not fire event', async () => {
|
|
23
|
-
const el = document.createElement('div');
|
|
24
|
-
const ev = new MouseEvent('click', { ctrlKey: true });
|
|
25
|
-
el.addEventListener('click', itemClick({ index: 3, activate: 'list' }));
|
|
26
|
-
setTimeout(() => el.dispatchEvent(ev));
|
|
27
|
-
});
|
|
28
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"render.test.d.ts","sourceRoot":"","sources":["../../../src/queue/test/render.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { expect, fixture } from '@open-wc/testing';
|
|
2
|
-
import { nothing } from 'lit-html';
|
|
3
|
-
import { spy } from 'sinon';
|
|
4
|
-
import { renderNav, renderPagination } from '../render';
|
|
5
|
-
describe('queue > render', () => {
|
|
6
|
-
it('renderNav', async () => {
|
|
7
|
-
const el = await fixture(renderNav({}));
|
|
8
|
-
await expect(el).dom.to.equalSnapshot();
|
|
9
|
-
});
|
|
10
|
-
it('renderPagination nothing', async () => {
|
|
11
|
-
expect(renderPagination()).to.equal(nothing);
|
|
12
|
-
});
|
|
13
|
-
it('renderPagination', async () => {
|
|
14
|
-
const onPage = spy();
|
|
15
|
-
const el = await fixture(renderPagination({
|
|
16
|
-
totalPages: 10,
|
|
17
|
-
pageNumber: 3,
|
|
18
|
-
onPage,
|
|
19
|
-
}));
|
|
20
|
-
await expect(el).dom.to.equalSnapshot();
|
|
21
|
-
el.querySelector('.page-next')?.click();
|
|
22
|
-
expect(onPage).to.have.been.calledWith(4);
|
|
23
|
-
onPage.resetHistory();
|
|
24
|
-
el.querySelector('.page-prev')?.click();
|
|
25
|
-
expect(onPage).to.have.been.calledWith(2);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-pref.test.d.ts","sourceRoot":"","sources":["../../../src/queue/test/use-pref.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { renderHook } from '@neovici/testing';
|
|
2
|
-
import { assert } from '@open-wc/testing';
|
|
3
|
-
import { usePref } from '../use-pref';
|
|
4
|
-
describe('use-pref', () => {
|
|
5
|
-
it('default pref', async () => {
|
|
6
|
-
const { result } = await renderHook(() => usePref('some', 'asdad'));
|
|
7
|
-
assert.equal(result.current[0], 'asdad');
|
|
8
|
-
});
|
|
9
|
-
it('update pref', async () => {
|
|
10
|
-
const { result, nextUpdate } = await renderHook(() => usePref('somethingelse'));
|
|
11
|
-
assert.equal(result.current[0], undefined);
|
|
12
|
-
setTimeout(() => result.current[1]('dads'));
|
|
13
|
-
await nextUpdate();
|
|
14
|
-
assert.equal(result.current[0], 'dads');
|
|
15
|
-
});
|
|
16
|
-
});
|
|
File without changes
|