Trash and restore
Deleted records, fields and tables go to the trash with who deleted them and when. Restore puts a record back in its place with its links, a field with its values, a table as a tab; items stay 30 days. This base opens with a deal, a field and a whole table in the trash, deleted by teammates. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Base). See the SvGrid documentation for the full API.
About this example
A trash for an SvGrid Base, in Svelte 5. Deleted records, fields and tables are kept with who deleted them and when, and Restore puts each back: a record in its place with its links, a field with its values, a table as a tab. The trash is plain data bound with bind:trash, so the app stores it, and items older than trashDays are dropped.
Deleted records, fields and tables go to the trash, not away. The trash button at the end of the tab bar says how many items it holds; it lists them newest first with who deleted them and when, and Restore puts one back: a record in its table at the place it had, with its links; a field with its values; a table as a tab. Items stay 30 days (trashDays).
The trash is plain data (bind:trash), so an app stores it with the tables. This one starts with three items other people deleted earlier: a deal, a field and a whole table. Delete something yourself, then use Restore in the notice, or open the trash.
Imports, features and API used
Imports: @svgrid/grid, @svgrid/enterprise/base, ../shared/base-people
Columns: name (Account), industry (Industry), deals (Deals), region (Region), name (Lead), source (Source), email (Email)
Frequently asked questions
Where is the trash kept?
In the trash prop, an array of plain items. Bind it, or listen to onTrashChange, and store it next to the tables. Nothing is sent anywhere by the base itself.
How long do items stay?
30 days by default. Set trashDays to change it, or 0 to keep items until the trash is emptied. Older items are dropped when the base opens.
Can every item be restored?
A record whose table was deleted waits until the table is restored, and the dialog says so. A record or field that is already back cannot be restored twice; the Restore button is off and its title says why.
Related documentation
Related articles
- A Live Svelte Data Grid with Firebase Firestore - Wire Firestore onSnapshot to SvGrid in Svelte 5 - reactive listeners, query-driven sort and filter, cursor pagination, and the subtle cleanup detail most implementations get wrong.
- A Svelte Data Grid with SvelteKit and Supabase - Wire SvGrid to a Supabase Postgres backend with server-side pagination, sorting, and filtering - keeping credentials on the server and queries fast with proper indexing.
- A Svelte Data Grid with PocketBase - Wire SvGrid to PocketBase for server-side paging, sorting, and filtering via getList, plus live row updates through PocketBase realtime subscriptions.
Source code (706-base-trash.svelte)
<script lang="ts">
/**
* 706. SvGrid Base: trash and restore
* -----------------------------------
* Deleted records, fields and tables go to the trash, not away. The trash
* button at the end of the tab bar says how many items it holds; it lists
* them newest first with who deleted them and when, and Restore puts one
* back: a record in its table at the place it had, with its links; a field
* with its values; a table as a tab. Items stay 30 days (`trashDays`).
*
* The trash is plain data (`bind:trash`), so an app stores it with the
* tables. This one starts with three items other people deleted earlier:
* a deal, a field and a whole table. Delete something yourself, then use
* Restore in the notice, or open the trash.
*/
import { SvToaster } from '@svgrid/grid'
import { SvBase, enableBase, type BaseTable, type BaseTrashItem } from '@svgrid/enterprise/base'
import { usePeople } from '../shared/base-people'
enableBase()
usePeople()
const ago = (days: number, hours = 0) => new Date(Date.now() - days * 86_400_000 - hours * 3_600_000).toISOString()
const STAGE = [
{ value: 'Lead', color: '#94a3b8' },
{ value: 'Qualified', color: '#2d7ff9' },
{ value: 'Proposal', color: '#8b46ff' },
{ value: 'Won', color: '#20c933' },
{ value: 'Lost', color: '#f82b60' },
]
const DEALS: Array<[string, string, string, number, string]> = [
['Northwind renewal', 'a1', 'Proposal', 48000, 'u2'],
['Contoso pilot', 'a2', 'Qualified', 12000, 'u1'],
['Fabrikam expansion', 'a3', 'Won', 96000, 'u4'],
['Litware onboarding', 'a4', 'Lead', 8000, 'u3'],
['Northwind support plan', 'a1', 'Lead', 6000, 'u2'],
]
const deals = DEALS.map(([deal, account, stage, amount, owner], i) => ({ id: `d${i + 1}`, deal, account: [account], stage, amount, owner, region: ['EMEA', 'NA', 'NA', 'APAC', 'EMEA'][i] }))
const accounts = [
{ id: 'a1', name: 'Northwind Traders', industry: 'Retail' },
{ id: 'a2', name: 'Contoso', industry: 'Manufacturing' },
{ id: 'a3', name: 'Fabrikam', industry: 'Finance' },
{ id: 'a4', name: 'Litware', industry: 'Software' },
].map((a) => ({ ...a, deals: deals.filter((d) => d.account[0] === a.id).map((d) => d.id) }))
const dealColumns = [
{ field: 'deal', header: 'Deal', width: 240, fieldType: 'text' },
{ field: 'account', header: 'Account', width: 200, fieldType: 'link', fieldProps: { table: 'trash-accounts', multiple: false, inverse: 'deals' } },
{ field: 'stage', header: 'Stage', width: 130, fieldType: 'singleSelect', fieldProps: { options: STAGE } },
{ field: 'amount', header: 'Amount', width: 130, fieldType: 'number', fieldProps: { format: 'currency', symbol: '$', precision: 0 } },
{ field: 'owner', header: 'Owner', width: 160, fieldType: 'user' },
]
let tables = $state.raw<BaseTable[]>([
{
id: 'trash-deals',
name: 'Deals',
primaryField: 'deal',
data: deals,
columns: dealColumns,
views: [{ id: 'pipeline', name: 'Pipeline', type: 'grid', color: { mode: 'field', field: 'stage' } }],
props: { rowAdder: { label: 'Add deal', defaults: () => ({ stage: 'Lead' }) } },
},
{
id: 'trash-accounts',
name: 'Accounts',
primaryField: 'name',
data: accounts,
columns: [
{ field: 'name', header: 'Account', width: 220, fieldType: 'text' },
{ field: 'industry', header: 'Industry', width: 150, fieldType: 'text' },
{ field: 'deals', header: 'Deals', width: 320, fieldType: 'link', fieldProps: { table: 'trash-deals', multiple: true, inverse: 'account' } },
],
views: [{ id: 'all', name: 'All accounts', type: 'grid' }],
},
])
// What other people deleted before you opened the base.
let trash = $state<BaseTrashItem[]>([
{
id: 'seed-deal',
kind: 'record',
table: 'trash-deals',
index: 2,
row: { id: 'd9', deal: 'Adventure Works trial', account: [], stage: 'Lost', amount: 4000, owner: 'u5', region: 'NA' },
deletedAt: ago(0, 3),
deletedBy: 'u5',
},
{
id: 'seed-field',
kind: 'field',
table: 'trash-deals',
index: 5,
column: { field: 'region', header: 'Region', width: 120, fieldType: 'singleSelect', fieldProps: { options: [{ value: 'EMEA', color: '#2d7ff9' }, { value: 'NA', color: '#20c933' }, { value: 'APAC', color: '#ff6f2c' }] } },
deletedAt: ago(1, 2),
deletedBy: 'u2',
},
{
id: 'seed-table',
kind: 'table',
index: 2,
table: {
id: 'trash-leads',
name: 'Old leads',
primaryField: 'name',
columns: [
{ field: 'name', header: 'Lead', width: 220, fieldType: 'text' },
{ field: 'source', header: 'Source', width: 140, fieldType: 'text' },
{ field: 'email', header: 'Email', width: 220, fieldType: 'email' },
],
data: [
{ id: 'l1', name: 'Tailspin Toys', source: 'Webinar', email: '[email protected]' },
{ id: 'l2', name: 'Wide World Importers', source: 'Referral', email: '[email protected]' },
{ id: 'l3', name: 'Proseware', source: 'Trade show', email: '[email protected]' },
],
views: [{ id: 'all', name: 'All leads', type: 'grid' }],
},
deletedAt: ago(5),
deletedBy: 'u4',
},
])
</script>
<section class="base-demo">
<p class="base-demo-tip">
The trash at the top right holds {trash.length} {trash.length === 1 ? 'item' : 'items'}. Open it to restore the deal, the Region field or the Old leads table, or delete
a deal yourself (right-click, Delete record) and press Restore in the notice.
</p>
<SvBase
bind:tables
bind:trash
editable
sortable
showRowNumbers
expandRows
rowNumberWidth={64}
showRowSelection={false}
rowHeight={40}
containerHeight="100%"
selectionBar={['selectAll', 'editFields', 'deleteRows']}
columnAdder
fieldMenu
/>
<SvToaster />
</section>
<style>
.base-demo { display: flex; flex-direction: column; flex: 1; min-height: 480px; height: 100%; gap: 10px; }
.base-demo-tip { margin: 0; font-size: 13px; line-height: 1.5; color: var(--sg-muted, #64748b); }
</style>More Base examples
- Project tracker - A project tracker, ready to use: Tasks and Milestones as two linked tables. Tasks have status, tags, assignee, dates, estimate, progress, budget and files; each milestone lists its tasks with their progress and budget. Views for all tasks, the board by status, due dates on a calendar, a gallery and a request form. Automations close finished tasks, start work from a button and flag overdue ones.
- Content calendar - An editorial content calendar, ready to use: Posts and Campaigns as two linked tables. Posts have stage, channels, author, publish date, cover and brief. Views for the plan, the pipeline as a Kanban, the publishing calendar, a wall of covers and a pitch form.
- Sales CRM - A sales CRM, ready to use: Deals, Accounts and Contacts as three linked tables. Link a deal to an account and the account lists it. Views for all deals, the pipeline as a Kanban, the closing calendar, open deals by owner and a new-lead form.
- Product roadmap - A product roadmap, ready to use: Features and customer Feedback as two linked tables. Features have quarter, status, teams, lead, impact, effort and votes. Views for the roadmap grouped by quarter, a status board, a timeline, the work in progress now and a request form.
- Hiring tracker - A hiring tracker, ready to use: Candidates and Roles as two linked tables. Candidates have photo, role, stage, recruiter, rating, interview date and resume; each role lists its candidates. Views for the pipeline as a Kanban, all candidates, a gallery of faces, interviews and an application form.