@neovici/cosmoz-queue 2.7.2 → 2.8.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/README.md +293 -259
- package/package.json +4 -20
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @neovici/cosmoz-queue
|
|
2
2
|
|
|
3
|
+
[](https://github.com/changesets/changesets)
|
|
4
|
+
|
|
3
5
|
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
6
|
|
|
5
7
|
> **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.
|
|
@@ -26,60 +28,60 @@ import { fetchOrderDetails$ } from './api';
|
|
|
26
28
|
import type { OrderListItem } from './types';
|
|
27
29
|
|
|
28
30
|
const OrderListQueue = ({ heading }: { heading: string }) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
31
|
+
queue<OrderListItem>({
|
|
32
|
+
// Title displayed above the list
|
|
33
|
+
heading,
|
|
34
|
+
|
|
35
|
+
// Unique key for persisting table column settings
|
|
36
|
+
settingsId: 'order-list',
|
|
37
|
+
|
|
38
|
+
// URL hash parameter names (enables deep-linking and back/forward)
|
|
39
|
+
idHashParam: 'id',
|
|
40
|
+
tabHashParam: 'qtab',
|
|
41
|
+
|
|
42
|
+
// Async function that fetches full details for a list item.
|
|
43
|
+
// Results are memoized per item identity.
|
|
44
|
+
details: useCallback(
|
|
45
|
+
(item: OrderListItem) => fetchOrderDetails$(item.id),
|
|
46
|
+
[],
|
|
47
|
+
),
|
|
48
|
+
|
|
49
|
+
// Render the list component. `thru` contains bindings that wire
|
|
50
|
+
// the omnitable events to queue state (items, selection, clicks).
|
|
51
|
+
// Spread them onto your list component.
|
|
52
|
+
list: (thru, { onRef }) =>
|
|
53
|
+
html`<order-list-core ${spread(thru)} ${ref(onRef)}></order-list-core>`,
|
|
54
|
+
|
|
55
|
+
// Render the detail view. `nav` contains prev/next buttons --
|
|
56
|
+
// place them in a slot or wherever navigation belongs in your view.
|
|
57
|
+
view: (thru, { nav }) =>
|
|
58
|
+
html`<order-view-core ${spread(thru)}>${nav}</order-view-core>`,
|
|
59
|
+
|
|
60
|
+
// Render a loading skeleton shown while `details` resolves.
|
|
61
|
+
loader: (thru, { nav }) =>
|
|
62
|
+
html`<order-view-skeleton ${spread(thru)}>${nav}</order-view-skeleton>`,
|
|
63
|
+
});
|
|
62
64
|
|
|
63
65
|
customElements.define('order-list-queue', component(OrderListQueue));
|
|
64
66
|
```
|
|
65
67
|
|
|
66
68
|
### `queue()` props
|
|
67
69
|
|
|
68
|
-
| Prop
|
|
69
|
-
|
|
70
|
-
| `heading`
|
|
71
|
-
| `settingsId`
|
|
72
|
-
| `idHashParam`
|
|
73
|
-
| `tabHashParam` | `string`
|
|
74
|
-
| `details`
|
|
75
|
-
| `api`
|
|
76
|
-
| `list`
|
|
77
|
-
| `view`
|
|
78
|
-
| `loader`
|
|
79
|
-
| `pagination`
|
|
80
|
-
| `fallback`
|
|
81
|
-
| `split`
|
|
82
|
-
| `afterHeading` | `unknown?`
|
|
70
|
+
| Prop | Type | Default | Description |
|
|
71
|
+
| -------------- | --------------------------------- | -------- | ---------------------------------------------------------------------- |
|
|
72
|
+
| `heading` | `string` | -- | Title displayed above the tabs |
|
|
73
|
+
| `settingsId` | `string?` | -- | Key for persisting omnitable column settings |
|
|
74
|
+
| `idHashParam` | `string` | `'qid'` | URL hash param for the selected item ID |
|
|
75
|
+
| `tabHashParam` | `string` | `'qtab'` | URL hash param for the active tab |
|
|
76
|
+
| `details` | `(item: I) => PromiseLike<D>` | -- | Fetches full details for a list item |
|
|
77
|
+
| `api` | `(id: string, item: I) => string` | -- | Alternative to `details`: returns a URL, queue fetches JSON internally |
|
|
78
|
+
| `list` | `(thru, props) => TemplateResult` | -- | Renders the list component |
|
|
79
|
+
| `view` | `(thru, props) => TemplateResult` | -- | Renders the detail view |
|
|
80
|
+
| `loader` | `(thru, props) => TemplateResult` | -- | Renders the loading skeleton |
|
|
81
|
+
| `pagination` | `Pagination?` | -- | Server-side pagination config |
|
|
82
|
+
| `fallback` | `string?` | -- | Default tab when none is set in URL hash |
|
|
83
|
+
| `split` | `SplitOpts?` | -- | Split.js configuration (sizes, min sizes) |
|
|
84
|
+
| `afterHeading` | `unknown?` | -- | Extra content rendered after the heading |
|
|
83
85
|
|
|
84
86
|
Use **`details`** when you control the fetch (most common). Use **`api`** when you want the queue to handle fetching via URL.
|
|
85
87
|
|
|
@@ -88,12 +90,14 @@ Use **`details`** when you control the fetch (most common). Use **`api`** when y
|
|
|
88
90
|
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
91
|
|
|
90
92
|
**For `list`:**
|
|
93
|
+
|
|
91
94
|
- `.settingsId`, `.exposedParts` -- table configuration
|
|
92
95
|
- `@visible-items-changed`, `@selected-items-changed`, `@total-available-changed` -- sync items/selection back to queue
|
|
93
96
|
- `@omnitable-item-click` -- item click handler
|
|
94
97
|
- `@async-simple-action` -- action completion handler
|
|
95
98
|
|
|
96
99
|
**For `view`:**
|
|
100
|
+
|
|
97
101
|
- `.item` -- the current detail item (list item or resolved detail)
|
|
98
102
|
- `.hideActions` -- whether to hide actions (true when items are selected in split mode)
|
|
99
103
|
- `@async-simple-action` -- action completion handler
|
|
@@ -104,11 +108,11 @@ Spread these onto your component with `${spread(thru)}` from `@open-wc/lit-helpe
|
|
|
104
108
|
|
|
105
109
|
The queue provides three tab-based view modes:
|
|
106
110
|
|
|
107
|
-
| Mode
|
|
108
|
-
|
|
109
|
-
| **List**
|
|
110
|
-
| **Split** | `split`
|
|
111
|
-
| **Queue** | `queue`
|
|
111
|
+
| Mode | Tab name | Behavior |
|
|
112
|
+
| --------- | ---------- | --------------------------------------------------------------------------------------- |
|
|
113
|
+
| **List** | `overview` | Full-width data table (`cosmoz-omnitable`). The default view. |
|
|
114
|
+
| **Split** | `split` | Side-by-side: resizable list on the left, detail view on the right. Disabled on mobile. |
|
|
115
|
+
| **Queue** | `queue` | Full-screen detail view with prev/next navigation and keyboard arrow keys. |
|
|
112
116
|
|
|
113
117
|
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
118
|
|
|
@@ -130,111 +134,111 @@ import { t } from 'i18next';
|
|
|
130
134
|
import type { OrderListItem } from './types';
|
|
131
135
|
|
|
132
136
|
interface Props {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
exposedParts: string;
|
|
138
|
+
api: (params: { pathLocator: string }) => Promise<{
|
|
139
|
+
items: OrderListItem[];
|
|
140
|
+
metaData: { totalAvailable: number };
|
|
141
|
+
}>;
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
const OrderListCore = ({ exposedParts, api }: Props) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
145
|
+
return listCore({
|
|
146
|
+
// Unique key for persisting column settings
|
|
147
|
+
settingsId: 'order-list-core',
|
|
148
|
+
|
|
149
|
+
// CSS parts to expose for external styling
|
|
150
|
+
exposedParts,
|
|
151
|
+
|
|
152
|
+
// When false, omnitable applies its own local filtering
|
|
153
|
+
noLocal: false,
|
|
154
|
+
|
|
155
|
+
// Column definitions. The tuple is [factory, deps] -- same pattern
|
|
156
|
+
// as useMemo. The factory returns an object where each key becomes
|
|
157
|
+
// a column name.
|
|
158
|
+
columns: [
|
|
159
|
+
() => ({
|
|
160
|
+
title: column({
|
|
161
|
+
render: ({ name }) =>
|
|
162
|
+
html`<cosmoz-omnitable-column
|
|
163
|
+
name="${name}"
|
|
164
|
+
title=${t('Title')}
|
|
165
|
+
flex="2"
|
|
166
|
+
.renderCell=${(
|
|
167
|
+
_col: unknown,
|
|
168
|
+
{ item, index }: { item: OrderListItem; index: number },
|
|
169
|
+
) =>
|
|
170
|
+
html`<a
|
|
171
|
+
@click="${itemClick({
|
|
172
|
+
index,
|
|
173
|
+
activate: ['split', 'queue'],
|
|
174
|
+
})}"
|
|
175
|
+
>${item.title}</a
|
|
176
|
+
>`}
|
|
177
|
+
></cosmoz-omnitable-column>`,
|
|
178
|
+
}),
|
|
179
|
+
|
|
180
|
+
status: column({
|
|
181
|
+
render: ({ name }) =>
|
|
182
|
+
html`<cosmoz-omnitable-column-autocomplete
|
|
183
|
+
name="${name}"
|
|
184
|
+
title=${t('Status')}
|
|
185
|
+
value-path="status.name"
|
|
186
|
+
></cosmoz-omnitable-column-autocomplete>`,
|
|
187
|
+
}),
|
|
188
|
+
|
|
189
|
+
createdAt: column({
|
|
190
|
+
render: ({ name }) =>
|
|
191
|
+
html`<cosmoz-omnitable-column-date
|
|
192
|
+
name="${name}"
|
|
193
|
+
title=${t('Date created')}
|
|
194
|
+
></cosmoz-omnitable-column-date>`,
|
|
195
|
+
}),
|
|
196
|
+
}),
|
|
197
|
+
[], // dependency array (empty = compute once)
|
|
198
|
+
],
|
|
199
|
+
|
|
200
|
+
// Reactive query parameters. Recomputed when deps change,
|
|
201
|
+
// which triggers a new data fetch.
|
|
202
|
+
params: [() => ({ pathLocator: '/some/path' }), []],
|
|
203
|
+
|
|
204
|
+
// Data fetcher. Receives { params, page, pageSize }.
|
|
205
|
+
// Must return { items, total }.
|
|
206
|
+
list$: [
|
|
207
|
+
({ params }) =>
|
|
208
|
+
api(params).then((r) => ({
|
|
209
|
+
items: r.items ?? [],
|
|
210
|
+
total: r.metaData?.totalAvailable ?? 0,
|
|
211
|
+
})),
|
|
212
|
+
[api],
|
|
213
|
+
],
|
|
214
|
+
|
|
215
|
+
// Batch actions shown when items are selected
|
|
216
|
+
actions: [myAction()],
|
|
217
|
+
});
|
|
214
218
|
};
|
|
215
219
|
|
|
216
220
|
customElements.define(
|
|
217
|
-
|
|
218
|
-
|
|
221
|
+
'order-list-core',
|
|
222
|
+
component(OrderListCore, { styleSheets: [style] }),
|
|
219
223
|
);
|
|
220
224
|
```
|
|
221
225
|
|
|
222
226
|
### `listCore()` props
|
|
223
227
|
|
|
224
|
-
| Prop
|
|
225
|
-
|
|
226
|
-
| `settingsId`
|
|
227
|
-
| `exposedParts`
|
|
228
|
-
| `noLocal`
|
|
229
|
-
| `columns`
|
|
230
|
-
| `params`
|
|
231
|
-
| `list$`
|
|
232
|
-
| `pageSize`
|
|
233
|
-
| `actions`
|
|
234
|
-
| `content`
|
|
235
|
-
| `hashParam`
|
|
236
|
-
| `csvFilename`
|
|
237
|
-
| `enabledColumns` | `string[]?`
|
|
228
|
+
| Prop | Type | Description |
|
|
229
|
+
| ---------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
230
|
+
| `settingsId` | `string` | Persistence key for column settings |
|
|
231
|
+
| `exposedParts` | `string?` | CSS `exportparts` value |
|
|
232
|
+
| `noLocal` | `boolean` | Skip omnitable local filtering (default: `true`) |
|
|
233
|
+
| `columns` | `[() => Columns, deps[]]` | Column definitions (memoized) |
|
|
234
|
+
| `params` | `[(opts) => Params, deps[]]` | Query parameters (memoized). The `opts` include `{ filters, descending, sortOn, columns }` |
|
|
235
|
+
| `list$` | `[(props) => Promise<{ items, total }>, deps[]]` | Data fetcher. Receives `{ params, page, pageSize }` |
|
|
236
|
+
| `pageSize` | `number?` | Items per page for "load more" (default: `50`) |
|
|
237
|
+
| `actions` | `Action[]?` | Batch actions |
|
|
238
|
+
| `content` | `(opts) => Renderable` | Extra content inside the omnitable |
|
|
239
|
+
| `hashParam` | `string?` | URL hash param for omnitable state |
|
|
240
|
+
| `csvFilename` | `string?` | Filename for CSV export |
|
|
241
|
+
| `enabledColumns` | `string[]?` | Initially visible columns |
|
|
238
242
|
|
|
239
243
|
### `column()`
|
|
240
244
|
|
|
@@ -244,22 +248,22 @@ Type-safe column definition helper:
|
|
|
244
248
|
import { column } from '@neovici/cosmoz-queue/list';
|
|
245
249
|
|
|
246
250
|
const myColumn = column({
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
251
|
+
// Optional ordering hint
|
|
252
|
+
order: 1,
|
|
253
|
+
|
|
254
|
+
// Optional sort key
|
|
255
|
+
sort: 'name',
|
|
256
|
+
|
|
257
|
+
// Optional filter transform: receives the raw filter value,
|
|
258
|
+
// returns the value sent to the API
|
|
259
|
+
filter: (value: string) => ({ name: value }),
|
|
260
|
+
|
|
261
|
+
// Render function -- receives { name } where name is the object key
|
|
262
|
+
render: ({ name }) =>
|
|
263
|
+
html`<cosmoz-omnitable-column
|
|
264
|
+
name="${name}"
|
|
265
|
+
title="Name"
|
|
266
|
+
></cosmoz-omnitable-column>`,
|
|
263
267
|
});
|
|
264
268
|
```
|
|
265
269
|
|
|
@@ -283,32 +287,34 @@ import { when } from 'lit-html/directives/when.js';
|
|
|
283
287
|
|
|
284
288
|
// action() is an identity function that provides type inference
|
|
285
289
|
export const approveOrder = action<OrderItem, ApproveFields>({
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
290
|
+
// Label shown on the button
|
|
291
|
+
title: () => t('Approve'),
|
|
292
|
+
|
|
293
|
+
// Optional: filter which items this action applies to.
|
|
294
|
+
// If provided, non-applicable items are excluded and the button
|
|
295
|
+
// shows a count badge like "Approve (3/5)".
|
|
296
|
+
applicable: (item) => item.status === 'pending',
|
|
297
|
+
|
|
298
|
+
// Optional: custom button renderer. Defaults to `defaultButton()`.
|
|
299
|
+
// Return `nothing` to hide the button conditionally.
|
|
300
|
+
button: (opts) => when(opts.items.length >= 1, () => defaultButton(opts)),
|
|
301
|
+
|
|
302
|
+
// Dialog configuration. Opens a `cosmoz-form` dialog.
|
|
303
|
+
// `items` contains only the applicable items.
|
|
304
|
+
dialog: ({ items, title }) => ({
|
|
305
|
+
heading: title,
|
|
306
|
+
description: t('Approve the selected orders'),
|
|
307
|
+
fields: [
|
|
308
|
+
// cosmoz-form field definitions
|
|
309
|
+
],
|
|
310
|
+
initial: {},
|
|
311
|
+
onSave: async (values) => {
|
|
312
|
+
await approveOrders(
|
|
313
|
+
items.map((i) => i.id),
|
|
314
|
+
values,
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
312
318
|
});
|
|
313
319
|
```
|
|
314
320
|
|
|
@@ -321,25 +327,23 @@ import { renderActions } from '@neovici/cosmoz-queue/actions';
|
|
|
321
327
|
import { approveOrder } from './actions';
|
|
322
328
|
|
|
323
329
|
// In a view component:
|
|
324
|
-
const bottomBar = renderActions({ items: [currentItem], open })([
|
|
325
|
-
approveOrder,
|
|
326
|
-
]);
|
|
330
|
+
const bottomBar = renderActions({ items: [currentItem], open })([approveOrder]);
|
|
327
331
|
```
|
|
328
332
|
|
|
329
333
|
### Action types
|
|
330
334
|
|
|
331
335
|
```ts
|
|
332
336
|
interface Action<TItem, TDialog> {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
+
title: () => string;
|
|
338
|
+
applicable?: (item: TItem) => boolean;
|
|
339
|
+
button?: (opts: Action & ActionOpts) => unknown;
|
|
340
|
+
dialog: (opts: DialogOpts) => Dialogable | Promise<Dialogable>;
|
|
337
341
|
}
|
|
338
342
|
|
|
339
343
|
interface ActionOpts<TItem> {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
items: TItem[];
|
|
345
|
+
open: (dialog: Dialogable) => void;
|
|
346
|
+
slot?: string;
|
|
343
347
|
}
|
|
344
348
|
```
|
|
345
349
|
|
|
@@ -353,49 +357,56 @@ When `queue()` is too opinionated, use the individual hooks directly.
|
|
|
353
357
|
import { useQueue, renderQueue, renderNav } from '@neovici/cosmoz-queue';
|
|
354
358
|
|
|
355
359
|
const MyQueue = ({ heading }: { heading: string }) => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
360
|
+
const {
|
|
361
|
+
index,
|
|
362
|
+
mobile,
|
|
363
|
+
tabnav,
|
|
364
|
+
items,
|
|
365
|
+
setItems,
|
|
366
|
+
setSelected,
|
|
367
|
+
setTotalAvailable,
|
|
368
|
+
totalAvailable,
|
|
369
|
+
onItemClick,
|
|
370
|
+
nav,
|
|
371
|
+
} = useQueue<MyItem>({
|
|
372
|
+
idHashParam: 'id',
|
|
373
|
+
tabHashParam: 'tab',
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
return renderQueue({
|
|
377
|
+
heading,
|
|
378
|
+
mobile,
|
|
379
|
+
index,
|
|
380
|
+
items,
|
|
381
|
+
tabnav,
|
|
382
|
+
totalAvailable,
|
|
383
|
+
nav,
|
|
384
|
+
list: html`<my-list
|
|
385
|
+
@visible-items-changed=${updateWith(setItems)}
|
|
386
|
+
@selected-items-changed=${updateWith(setSelected)}
|
|
387
|
+
@total-available-changed=${updateWith(setTotalAvailable)}
|
|
388
|
+
@omnitable-item-click=${onItemClick}
|
|
389
|
+
></my-list>`,
|
|
390
|
+
renderItem: ({ item, nav }) =>
|
|
391
|
+
html`<my-view .item=${item}>${nav}</my-view>`,
|
|
392
|
+
renderLoader: ({ item, nav }) =>
|
|
393
|
+
html`<my-skeleton .item=${item}>${nav}</my-skeleton>`,
|
|
394
|
+
});
|
|
384
395
|
};
|
|
385
396
|
```
|
|
386
397
|
|
|
387
398
|
### Individual hooks
|
|
388
399
|
|
|
389
|
-
| Hook
|
|
390
|
-
|
|
391
|
-
| `useTabs({ items, hashParam, mobile, fallback })` | `@neovici/cosmoz-queue` | Manages overview/split/queue tabs. Returns `{ tabnav, activeTab }`.
|
|
392
|
-
| `useDataNav(items, opts)`
|
|
393
|
-
| `useSplit({ activeTab, ...splitOpts })`
|
|
394
|
-
| `useListState()`
|
|
395
|
-
| `useListSSE({ entity, params, list$ })`
|
|
396
|
-
| `useFetchActions({ pathLocator, selected, api })` | `@neovici/cosmoz-queue` | Fetches available actions for selected items from an API. Returns `{ actions, actionRows, actionsFetching }`.
|
|
397
|
-
| `useAsyncAction(nav)`
|
|
398
|
-
| `usePagination()`
|
|
400
|
+
| Hook | Import | Purpose |
|
|
401
|
+
| ------------------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
402
|
+
| `useTabs({ items, hashParam, mobile, fallback })` | `@neovici/cosmoz-queue` | Manages overview/split/queue tabs. Returns `{ tabnav, activeTab }`. |
|
|
403
|
+
| `useDataNav(items, opts)` | `@neovici/cosmoz-queue` | Item navigation with prev/next, URL hash sync. Returns `{ item, setItem, next, prev, forward, index }`. |
|
|
404
|
+
| `useSplit({ activeTab, ...splitOpts })` | `@neovici/cosmoz-queue` | Initializes Split.js when in split mode. |
|
|
405
|
+
| `useListState()` | `@neovici/cosmoz-queue` | Creates `items`, `selected`, `totalAvailable` state with setters. |
|
|
406
|
+
| `useListSSE({ entity, params, list$ })` | `@neovici/cosmoz-queue` | Subscribes to Server-Sent Events (`cosmoz-${entity}-updated`) for real-time list updates. |
|
|
407
|
+
| `useFetchActions({ pathLocator, selected, api })` | `@neovici/cosmoz-queue` | Fetches available actions for selected items from an API. Returns `{ actions, actionRows, actionsFetching }`. |
|
|
408
|
+
| `useAsyncAction(nav)` | `@neovici/cosmoz-queue` | Handles async action completion with automatic item removal from the list. Returns `{ listRef, onAsyncSimpleAction }`. |
|
|
409
|
+
| `usePagination()` | `@neovici/cosmoz-queue` | URL hash-based page state. Returns `{ page, onPage }`. |
|
|
399
410
|
|
|
400
411
|
## Utilities
|
|
401
412
|
|
|
@@ -405,17 +416,17 @@ Pre-configured `fetch` wrapper with CORS and credentials:
|
|
|
405
416
|
|
|
406
417
|
```ts
|
|
407
418
|
import {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
419
|
+
fetch,
|
|
420
|
+
setBaseInit,
|
|
421
|
+
handleJSON,
|
|
422
|
+
RequestError,
|
|
412
423
|
} from '@neovici/cosmoz-queue/util/fetch';
|
|
413
424
|
|
|
414
425
|
// Configure default headers (call once at app startup)
|
|
415
426
|
setBaseInit({
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
427
|
+
headers: { 'X-Custom-Header': 'value' },
|
|
428
|
+
// Or use dynamic headers:
|
|
429
|
+
getHeaders: () => ({ Authorization: `Bearer ${getToken()}` }),
|
|
419
430
|
});
|
|
420
431
|
|
|
421
432
|
// fetch() includes mode: 'cors', credentials: 'include' by default
|
|
@@ -434,7 +445,7 @@ import { itemClick } from '@neovici/cosmoz-queue';
|
|
|
434
445
|
|
|
435
446
|
// In an omnitable cell renderer:
|
|
436
447
|
html`<a @click="${itemClick({ index, activate: ['split', 'queue'] })}">
|
|
437
|
-
|
|
448
|
+
${item.title}
|
|
438
449
|
</a>`;
|
|
439
450
|
```
|
|
440
451
|
|
|
@@ -442,10 +453,10 @@ The `activate` option specifies which tab to switch to. The queue picks the firs
|
|
|
442
453
|
|
|
443
454
|
### Other utilities
|
|
444
455
|
|
|
445
|
-
| Function
|
|
446
|
-
|
|
447
|
-
| `getItems(items, selected)` | Returns `selected` if non-empty, otherwise `items`
|
|
448
|
-
| `touch(list, id)`
|
|
456
|
+
| Function | Description |
|
|
457
|
+
| --------------------------- | -------------------------------------------------------------------- |
|
|
458
|
+
| `getItems(items, selected)` | Returns `selected` if non-empty, otherwise `items` |
|
|
459
|
+
| `touch(list, id)` | Forces an omnitable item refresh by replacing it with a shallow copy |
|
|
449
460
|
|
|
450
461
|
## Architecture
|
|
451
462
|
|
|
@@ -479,14 +490,14 @@ queue() factory
|
|
|
479
490
|
|
|
480
491
|
## Entry points
|
|
481
492
|
|
|
482
|
-
| Import path
|
|
483
|
-
|
|
484
|
-
| `@neovici/cosmoz-queue`
|
|
485
|
-
| `@neovici/cosmoz-queue/actions`
|
|
486
|
-
| `@neovici/cosmoz-queue/list`
|
|
487
|
-
| `@neovici/cosmoz-queue/list/more`
|
|
488
|
-
| `@neovici/cosmoz-queue/list/more/render` | `renderLoadMore()` -- "Load more" button
|
|
489
|
-
| `@neovici/cosmoz-queue/util/fetch`
|
|
493
|
+
| Import path | Description |
|
|
494
|
+
| ---------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
495
|
+
| `@neovici/cosmoz-queue` | Main: `queue()`, `useQueue()`, `renderQueue()`, navigation hooks, SSE, utilities |
|
|
496
|
+
| `@neovici/cosmoz-queue/actions` | `action()`, `renderActions()`, `defaultButton()`, `actionCount()` |
|
|
497
|
+
| `@neovici/cosmoz-queue/list` | `listCore()`, `column()`, `useListCore()`, `useListCoreState()`, `renderListCore()` |
|
|
498
|
+
| `@neovici/cosmoz-queue/list/more` | `useMore()` -- progressive "load more" pagination |
|
|
499
|
+
| `@neovici/cosmoz-queue/list/more/render` | `renderLoadMore()` -- "Load more" button |
|
|
500
|
+
| `@neovici/cosmoz-queue/util/fetch` | `fetch()`, `setBaseInit()`, `handleJSON()`, `RequestError` |
|
|
490
501
|
|
|
491
502
|
## Deprecation notices
|
|
492
503
|
|
|
@@ -497,14 +508,37 @@ removed in v2.0.0. Use `details` instead:
|
|
|
497
508
|
|
|
498
509
|
```ts
|
|
499
510
|
// Before (deprecated)
|
|
500
|
-
queue({ api: (id, item) => apiUrl(`items/${id}`) })
|
|
511
|
+
queue({ api: (id, item) => apiUrl(`items/${id}`) });
|
|
501
512
|
|
|
502
513
|
// After
|
|
503
|
-
queue({
|
|
514
|
+
queue({
|
|
515
|
+
details: (item) => fetch(apiUrl(`items/${item.id}`)).then((r) => r.json()),
|
|
516
|
+
});
|
|
504
517
|
```
|
|
505
518
|
|
|
506
519
|
See [Migration guide](docs/migration-api-to-details.md) for more patterns.
|
|
507
520
|
|
|
521
|
+
## Releasing
|
|
522
|
+
|
|
523
|
+
This project uses [Changesets](https://github.com/changesets/changesets) for managing releases.
|
|
524
|
+
|
|
525
|
+
### How to release
|
|
526
|
+
|
|
527
|
+
1. Create a changeset: `npm run changeset`
|
|
528
|
+
2. Select the packages you have changed
|
|
529
|
+
3. Select the type of change (patch, minor, or major)
|
|
530
|
+
4. Write a summary of the change
|
|
531
|
+
5. Commit the changeset file
|
|
532
|
+
6. Push to GitHub
|
|
533
|
+
|
|
534
|
+
When the "Version Packages" PR is merged, the release will be published automatically.
|
|
535
|
+
|
|
536
|
+
### Versioning
|
|
537
|
+
|
|
538
|
+
- **Patch**: Bug fixes
|
|
539
|
+
- **Minor**: New features (backward compatible)
|
|
540
|
+
- **Major**: Breaking changes
|
|
541
|
+
|
|
508
542
|
## License
|
|
509
543
|
|
|
510
544
|
[Apache-2.0](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neovici/cosmoz-queue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "A reusable queue component for master-detail views with list, split, and queue modes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web-components",
|
|
@@ -39,19 +39,8 @@
|
|
|
39
39
|
"storybook:build": "storybook build",
|
|
40
40
|
"storybook:deploy": "storybook-to-ghpages",
|
|
41
41
|
"storybook:preview": "npm run storybook:build && http-server -d ./storybook-static/",
|
|
42
|
-
"prepare": "husky"
|
|
43
|
-
|
|
44
|
-
"release": {
|
|
45
|
-
"plugins": [
|
|
46
|
-
"@semantic-release/commit-analyzer",
|
|
47
|
-
"@semantic-release/release-notes-generator",
|
|
48
|
-
"@semantic-release/changelog",
|
|
49
|
-
"@semantic-release/github",
|
|
50
|
-
"@semantic-release/npm",
|
|
51
|
-
"@semantic-release/git"
|
|
52
|
-
],
|
|
53
|
-
"branch": "master",
|
|
54
|
-
"preset": "conventionalcommits"
|
|
42
|
+
"prepare": "husky",
|
|
43
|
+
"changeset": "changeset"
|
|
55
44
|
},
|
|
56
45
|
"publishConfig": {
|
|
57
46
|
"access": "public"
|
|
@@ -86,13 +75,12 @@
|
|
|
86
75
|
"split.js": "^1.6.5"
|
|
87
76
|
},
|
|
88
77
|
"devDependencies": {
|
|
78
|
+
"@changesets/cli": "^2.31.0",
|
|
89
79
|
"@commitlint/cli": "^20.0.0",
|
|
90
80
|
"@commitlint/config-conventional": "^20.0.0",
|
|
91
81
|
"@eslint/eslintrc": "^2.0.0",
|
|
92
82
|
"@neovici/cfg": "^2.11.0",
|
|
93
83
|
"@neovici/testing": "^2.2.0",
|
|
94
|
-
"@semantic-release/changelog": "^6.0.0",
|
|
95
|
-
"@semantic-release/git": "^10.0.0",
|
|
96
84
|
"@storybook/addon-links": "^10.0.0",
|
|
97
85
|
"@storybook/addon-vitest": "^10.0.0",
|
|
98
86
|
"@storybook/web-components": "^10.0.0",
|
|
@@ -107,12 +95,8 @@
|
|
|
107
95
|
"husky": "^9.0.11",
|
|
108
96
|
"jsdom": "^26.0.0",
|
|
109
97
|
"playwright": "^1.52.0",
|
|
110
|
-
"semantic-release": "^25.0.0",
|
|
111
98
|
"storybook": "^10.0.0",
|
|
112
99
|
"typescript": "^5.4.3",
|
|
113
100
|
"vitest": "^4.0.0"
|
|
114
|
-
},
|
|
115
|
-
"overrides": {
|
|
116
|
-
"conventional-changelog-conventionalcommits": ">= 8.0.0"
|
|
117
101
|
}
|
|
118
102
|
}
|