An open API service indexing awesome lists of open source software.

https://github.com/neovici/cosmoz-queue

Cosmoz Queue to manage a list of entitites
https://github.com/neovici/cosmoz-queue

Last synced: 3 months ago
JSON representation

Cosmoz Queue to manage a list of entitites

Awesome Lists containing this project

README

          

# @neovici/cosmoz-queue

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/).

> **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.

## Installation

```sh
npm install @neovici/cosmoz-queue
```

Peer dependencies: `@pionjs/pion`, `lit-html`, `i18next`.

## Quick start

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.

```ts
import { queue } from '@neovici/cosmoz-queue';
import { spread } from '@open-wc/lit-helpers';
import { component, useCallback } from '@pionjs/pion';
import { html } from 'lit-html';
import { ref } from 'lit-html/directives/ref.js';
import { fetchOrderDetails$ } from './api';
import type { OrderListItem } from './types';

const OrderListQueue = ({ heading }: { heading: string }) =>
queue({
// Title displayed above the list
heading,

// Unique key for persisting table column settings
settingsId: 'order-list',

// URL hash parameter names (enables deep-linking and back/forward)
idHashParam: 'id',
tabHashParam: 'qtab',

// Async function that fetches full details for a list item.
// Results are memoized per item identity.
details: useCallback(
(item: OrderListItem) => fetchOrderDetails$(item.id),
[],
),

// Render the list component. `thru` contains bindings that wire
// the omnitable events to queue state (items, selection, clicks).
// Spread them onto your list component.
list: (thru, { onRef }) =>
html``,

// Render the detail view. `nav` contains prev/next buttons --
// place them in a slot or wherever navigation belongs in your view.
view: (thru, { nav }) =>
html`${nav}`,

// Render a loading skeleton shown while `details` resolves.
loader: (thru, { nav }) =>
html`${nav}`,
});

customElements.define('order-list-queue', component(OrderListQueue));
```

### `queue()` props

| Prop | Type | Default | Description |
|---|---|---|---|
| `heading` | `string` | -- | Title displayed above the tabs |
| `settingsId` | `string?` | -- | Key for persisting omnitable column settings |
| `idHashParam` | `string` | `'qid'` | URL hash param for the selected item ID |
| `tabHashParam` | `string` | `'qtab'` | URL hash param for the active tab |
| `details` | `(item: I) => PromiseLike` | -- | Fetches full details for a list item |
| `api` | `(id: string, item: I) => string` | -- | Alternative to `details`: returns a URL, queue fetches JSON internally |
| `list` | `(thru, props) => TemplateResult` | -- | Renders the list component |
| `view` | `(thru, props) => TemplateResult` | -- | Renders the detail view |
| `loader` | `(thru, props) => TemplateResult` | -- | Renders the loading skeleton |
| `pagination` | `Pagination?` | -- | Server-side pagination config |
| `fallback` | `string?` | -- | Default tab when none is set in URL hash |
| `split` | `SplitOpts?` | -- | Split.js configuration (sizes, min sizes) |
| `afterHeading` | `unknown?` | -- | Extra content rendered after the heading |

Use **`details`** when you control the fetch (most common). Use **`api`** when you want the queue to handle fetching via URL.

### The `thru` bindings

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:

**For `list`:**
- `.settingsId`, `.exposedParts` -- table configuration
- `@visible-items-changed`, `@selected-items-changed`, `@total-available-changed` -- sync items/selection back to queue
- `@omnitable-item-click` -- item click handler
- `@async-simple-action` -- action completion handler

**For `view`:**
- `.item` -- the current detail item (list item or resolved detail)
- `.hideActions` -- whether to hide actions (true when items are selected in split mode)
- `@async-simple-action` -- action completion handler

Spread these onto your component with `${spread(thru)}` from `@open-wc/lit-helpers`.

## View modes

The queue provides three tab-based view modes:

| Mode | Tab name | Behavior |
|---|---|---|
| **List** | `overview` | Full-width data table (`cosmoz-omnitable`). The default view. |
| **Split** | `split` | Side-by-side: resizable list on the left, detail view on the right. Disabled on mobile. |
| **Queue** | `queue` | Full-screen detail view with prev/next navigation and keyboard arrow keys. |

The active tab is synced to the URL hash (e.g. `#qtab=split&id=abc-123`), enabling deep-linking and browser back/forward navigation.

On mobile viewports, the split tab is disabled and falls back to queue mode automatically.

## List module

The `@neovici/cosmoz-queue/list` entry point provides a managed `cosmoz-omnitable` with built-in state management, data fetching, pagination, and action rendering.

### `listCore()`

The high-level factory that combines `useListCore()` + `renderListCore()`:

```ts
import { itemClick } from '@neovici/cosmoz-queue';
import { column, listCore, style } from '@neovici/cosmoz-queue/list';
import { component, html } from '@pionjs/pion';
import { t } from 'i18next';
import type { OrderListItem } from './types';

interface Props {
exposedParts: string;
api: (params: { pathLocator: string }) => Promise<{
items: OrderListItem[];
metaData: { totalAvailable: number };
}>;
}

const OrderListCore = ({ exposedParts, api }: Props) => {
return listCore({
// Unique key for persisting column settings
settingsId: 'order-list-core',

// CSS parts to expose for external styling
exposedParts,

// When false, omnitable applies its own local filtering
noLocal: false,

// Column definitions. The tuple is [factory, deps] -- same pattern
// as useMemo. The factory returns an object where each key becomes
// a column name.
columns: [
() => ({
title: column({
render: ({ name }) =>
html`
html`${item.title}`}
>`,
}),

status: column({
render: ({ name }) =>
html``,
}),

createdAt: column({
render: ({ name }) =>
html``,
}),
}),
[], // dependency array (empty = compute once)
],

// Reactive query parameters. Recomputed when deps change,
// which triggers a new data fetch.
params: [() => ({ pathLocator: '/some/path' }), []],

// Data fetcher. Receives { params, page, pageSize }.
// Must return { items, total }.
list$: [
({ params }) =>
api(params).then((r) => ({
items: r.items ?? [],
total: r.metaData?.totalAvailable ?? 0,
})),
[api],
],

// Batch actions shown when items are selected
actions: [myAction()],
});
};

customElements.define(
'order-list-core',
component(OrderListCore, { styleSheets: [style] }),
);
```

### `listCore()` props

| Prop | Type | Description |
|---|---|---|
| `settingsId` | `string` | Persistence key for column settings |
| `exposedParts` | `string?` | CSS `exportparts` value |
| `noLocal` | `boolean` | Skip omnitable local filtering (default: `true`) |
| `columns` | `[() => Columns, deps[]]` | Column definitions (memoized) |
| `params` | `[(opts) => Params, deps[]]` | Query parameters (memoized). The `opts` include `{ filters, descending, sortOn, columns }` |
| `list$` | `[(props) => Promise<{ items, total }>, deps[]]` | Data fetcher. Receives `{ params, page, pageSize }` |
| `pageSize` | `number?` | Items per page for "load more" (default: `50`) |
| `actions` | `Action[]?` | Batch actions |
| `content` | `(opts) => Renderable` | Extra content inside the omnitable |
| `hashParam` | `string?` | URL hash param for omnitable state |
| `csvFilename` | `string?` | Filename for CSV export |
| `enabledColumns` | `string[]?` | Initially visible columns |

### `column()`

Type-safe column definition helper:

```ts
import { column } from '@neovici/cosmoz-queue/list';

const myColumn = column({
// Optional ordering hint
order: 1,

// Optional sort key
sort: 'name',

// Optional filter transform: receives the raw filter value,
// returns the value sent to the API
filter: (value: string) => ({ name: value }),

// Render function -- receives { name } where name is the object key
render: ({ name }) =>
html``,
});
```

### `useListCore()` / `useListCoreState()`

For cases where you need more control over the list without `renderListCore()`:

- **`useListCore(props)`** -- manages columns, params, data fetching, pagination, and form dialog state. Returns `{ data$, columns, loadMore, dialog, open, ...state }`.
- **`useListCoreState(defaults?)`** -- lower-level state: `filters`, `sortOn`, `descending`, `groupOn`, `selectedItems`, plus their setters and `setTotalAvailable`.

## Actions module

The `@neovici/cosmoz-queue/actions` entry point provides a declarative system for defining user actions that open form dialogs.

### Defining an action

```ts
import { action, Action, defaultButton } from '@neovici/cosmoz-queue/actions';
import { t } from 'i18next';
import { when } from 'lit-html/directives/when.js';

// action() is an identity function that provides type inference
export const approveOrder = action({
// Label shown on the button
title: () => t('Approve'),

// Optional: filter which items this action applies to.
// If provided, non-applicable items are excluded and the button
// shows a count badge like "Approve (3/5)".
applicable: (item) => item.status === 'pending',

// Optional: custom button renderer. Defaults to `defaultButton()`.
// Return `nothing` to hide the button conditionally.
button: (opts) =>
when(opts.items.length >= 1, () => defaultButton(opts)),

// Dialog configuration. Opens a `cosmoz-form` dialog.
// `items` contains only the applicable items.
dialog: ({ items, title }) => ({
heading: title,
description: t('Approve the selected orders'),
fields: [
// cosmoz-form field definitions
],
initial: {},
onSave: async (values) => {
await approveOrders(items.map((i) => i.id), values);
},
}),
});
```

### Rendering actions

Actions are rendered automatically by `listCore()` when passed as the `actions` prop. For manual rendering (e.g. in a detail view's bottom bar):

```ts
import { renderActions } from '@neovici/cosmoz-queue/actions';
import { approveOrder } from './actions';

// In a view component:
const bottomBar = renderActions({ items: [currentItem], open })([
approveOrder,
]);
```

### Action types

```ts
interface Action {
title: () => string;
applicable?: (item: TItem) => boolean;
button?: (opts: Action & ActionOpts) => unknown;
dialog: (opts: DialogOpts) => Dialogable | Promise;
}

interface ActionOpts {
items: TItem[];
open: (dialog: Dialogable) => void;
slot?: string;
}
```

## Advanced: composing with hooks

When `queue()` is too opinionated, use the individual hooks directly.

### `useQueue()` + `renderQueue()`

```ts
import { useQueue, renderQueue, renderNav } from '@neovici/cosmoz-queue';

const MyQueue = ({ heading }: { heading: string }) => {
const {
index, mobile, tabnav, items, setItems,
setSelected, setTotalAvailable, totalAvailable,
onItemClick, nav,
} = useQueue({
idHashParam: 'id',
tabHashParam: 'tab',
});

return renderQueue({
heading,
mobile,
index,
items,
tabnav,
totalAvailable,
nav,
list: html``,
renderItem: ({ item, nav }) =>
html`${nav}`,
renderLoader: ({ item, nav }) =>
html`${nav}`,
});
};
```

### Individual hooks

| Hook | Import | Purpose |
|---|---|---|
| `useTabs({ items, hashParam, mobile, fallback })` | `@neovici/cosmoz-queue` | Manages overview/split/queue tabs. Returns `{ tabnav, activeTab }`. |
| `useDataNav(items, opts)` | `@neovici/cosmoz-queue` | Item navigation with prev/next, URL hash sync. Returns `{ item, setItem, next, prev, forward, index }`. |
| `useSplit({ activeTab, ...splitOpts })` | `@neovici/cosmoz-queue` | Initializes Split.js when in split mode. |
| `useListState()` | `@neovici/cosmoz-queue` | Creates `items`, `selected`, `totalAvailable` state with setters. |
| `useListSSE({ entity, params, list$ })` | `@neovici/cosmoz-queue` | Subscribes to Server-Sent Events (`cosmoz-${entity}-updated`) for real-time list updates. |
| `useFetchActions({ pathLocator, selected, api })` | `@neovici/cosmoz-queue` | Fetches available actions for selected items from an API. Returns `{ actions, actionRows, actionsFetching }`. |
| `useAsyncAction(nav)` | `@neovici/cosmoz-queue` | Handles async action completion with automatic item removal from the list. Returns `{ listRef, onAsyncSimpleAction }`. |
| `usePagination()` | `@neovici/cosmoz-queue` | URL hash-based page state. Returns `{ page, onPage }`. |

## Utilities

### Fetch helpers (`@neovici/cosmoz-queue/util/fetch`)

Pre-configured `fetch` wrapper with CORS and credentials:

```ts
import {
fetch,
setBaseInit,
handleJSON,
RequestError,
} from '@neovici/cosmoz-queue/util/fetch';

// Configure default headers (call once at app startup)
setBaseInit({
headers: { 'X-Custom-Header': 'value' },
// Or use dynamic headers:
getHeaders: () => ({ Authorization: `Bearer ${getToken()}` }),
});

// fetch() includes mode: 'cors', credentials: 'include' by default
const response = await fetch('/api/orders');
const data = await handleJSON(response);
```

`RequestError` extends `Error` with `.response` and `.data` properties for structured error handling.

### `itemClick()`

Makes list cells clickable, dispatching `omnitable-item-click` events that the queue listens for:

```ts
import { itemClick } from '@neovici/cosmoz-queue';

// In an omnitable cell renderer:
html`
${item.title}
`;
```

The `activate` option specifies which tab to switch to. The queue picks the first non-disabled tab from the array.

### Other utilities

| Function | Description |
|---|---|
| `getItems(items, selected)` | Returns `selected` if non-empty, otherwise `items` |
| `touch(list, id)` | Forces an omnitable item refresh by replacing it with a shallow copy |

## Architecture

```
queue() factory
|
+---> useQueue() State orchestration
| |
| +---> useListState() items, selected, totalAvailable
| +---> useTabs() overview | split | queue
| +---> useDataNav() current item, prev/next, URL hash
| +---> useKeyNav() arrow key navigation
| +---> useSplit() Split.js initialization
| +---> useUpdates() list-item-remove events
|
+---> useAsyncAction() Post-action item removal
|
+---> renderQueue() Template composition
|
+---> cosmoz-tabs-next Tab bar (List / Split / Queue)
+---> renderStats() "3-5 of 120"
+---> renderPagination() Page prev/next
+--->
+---> list cosmoz-omnitable (user-provided)
+---> cosmoz-slider Animated detail view
+---> renderSlide() --> renderView()
|
+---> details() Async fetch
+---> renderItem or renderLoader
```

## Entry points

| Import path | Description |
|---|---|
| `@neovici/cosmoz-queue` | Main: `queue()`, `useQueue()`, `renderQueue()`, navigation hooks, SSE, utilities |
| `@neovici/cosmoz-queue/actions` | `action()`, `renderActions()`, `defaultButton()`, `actionCount()` |
| `@neovici/cosmoz-queue/list` | `listCore()`, `column()`, `useListCore()`, `useListCoreState()`, `renderListCore()` |
| `@neovici/cosmoz-queue/list/more` | `useMore()` -- progressive "load more" pagination |
| `@neovici/cosmoz-queue/list/more/render` | `renderLoadMore()` -- "Load more" button |
| `@neovici/cosmoz-queue/util/fetch` | `fetch()`, `setBaseInit()`, `handleJSON()`, `RequestError` |

## Deprecation notices

### `api` property → `details`

The `api` property on `useQueue()` / `queue()` is **deprecated** and will be
removed in v2.0.0. Use `details` instead:

```ts
// Before (deprecated)
queue({ api: (id, item) => apiUrl(`items/${id}`) })

// After
queue({ details: (item) => fetch(apiUrl(`items/${item.id}`)).then(r => r.json()) })
```

See [Migration guide](docs/migration-api-to-details.md) for more patterns.

## License

[Apache-2.0](LICENSE)