npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@object-ui/plugin-list

v17.6.0

Published

ListView plugin for Object UI - unified view component with view type switching

Readme

@object-ui/plugin-list

ListView plugin for ObjectUI - A unified view component with view type switching, filtering, sorting, and view configuration persistence.

Features

  • View Type Switching: Switch between Grid, List, Kanban, Calendar, and Chart views
  • View Persistence: Automatically saves user's view preference
  • Integrated Search: Full-text search across records
  • Filtering: Advanced filter UI (expandable filter panel)
  • Sorting: Sort by any field, toggle ascending/descending
  • Flexible Configuration: Configure available view types per object
  • Custom Templates: Support for custom view options per view type

Visual density defaults (renderer-only, metadata always wins)

The toolbar and cell renderers are tuned for low visual noise on dense tables:

  • Unified toolbar row: view tabs (schema.tabs), user filters and tool buttons share a single bordered row. The previous stacked rows (tabs / description / toolbar) are collapsed into one separator line.
  • Flat user-filter pills: userFilters (dropdown mode) render as ghost text + count. Active state is shown via text-foreground font-medium rather than a filled / bordered pill.
  • Quiet active state for tool buttons: filter / group / sort / color / density / search no longer paint a bg-primary/10 border block when active — they switch to text-foreground font-medium and rely on the trailing count for emphasis.
  • Dot-style select/status cells (opt-in): the cell renderer supports appearance: 'dot' to render ● label instead of a filled badge for high-density tables. This is opt-in — by default select/status cells render as filled badges in both list and detail views, keeping visual consistency across views. Set appearance: 'dot' on the field (or column) in metadata when you want the lighter style.

Installation

pnpm add @object-ui/plugin-list

Usage

Basic Example

import { ListView } from '@object-ui/plugin-list';

function ContactsView() {
  return (
    <ListView
      schema={{
        type: 'list-view',
        objectName: 'contacts',
        viewType: 'grid',
        columns: ['name', 'email', 'phone', 'company'],
        sort: [{ field: 'name', order: 'asc' }],
      }}
    />
  );
}

Grouping Records (Airtable-style)

Group rows in grid/gallery views by one or more fields. Two equivalent shapes are supported on the schema:

// Spec-compliant: structured GroupingConfig (multi-level + per-field options)
<ListView
  schema={{
    type: 'list-view',
    objectName: 'tasks',
    viewType: 'grid',
    columns: ['title', 'status', 'assignee'],
    grouping: {
      fields: [
        { field: 'status', order: 'asc', collapsed: false },
        { field: 'assignee', order: 'asc', collapsed: true },
      ],
    },
  }}
/>

// Shorthand: a single field name (used by the visual view-config UI).
// Internally normalized into the GroupingConfig above.
<ListView
  schema={{
    type: 'list-view',
    objectName: 'tasks',
    viewType: 'grid',
    columns: ['title', 'status'],
    groupBy: 'status',
  }}
/>

When both are present, grouping wins. End users can also add or remove grouping fields at runtime via the Group toolbar button.

With Multiple View Types

<ListView
  schema={{
    type: 'list-view',
    objectName: 'deals',
    viewType: 'kanban',
    columns: ['name', 'amount', 'stage', 'close_date'],
    options: {
      kanban: {
        groupField: 'stage',
        titleField: 'name',
      },
      calendar: {
        startDateField: 'close_date',
        titleField: 'name',
      },
      chart: {
        chartType: 'bar',
        xAxisField: 'stage',
        yAxisFields: ['amount'],
      }
    }
  }}
/>

With Callbacks

<ListView
  schema={{
    type: 'list-view',
    objectName: 'tasks',
    columns: ['title', 'status', 'priority'],
  }}
  onViewChange={(view) => console.log('View changed to:', view)}
  onSearchChange={(search) => console.log('Search:', search)}
  onSortChange={(sort) => console.log('Sort:', sort)}
  onFilterChange={(filters) => console.log('Filters:', filters)}
/>

Schema

The ListView component accepts a ListViewSchema:

interface ListViewSchema {
  type: 'list-view';
  objectName: string;
  viewType?: 'grid' | 'kanban' | 'calendar' | 'gantt' | 'map' | 'chart';
  /** Spec-canonical column list. The legacy `fields` alias is still accepted
   *  on input (stored view metadata carries it) and folded into `columns` by
   *  `normalizeListViewSchema` — but nothing reads it, so emit `columns`. */
  columns?: string[] | ListColumn[];
  filters?: Array<any[] | string>;
  sort?: Array<{ field: string; order: 'asc' | 'desc' }>;
  options?: {
    grid?: Record<string, any>;
    list?: Record<string, any>;
    kanban?: {
      groupField: string;
      titleField?: string;
      cardFields?: string[];
    };
    calendar?: {
      startDateField: string;
      endDateField?: string;
      titleField: string;
    };
    chart?: {
      chartType: 'bar' | 'line' | 'pie' | 'area';
      xAxisField: string;
      yAxisFields: string[];
    };
  };
}

Page binding — dataSource (referencing a saved view by name)

On a metadata page, a list-view component can bind its data through the spec's per-element data source (PageComponentSchema.dataSource, ElementDataSourceSchema) instead of spelling objectName and inlining the view's configuration:

{
  "type": "list-view",
  "dataSource": { "object": "account", "view": "hot", "limit": 10 }
}

view names a saved view of that object — either one embedded in the object definition (listViews) or one created in the UI (the metadata overlay an adapter serves from listViews()). Its columns, filter, sort, page size and view kind are applied to the render, so a page no longer has to keep a second copy of a view's configuration in sync with the view itself. Both the short key (hot) and the qualified id (account.hot) resolve.

Precedence. dataSource.* keys are authoritative — the author wrote them on this placement, and they beat the component's own same-named key. Values that come from the named view are a baseline: a key written on the component itself is more specific than the view it points at, so the component's key wins (an empty columns: [] counts as "not authored"). filter is the exception — the spec calls dataSource.filter "additional filter criteria", so the component's filter, the view's filter and the binding's filter all AND together. A binding can narrow what the view selects, never widen it.

An unresolvable view is an error, not an empty table. If the named view does not exist, the block renders a configuration error listing the object's actual views, and issues no query. It deliberately does not fall back to the object's default view: that would turn a typo into a silently wider answer on a page that still looks like it works.

Sorting (and why relational columns are not offered)

The toolbar sort becomes a server $orderby on the flat field name, so the sort key is whatever that field stores. For a relational field (lookup / master_detail / user / tree) that is the foreign-key id, while the column shows the related record's name — the rows would come back in an order with no visible relation to the column ("sorting is broken", from the user's side). The server cannot order by the related name without a join, and objectstack#4256 settled that it will not add one.

So the sort picker withholds relational fields and says so. To sort by a related record's name, denormalize that name onto this object as a stored field, written when the source changes, then sort by that field — the remedy the server's own refusal prescribes, in the same words the sort panel's hint uses.

Not a formula field. A formula value is computed on read, so no driver materializes a column behind it, and since objectstack#6994 the server answers a sort that names one with a hard 400 INVALID_SORT (before that it degraded silently: the rows came back in an arbitrary order under a 200, asc and desc identical). The picker withholds formula fields for the same reason (objectui#4243), so there is no formula column here to sort "like any other text column" — the column you sort is the stored one the denormalization writes.

A field the view's CURRENT sort already uses stays listed under both rules — relational ones labelled (by ID) — so existing view metadata round-trips instead of silently losing its sort, and a sort the server would refuse can still be edited away in the picker that otherwise hides its field.

Column-header sorting inside the grid follows the same two rules, because a header click is a server $orderby as well whenever the grid is showing one window of a larger collection (objectui#3106) — not a client-side reorder of the loaded rows. So on that path a relational or formula column carries no clickable header either (objectui#3950); offering one would have been the same illusion through a different control.

Where the sort really does stay in the browser — inline data, or the grouped view, which holds every row it groups — both kinds of header stay live and order by the value the cell shows: the resolved label for a relational column, the server-hydrated result for a formula one (see getSortValue in @object-ui/core).

View Persistence

The ListView automatically persists the user's view type preference in localStorage using the key listview-{objectName}-view.

Links

License

MIT — see LICENSE.