Support ticket log: AutoFilter
Forty support tickets with Excel's Filter on the header row, opened already filtered to what is still open: the funnel on Status, blue row numbers, "N of 40 records found" in the status bar. The arrows drop Excel's menu: sort, Clear Filter, Text and Number Filters with two conditions, a search box, (Select All) and the values with counts. The rows are worked out again after every edit, so a ticket typed Closed folds away at once. Ctrl+Shift+L toggles it. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Spreadsheet). See the SvGrid documentation for the full API.
About this example
A support ticket log on the Svelte 5 spreadsheet shell with Excel's AutoFilter on the header row, opened already filtered to the tickets still open: the funnel on the Status arrow, blue row numbers over the region and "N of 40 records found" in the status bar. Each arrow drops Excel's menu with Sort A to Z and Z to A, Clear Filter From the column, Text Filters or Number Filters with two conditions joined by And or Or, a search box, (Select All) and the column's values with their counts. The hidden rows are worked out again after every edit, so a ticket typed Closed folds away at once, and Ctrl+Shift+L turns the filter off and on. Enterprise, in @svgrid/enterprise.
The log a support lead keeps open, with Excel's Filter on it. Forty tickets; the arrows on the header row drop Excel's menu, and the document opens already filtered to what is still open:
Values untick Closed under Status and the closed tickets fold away; the arrow turns into a funnel, the row numbers of the region turn blue and the status bar counts "N of 40 records found". A folded row is the filter's, not a hidden one: Unhide leaves it where it is. Conditions Number Filters on Hours open (greater than 48), Text Filters on Customer (begins with), two of them joined with And / Or. Search type in the box and the list narrows; OK applies what it shows. Sort A to Z and Z to A from the menu keep the header row put. Live the rows are worked out again after every edit: type Closed into an open ticket and it folds away at once, the way Excel's Reapply would.
Ctrl+Shift+L toggles the whole thing; the filter is part of the document and comes back from getState() with the rows it hides.
Try: open the arrow on Owner and keep only Priya, then Number Filters on Hours open, greater than 48: the tickets that need chasing. Clear Filter From "Owner" widens it again; Ctrl+Shift+L twice resets.
Imports, features and API used
Imports: @svgrid/enterprise
Frequently asked questions
How is a filtered row different from a hidden one?
A filtered row belongs to the filter: Unhide leaves it folded, getState().hidden leaves it out, and turning the filter off shows it while a row hidden by hand stays hidden.
Does the filter follow edits?
Yes. The rows are re-evaluated after every change, the way Excel's Reapply would, so a formula that drops out of a Number Filter folds away and a row that comes back returns.
Where does the filter get its operators?
From the grid's own Excel-filter compiler in @svgrid/grid/filtering: equals, does not equal, begins with, contains, greater than, between and the rest, numeric operators on the value and text operators on the display.
Related documentation
Related articles
- A Fill Handle (Drag to Fill) in SvGrid - Build a working spreadsheet-style fill handle on top of SvGrid's cell selection and editing - pointer tracking, range highlighting, series fill, and undo/redo integration all covered.
- Pivot Tables in Svelte - Summarize Data Without a Spreadsheet - Run a cross-tab pivot directly inside your Svelte app using @svgrid/enterprise createPivotModel - no Excel, no server-side aggregation, no stale exports.
- Spreadsheet-Style Cell Range Selection in SvGrid - How to enable drag-to-select cell ranges, read live selection state, and build a status-bar footer that sums and averages the selected values.
Source code (464-ticket-log-autofilter.svelte)
<script lang="ts">
/**
* 464. Support ticket log: AutoFilter
* -----------------------------------
* The log a support lead keeps open, with Excel's Filter on it. Forty
* tickets; the arrows on the header row drop Excel's menu, and the
* document opens already filtered to what is still open:
*
* Values untick Closed under Status and the closed tickets fold
* away; the arrow turns into a funnel, the row numbers of
* the region turn blue and the status bar counts "N of 40
* records found". A folded row is the filter's, not a
* hidden one: Unhide leaves it where it is.
* Conditions Number Filters on Hours open (greater than 48), Text
* Filters on Customer (begins with), two of them joined
* with And / Or.
* Search type in the box and the list narrows; OK applies what
* it shows.
* Sort A to Z and Z to A from the menu keep the header row put.
* Live the rows are worked out again after every edit: type
* Closed into an open ticket and it folds away at once,
* the way Excel's Reapply would.
*
* Ctrl+Shift+L toggles the whole thing; the filter is part of the
* document and comes back from getState() with the rows it hides.
*
* Try: open the arrow on Owner and keep only Priya, then Number Filters
* on Hours open, greater than 48: the tickets that need chasing. Clear
* Filter From "Owner" widens it again; Ctrl+Shift+L twice resets.
*/
import { SvSheet, createWorkbook, createSheetDocument, type CellFormatEntry } from '@svgrid/enterprise'
const CUSTOMERS = ['Acme Foods', 'Borealis', 'Cobalt Labs', 'Delta Freight', 'Evergreen', 'Fjord Media', 'Granite Bank', 'Helix Health']
const OWNERS = ['Priya', 'Marco', 'Lena', 'Tom']
const PRIORITIES = ['P1', 'P2', 'P3']
const STATUSES = ['Open', 'Pending', 'Closed']
const SUBJECTS = [
'Login loop after password reset', 'Export stops at 10,000 rows', 'Invoice PDF shows the wrong currency',
'Webhook retries every minute', 'Dashboard blank on Safari', 'SSO group mapping ignored',
'Duplicate notifications', 'Date column off by one day', 'Cannot delete an archived project',
'API rate limit lower than documented',
]
// A fixed sequence, so the demo reads the same every time it opens.
let seed = 7
function pick<T>(list: readonly T[]): T { seed = (seed * 9301 + 49297) % 233280; return list[Math.floor((seed / 233280) * list.length)]! }
const between = (lo: number, hi: number) => { seed = (seed * 9301 + 49297) % 233280; return lo + Math.floor((seed / 233280) * (hi - lo + 1)) }
const N = 40
const tickets = Array.from({ length: N }, (_, i) => {
const day = 1 + Math.floor(i / 2)
const status = i < 26 ? pick(STATUSES) : pick(['Open', 'Pending']) // the newest are still open
const hours = status === 'Closed' ? between(2, 60) : between(1, 140)
const csat = status === 'Closed' ? String(between(2, 5)) : ''
return [`T-${1041 + i}`, `2026-09-${String(day).padStart(2, '0')}`, pick(CUSTOMERS), pick(SUBJECTS), pick(PRIORITIES), status, pick(OWNERS), String(hours), csat]
})
const cells = [
['Ticket', 'Opened', 'Customer', 'Subject', 'Priority', 'Status', 'Owner', 'Hours open', 'CSAT'],
...tickets,
[],
['Open', `=COUNTIF(F2:F${N + 1},"Open")`, 'Pending', `=COUNTIF(F2:F${N + 1},"Pending")`, 'Closed', `=COUNTIF(F2:F${N + 1},"Closed")`, 'Avg CSAT', `=AVERAGE(I2:I${N + 1})`],
]
const wb = createWorkbook([{ name: 'Tickets', cells }])
const doc = createSheetDocument({ workbook: wb })
const sheet = doc.get('Tickets')
// The AutoFilter, already applied: Status without Closed. The range is
// the header row plus the forty tickets; filters are keyed by column.
sheet.autoFilter = { range: [0, 0, N, 8], filters: { 5: { kind: 'values', excluded: ['Closed'] } } }
sheet.validation = [
{ id: 'status', rects: [[1, 5, N, 5]], allow: 'list', value1: STATUSES.join(','), ignoreBlank: true, inCellDropdown: true, alert: { style: 'stop' } },
{ id: 'priority', rects: [[1, 4, N, 4]], allow: 'list', value1: PRIORITIES.join(','), ignoreBlank: true, inCellDropdown: true, alert: { style: 'stop' } },
]
sheet.conditionalFormats = [
{ id: 'p1', rects: [[1, 4, N, 4]], kind: 'text', match: 'contains', value: 'P1', style: { fill: '#FFC7CE', color: '#9C0006' } },
{ id: 'old', rects: [[1, 7, N, 7]], kind: 'cellIs', operator: 'greater', value1: '48', style: { color: '#9C0006', bold: true } },
]
sheet.freeze = { rows: 1, cols: 0 }
const BAND = { bold: true, fill: '#e2e8f0', color: '#0f172a' } as const
type Entry = Record<string, CellFormatEntry>
const across = (cols: string, row: number, entry: CellFormatEntry): Entry =>
Object.fromEntries([...cols].map((c) => [`${c}${row}`, entry]))
const formats: Entry = {
...across('ABCDEFGHI', 1, BAND),
...across('ACEG', N + 3, { bold: true }),
[`H${N + 3}`]: { numFmt: '0.0' },
}
</script>
<section class="wrap flex flex-col flex-1 min-h-0">
<SvSheet document={doc} height="100%" rows={N + 4} columns={10} columnWidths={{ A: 80, B: 100, C: 120, D: 230, E: 80, F: 90, G: 80, H: 100, I: 70 }} {formats} />
<p class="note shrink-0">
The log opens filtered to the tickets still open: the funnel on
<strong>Status</strong>, the blue row numbers and the status bar's
"records found" say so. Open the arrow on <strong>Owner</strong> and keep
one name, add <strong>Number Filters > Greater Than 48</strong> on
Hours open, or type Closed into an open ticket and watch it fold away.
Ctrl+Shift+L turns the filter off and on.
</p>
</section>
<style>
.wrap { gap: 8px; }
.note { margin: 0; font-size: 13px; line-height: 1.6; color: var(--sg-muted, #64748b); }
</style>More Spreadsheet examples
- Spreadsheet + Ribbon bar - The whole Excel surface as one component, <SvSheet workbook={wb} />: a six-tab ribbon (Home, Insert, Formulas, Data, Review, View), the Name Box and fx bar, sheet tabs and the Sum / Average / Count status bar. Format cells, merge them, comment on them, validate what goes in, colour them by rule, filter the region, protect the sheet; every button is the same call as its shortcut, so Ctrl+B and the Bold button cannot drift. A two-sheet P&L with the formats travelling in the document.
- Spreadsheet + formulas - Real formula engine inside the grid: cell refs (A1), ranges (A1:A10), SUM / AVG / IF / COUNTIF / ROUND, arithmetic, string concat, cycle detection.
- Per-cell custom borders (KPI) - Editable KPI scorecard. spreadsheetLayout paints spreadsheet-style per-edge custom borders via an absolute-positioned overlay (no border-collapse conflicts). Edit any quarter or target - the borders re-derive: green double = beat target, blue solid = hit, amber dotted = near miss, red dashed = bad miss; row champion gets a colored full frame.
- Cell merging (spreadsheet shell) - A real invoice rendered on an Excel-style shell: A / B / C / D / E column letters across the top, row numbers down the left. Brand band, bill-from / bill-to address blocks, meta block, line items, totals, notes, signatures - all assembled from MergeSpec + CellBorderSpec. Editable Qty / Rate / addresses / notes; totals recompute live.
- HyperFormula integration - Full HyperFormula engine wired into the grid as a peer-optional dep. Editable spreadsheet with A1-style cell refs, dozens of formulas across math (SUM / SUMIF), lookup (VLOOKUP / INDEX-MATCH), text (CONCAT / UPPER), date (TODAY / DATEDIF), logical (IF nests), financial (PMT / IRR / NPV), statistical (AVERAGE / MAX / RANK).