Overdue filters, cell labels and a row count

Enterprise filter and sort additions on a review queue. Due's filter menu offers Today, Before today, Next 7 days and the rest, with nothing to type; Status is stored as codes and its checklist reads Approved and Failed, as the cells do; the status bar counts rows, "6 of 24 rows" under a filter; and reviews with no due date stay last whichever way Due sorts. (requires @svgrid/enterprise)

A live, editable Svelte 5 data grid example from the SvGrid gallery (Misc). See the SvGrid documentation for the full API.

About this example

A review queue in a Svelte 5 data grid with the Enterprise filter additions: Today, Before today and Next 7 days on a date column with nothing to type, a Status checklist that reads as the cells do while the data stays as codes, a row count in the status bar, and rows with no due date held last in either sort direction.

Four @svgrid/enterprise additions to the grid's filtering and sorting, each switched on once and then used through ordinary column options:

enableRelativeDateFilters() Due's filter menu gains Today, Before today, After today, Next 7 days and Last 7 days. No value to type: pick one and it filters. A server data source receives the dates they mean, not the operator. enableFilterLabels() Status is stored as APPROVED / MARKED_AS_FAILED; its filter checklist lists "Approved" and "Failed", as the cells show them. enableRowCount() statusBar={{ rows: true }} shows "24 rows", or "6 of 24 rows" under a filter. sortComparator: blanksLast() Reviews with no due date stay at the bottom whichever way Due sorts.

The accessor of every column stays the raw value, so Reviews sorts 0, 1, 2, 10 and Due sorts by date, while the cells and the checklist read as words.

Imports, features and API used

Imports: @svgrid/grid, @svgrid/enterprise

Columns: name (Artifact), version (Version), status (Status), reviews (Reviews), reviewer (Reviewer), due (Due)

SvGridApi methods called: api.setFilter()

Frequently asked questions

What does Before today compare against?

The user's local calendar day, read when the filter is applied. A bare YYYY-MM-DD value counts as that local day, not UTC midnight, and a blank cell never matches.

Does a server data source need to understand the new operators?

No. The grid sends the dates they stand for: Before today goes out as lessThan today's date and Next 7 days as between today and a week out. With externalFilter, onFiltersChange keeps the operator, and resolveRelativeDateFilter turns it into dates when you build the query.

Why does the Status checklist show Approved when the data says APPROVED?

enableFilterLabels lists each value the way its cell renders it, taken from a cell renderer that returns text, static editorOptions labels, or the column's formatter. The filter still matches the stored value.

Which license does it need?

These are part of the Grid edition of @svgrid/enterprise. Without a key they run with a watermark, so the demo can be evaluated as is.

Related documentation

Related articles

  • Status Badge Cells in SvGrid - Turn a plain text status column into colored, accessible pill badges without breaking sort, filter, or virtualization - using SvGrid's snippet cell renderer.
  • Sparkline Cells in a Svelte Data Grid - Show inline trend sparklines inside grid cells using SvGrid's built-in sparkline column property - no charting library needed, just a field that holds a number array.
  • Saved Views - Persist Grid Layout and Filters - Give users named, switchable snapshots of column order, sorting, filters, and grouping - persisted to localStorage or a server adapter - using SvGrid's createNamedViews API.

Source code (701-enterprise-grid-filters.svelte)

<script lang="ts">
  /**
   * 701. Review queue: overdue, labels and a row count
   * ---------------------------------------------------
   * Four @svgrid/enterprise additions to the grid's filtering and sorting,
   * each switched on once and then used through ordinary column options:
   *
   *   enableRelativeDateFilters()   Due's filter menu gains Today, Before
   *                                 today, After today, Next 7 days and Last
   *                                 7 days. No value to type: pick one and it
   *                                 filters. A server data source receives
   *                                 the dates they mean, not the operator.
   *   enableFilterLabels()          Status is stored as APPROVED /
   *                                 MARKED_AS_FAILED; its filter checklist
   *                                 lists "Approved" and "Failed", as the
   *                                 cells show them.
   *   enableRowCount()              statusBar={{ rows: true }} shows "24
   *                                 rows", or "6 of 24 rows" under a filter.
   *   sortComparator: blanksLast()  Reviews with no due date stay at the
   *                                 bottom whichever way Due sorts.
   *
   * The accessor of every column stays the raw value, so Reviews sorts
   * 0, 1, 2, 10 and Due sorts by date, while the cells and the checklist read
   * as words.
   */
  import { SvGrid, type GridColumns, type SvGridApi } from '@svgrid/grid'
  import {
    blanksLast,
    enableFilterLabels,
    enableRelativeDateFilters,
    enableRowCount,
  } from '@svgrid/enterprise'

  enableRelativeDateFilters()
  enableFilterLabels()
  enableRowCount()

  type Status = 'APPROVED' | 'MARKED_AS_FAILED' | 'UNDECIDED'
  type Review = {
    id: number
    name: string
    version: string
    status: Status
    reviews: number
    reviewer: string
    due: string | null
  }

  const STATUS_TEXT: Record<Status, string> = {
    APPROVED: 'Approved',
    MARKED_AS_FAILED: 'Failed',
    UNDECIDED: 'Undecided',
  }
  const STATUSES: Status[] = ['UNDECIDED', 'APPROVED', 'UNDECIDED', 'MARKED_AS_FAILED']
  const NAMES = [
    'kernel-image', 'web-gateway', 'postgres-operator', 'metrics-agent', 'auth-service',
    'snap-store-proxy', 'log-shipper', 'dns-resolver', 'object-store', 'build-runner',
    'queue-broker', 'edge-cache',
  ]
  const REVIEWERS = ['A. Osei', 'R. Vance', 'M. Iqbal', 'J. Lindqvist', '']

  // Due dates are relative to today so Overdue and Next 7 days always have
  // something to show. Every fifth review has no due date.
  const OFFSETS = [-9, -2, 0, 3, 6, 12, -1, 5, 21, 1, -15, 8]
  const isoFromToday = (days: number) => {
    const d = new Date()
    d.setDate(d.getDate() + days)
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
  }

  const reviews: Review[] = Array.from({ length: 24 }, (_, i) => ({
    id: i + 1,
    name: NAMES[i % NAMES.length],
    version: `${1 + (i % 3)}.${(i * 7) % 12}.${i % 5}`,
    status: STATUSES[(i * 5) % STATUSES.length],
    // 0 through 12, so a text sort would put 10 before 2.
    reviews: (i * 7) % 13,
    reviewer: REVIEWERS[i % REVIEWERS.length],
    due: i % 5 === 4 ? null : isoFromToday(OFFSETS[i % OFFSETS.length]),
  }))

  const columns: GridColumns<Review> = [
    { field: 'name', header: 'Artifact', width: 190 },
    { field: 'version', header: 'Version', width: 100 },
    {
      field: 'status',
      header: 'Status',
      width: 130,
      cell: ({ getValue }) => STATUS_TEXT[getValue() as Status] ?? '',
    },
    { field: 'reviews', header: 'Reviews', width: 100, cellDataType: 'number' },
    { field: 'reviewer', header: 'Reviewer', width: 140 },
    {
      field: 'due',
      header: 'Due',
      width: 150,
      cellDataType: 'dateString',
      format: { type: 'date', options: { dateStyle: 'medium' } },
      sortComparator: blanksLast('date'),
    },
  ]

  let api = $state<SvGridApi<{}, Review> | null>(null)
  let preset = $state<'all' | 'beforeToday' | 'today' | 'next7Days'>('all')

  const PRESETS = [
    { id: 'all', label: 'All' },
    { id: 'beforeToday', label: 'Overdue' },
    { id: 'today', label: 'Due today' },
    { id: 'next7Days', label: 'Next 7 days' },
  ] as const

  // The same operators the Due column's filter menu offers.
  function pick(id: (typeof PRESETS)[number]['id']) {
    preset = id
    api?.setFilter('due', id === 'all' ? null : { operator: id })
  }
</script>

<section class="wrap">
  <header class="chrome">
    <p class="hint">
      Open the filter on <strong>Due</strong> for the days-from-today operators, or on
      <strong>Status</strong> for a checklist that reads as the cells do. Sort Due either way:
      reviews with no date stay last.
    </p>
    <div class="seg" role="group" aria-label="Due date">
      {#each PRESETS as p (p.id)}
        <button class:on={preset === p.id} aria-pressed={preset === p.id} onclick={() => pick(p.id)}>
          {p.label}
        </button>
      {/each}
    </div>
  </header>

  <div class="grid-host">
    <SvGrid
      responsive={true}
      data={reviews}
      {columns}
      getRowId={(r) => String(r.id)}
      sortable
      filterable
      filterMode="menu"
      initialSorting={[{ id: 'due', desc: false }]}
      statusBar={{ rows: true, aggregates: [] }}
      rowHeight={36}
      containerHeight="100%"
      fitColumns
      onApiReady={(next) => (api = next)}
      onFiltersChange={(f) => {
        // Due set from its menu to something the buttons do not name: no button is on.
        const op = f.columns.find((c) => c.id === 'due')?.operator
        if (!PRESETS.some((p) => p.id === op)) preset = 'all'
      }}
    />
  </div>
</section>

<style>
  .wrap {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    gap: 12px;
  }

  .chrome {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    flex: none;
  }
  .hint {
    margin: 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--sg-muted, #64748b);
    max-width: 62ch;
  }
  .hint strong { color: var(--sg-fg, #0f172a); font-weight: 600; }

  .seg {
    display: inline-flex;
    border: 1px solid var(--sg-border, #cbd5e1);
    border-radius: 8px;
    overflow: hidden;
  }
  .seg button {
    border: 0;
    padding: 6px 12px;
    font: inherit;
    font-size: 13px;
    background: var(--sg-bg, #fff);
    color: var(--sg-muted, #64748b);
    cursor: pointer;
  }
  .seg button + button { border-left: 1px solid var(--sg-border, #cbd5e1); }
  .seg button.on { background: var(--sg-accent, #6366f1); color: var(--sg-on-accent, #fff); }
  .seg button:focus-visible { outline: 2px solid var(--sg-focus-ring, #6366f1); outline-offset: -2px; }

  .grid-host {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
</style>

View this example on GitHub

More Misc examples

  • Bulk-action bar - selectionBar floats a bar over the grid while rows are selected - the count, your actions, an overflow menu, and Clear - the pattern an issue tracker uses for bulk edit. position pins it to the bottom (default) or top edge. Actions receive the selected rows in display order, and hidden / disabled re-check against the live selection so buttons appear and grey out as the set changes.
  • Staged / batch editing - Edits buffer into a draft; user reviews every change in a side panel, then commits the batch (one server roundtrip) or reverts back to originals.