Skip to content

Neovici/cosmoz-queue

Repository files navigation

@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 and lit-html.

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

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.

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<OrderListItem>({
    // 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`<order-list-core ${spread(thru)} ${ref(onRef)}></order-list-core>`,

    // 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`<order-view-core ${spread(thru)}>${nav}</order-view-core>`,

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

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<D> -- 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():

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`<cosmoz-omnitable-column
              name="${name}"
              title=${t('Title')}
              flex="2"
              .renderCell=${(
                _col: unknown,
                { item, index }: { item: OrderListItem; index: number },
              ) =>
                html`<a
                  @click="${itemClick({
                    index,
                    activate: ['split', 'queue'],
                  })}"
                  >${item.title}</a
                >`}
            ></cosmoz-omnitable-column>`,
        }),

        status: column({
          render: ({ name }) =>
            html`<cosmoz-omnitable-column-autocomplete
              name="${name}"
              title=${t('Status')}
              value-path="status.name"
            ></cosmoz-omnitable-column-autocomplete>`,
        }),

        createdAt: column({
          render: ({ name }) =>
            html`<cosmoz-omnitable-column-date
              name="${name}"
              title=${t('Date created')}
            ></cosmoz-omnitable-column-date>`,
        }),
      }),
      [], // 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:

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`<cosmoz-omnitable-column
      name="${name}"
      title="Name"
    ></cosmoz-omnitable-column>`,
});

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

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<OrderItem, ApproveFields>({
  // 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):

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

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

Action types

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

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

Advanced: composing with hooks

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

useQueue() + renderQueue()

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<MyItem>({
    idHashParam: 'id',
    tabHashParam: 'tab',
  });

  return renderQueue({
    heading,
    mobile,
    index,
    items,
    tabnav,
    totalAvailable,
    nav,
    list: html`<my-list
      @visible-items-changed=${updateWith(setItems)}
      @selected-items-changed=${updateWith(setSelected)}
      @total-available-changed=${updateWith(setTotalAvailable)}
      @omnitable-item-click=${onItemClick}
    ></my-list>`,
    renderItem: ({ item, nav }) =>
      html`<my-view .item=${item}>${nav}</my-view>`,
    renderLoader: ({ item, nav }) =>
      html`<my-skeleton .item=${item}>${nav}</my-skeleton>`,
  });
};

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:

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:

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

// In an omnitable cell renderer:
html`<a @click="${itemClick({ index, activate: ['split', 'queue'] })}">
  ${item.title}
</a>`;

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
          +---> <div.split>
                 +---> 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:

// 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 for more patterns.

License

Apache-2.0

About

Cosmoz Queue to manage a list of entitites

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 5