Goal: Evolve Slate from a minimal Tauri 2 + React + SQLite CRUD app into a local, single-user NocoDB equivalent — with rich field types, multiple view types, per-view filters/sorts, and a polished UX.
Stack: Tauri 2 · React 18 · TypeScript · Vite · Rust · SQLite (rusqlite) · Zustand
How to contribute: Pick any incomplete task below, implement it, mark it [x], and open a PR.
Run tests: ~/.cargo/bin/cargo test (backend) · npm run dev (frontend)
Expand beyond the 5 current types (text, long_text, date, checkbox, link/URL) to match NocoDB's most-used field types.
-
db/init.rs— Extendfield_typeCHECK constraint + SQLite table-recreation migration -
db/mod.rs— Updatedis_supported_field_type()andto_sql_column_type() -
record_service.rs— Extendjson_to_sql()for REAL/INTEGER types -
db/init.rs— Addapp_field_optionstable - New:
field_option_service.rs— Full CRUD for select options -
commands.rs— Added 5 field option commands + updatedget_table_snapshotto include options -
search_service.rs— Include email/phone/url/select in LIKE search
| Type | SQL Storage | Status |
|---|---|---|
number |
REAL | ✅ |
currency |
REAL | ✅ |
percent |
REAL | ✅ |
email |
TEXT | ✅ |
url |
TEXT | ✅ |
phone |
TEXT | ✅ |
single_select |
TEXT | ✅ |
multi_select |
TEXT | ✅ |
rating |
INTEGER | ✅ |
duration |
INTEGER | ✅ |
tags |
TEXT (CSV) | ✅ |
-
src/types/slate.ts— ExtendedFieldTypeunion to 16 types (incl.tags) -
AddColumnModal.tsx— Grouped field type picker (Text / Number / Date / Selection / Toggle) -
TableCell.tsx— Renderers for all new types (stars, chips, number inputs, mailto/tel links) -
FieldEditor.tsx— Detail panel editors for all new types - New:
SelectFieldEditor.tsx— Chip-based single/multi select editor with inline option creation -
workspaceStore.ts—fieldOptionsByFieldstate + createFieldOption/updateFieldOption/deleteFieldOption actions - New:
TagsCell.tsx— Inline chip input; free-text tags with deterministic color hashing; Enter/comma to add, Backspace to remove, deduplication; CSV storage (same format as multi_select)
Per-view column controls that make Slate feel like a real query tool.
- New:
filter_service.rs—build_sort_clause()builds ORDER BY from SortInput array -
record_service.rs—list_records()acceptssorts: Option<&[SortInput]> -
commands.rs— Pass sort params throughget_table_snapshot -
TableHeaderCell.tsx— Click cycles asc → desc → none with ▲▼⇅ indicator -
workspaceStore.ts—sortsByTablestate +setSortsaction
-
filter_service.rs—build_filter_clause()supports eq/neq/contains/not_contains/is_empty/is_not_empty/gt/lt/gte/lte -
record_service.rs— Acceptfilters: Option<&[FilterInput]> -
commands.rs— Pass filter params throughget_table_snapshot - New:
FilterBar.tsx— Per-filter rows (field + op + value), "+Add filter" button -
workspaceStore.ts—filtersByTablestate +setFiltersaction
-
table_service.rs—reorder_fields(table_id, field_ids)updates field_order -
commands.rs—reorder_fieldscommand registered - Frontend: Drag-and-drop column headers via
@dnd-kit/core+@dnd-kit/sortableTableHeaderCell.tsx—useSortablehook +GripVerticaldrag handle (hidden until hover); listeners on handle only so sort-click still worksTableGrid.tsx—DndContext+SortableContext(horizontal);PointerSensorwithdistance: 5activation constraint; optimistic local state +DragOverlayghost labelMainTableView.tsx— Hidden-field merge algorithm: visible fields reordered while hidden fields stay in place; full field ID list sent toreorderFieldsstore action
-
table_service.rs—toggle_field_visibility(field_id) -
commands.rs—toggle_field_visibilitycommand registered - Frontend: "Fields" panel in toolbar with checkboxes per field
-
MainTableView.tsx— Filters visible fields before passing to grid
- Frontend only: Draggable column edge; persist widths to
localStorage(keyedslate-col-w-{fieldId}); minimum 60px;<colgroup>/<col>viatable-layout: fixed; resize handle via.col-resize-handleCSS;mousedown/mousemove/mouseupon document
Multiple saved views per table, each with its own sorts/filters/field visibility.
app_viewstable already exists in schema.
- New:
view_service.rs— CRUD onapp_views;config_jsonstores{hiddenFieldIds, kanbanGroupByFieldId, rowHeight} -
commands.rs— New commands:create_view,rename_view,delete_view,list_views,update_view_config -
get_table_snapshot— Applies view's sorts/filters/hidden fields when rendering
- Grid ✅ — Migrated to named view model; "Grid 1" created as default view
- Gallery ✅
- New:
GalleryView.tsx— card grid showing primary field + visible fields as label/value rows
- New:
- Kanban ✅
- New:
KanbanView.tsx— group by anysingle_selectfield; drag cards between columns (updates record value in DB) - Group-by field selector in kanban toolbar; config persisted to
config_json
- New:
- Calendar ✅
- New:
CalendarView.tsx— month grid; group by anydatefield; records appear on their date cells
- New:
-
ViewTabsBar.tsx— View tabs below toolbar: click to switch, "+" to add, rename/delete context menu -
AddViewModal.tsx— View type picker with icons (Grid, Gallery, Kanban) -
workspaceStore.ts—viewsByTable,activeViewIdByTable,hiddenFieldIdsByTable,kanbanGroupByFieldIdByTablestate -
workspaceStore.ts—setActiveView,saveActiveViewConfig,setKanbanGroupByFieldactions
Row-level features that complete the database UI feel.
- Full-screen record expand — Double-click row opens
ExpandedRecordModal.tsxas a full-screen overlay; Escape or backdrop-click to close - Row height toggle — Compact / Default / Tall modes in toolbar; stored per view in
config_json; applied as CSS class on<table> - Keyboard navigation — Arrow keys move between cells; Tab advances cell (wraps to next row); Enter expands record modal; Escape clears focus
- Bulk operations — Checkbox multi-select column; shift+click range select; bulk delete action bar appears when selection > 0
- Record notes/activity —
record_notesSQLite table (id, table_id, record_id, body, created_at);note_service.rsfor CRUD; notes section inExpandedRecordModal.tsxwith Cmd+Enter submit; notes delete button per-note
Get data in and out of Slate easily.
- CSV Import —
csv_service::import_csv(Rust): native file picker viarfd, RFC-4180 parser, case-insensitive header→field matching bydisplay_name; frontend "Import" button triggers immediately - CSV Export —
csv_service::export_csv(Rust): RFC-4180 escaping, native save dialog viarfd; frontend "Export" button triggers immediately - JSON Export —
csv_service::export_json(Rust):serde_json::to_string_pretty, native save dialog; records exported as array of objects keyed bydisplay_name; frontend "JSON" button inTableToolbar
- Grouped Grid View — "Group By" toolbar dropdown; records bucketed by field value with collapsible section headers; ungrouped records in "No value" group
- Calendar View — Month grid (7×6); records appear on date cells matched by a configurable date field; added as 4th view type in
AddViewModal - Bulk Operations — Checkbox column (shift+click range select); "Delete N" action bar; backend
delete_recordsbatch command - Command Palette — Cmd+K global overlay; fuzzy search across tables + actions; arrow-key navigation; Escape to close
-
db/init.rs—app_field_computedtable +migrate_field_type_constraint_v2()addslookup,rollup,formulato CHECK constraint -
models.rs—AppField.computed_config: Option<String> -
metadata_service.rs—list_fields+get_fieldLEFT JOINapp_field_computed -
table_service.rs—is_computed_field_type()helper;create_fieldwrites toapp_field_computed;repair_table_storage+delete_fieldskip computed fields -
record_service.rs—fetch_computed_configs→build_select_exprs→FROM {} ralias → lookup/rollup SQL subqueries →apply_formula_fields(evalexpr post-processing) -
commands.rs—create_fieldacceptscomputed_config: Option<String> -
Cargo.toml—evalexpr = "11"for formula evaluation
| Type | Strategy | Status |
|---|---|---|
lookup |
Correlated subquery via record_links (LIMIT 1) |
✅ |
rollup |
Aggregate subquery via record_links (COUNT/SUM/AVG/MIN/MAX) |
✅ |
formula |
Post-process with evalexpr after row fetch |
✅ |
-
src/types/slate.ts—isComputedFieldType,COMPUTED_FIELD_TYPES,AppField.computed_config,FieldMutationInput.computed_config -
src/lib/tauri.ts—createFieldpassescomputedConfig -
workspaceStore.ts—addFieldaction flowscomputed_configthrough -
AddColumnModal.tsx— "Computed" group with inline config UI: table/field selectors for lookup+rollup; fn dropdown for rollup; formula textarea + field-insert chips -
App.tsx— Passestables,fieldsByTable,currentTableIdto<AddColumnModal> -
TableCell.tsx— Read-only.computed-cellrendering for all computed types -
FieldEditor.tsx— Read-only.computed-field-valuedisplay in expand modal
| Concept | Location |
|---|---|
| Single SQLite connection | Mutex<Connection> in AppState |
| All IPC | with_conn() in commands.rs |
| Schema repair | repair_all_table_storage in init_app (NOT in initialize_database) |
| Tauri commands | src-tauri/src/commands.rs |
| State management | src/store/workspaceStore.ts (Zustand) |
| Field options table | app_field_options (created in Phase 1) |
| View config | app_views.config_json JSON blob (table already exists) |
| Tests | src-tauri/src/tests.rs — run with ~/.cargo/bin/cargo test |
Security notes: All SQL identifiers go through quote_ident() in schema_service.rs. Filter/sort values must be parameterized (use rusqlite params, not string interpolation).
-
ViewType— Added"form"to union type - New:
FormView.tsx— Card-style entry form; blank defaults; Submit creates record viasubmitFormRecord; Clear resets; 3-second "✓ Record added" confirmation; computed fields filtered out -
AddViewModal.tsx— Form added as 5th view type withClipboardListicon -
workspaceStore.ts—submitFormRecordaction callscreateRecordwith prefilled values -
App.tsx—viewType === "form"branch renders<FormView>
- New:
backup_service.rs—pick_backup_folder(rfd dialog),create_backup(SQLite.backup()API via raw SQLVACUUM INTO),list_backup_files(reads dir sorted by mtime) -
commands.rs—pick_backup_folder,create_backup,list_backup_filescommands -
workspaceStore.ts—backupDir,lastBackupAt,backupFiles,backupsLoadingstate;pickBackupFolder,runBackupactions; loaded ininitialize() -
SettingsModal.tsx— Backups section: folder picker, "Backup Now" button, last backup timestamp, recent backup file list
-
external_db_service.rs—list_external_connections()readsapp_metaforext_db_*keys; returnsExternalConnectionsummaries with alias, path, table IDs/names -
SettingsModal.tsx— Databases section: internal DB path, external connection list with disconnect buttons, "Connect Database…" button
-
db/init.rs—app_folderstable +idx_app_folders_orderindex;migrate_add_folder_id()addsfolder_idcolumn toapp_tables - New:
folder_service.rs—list_folders,create_folder,rename_folder,delete_folder(ungroups tables, does not delete them),move_table_to_folder,reorder_folders -
models.rs—AppFolder,folder_id: Option<String>onAppTable -
metadata_service.rs—list_tables+get_tableincludefolder_id - New:
FolderListItem.tsx— Collapsible folder group with chevron, rename/delete buttons, table list; collapse state persisted tolocalStorage -
TableListItem.tsx— Move-to-folder popover (FolderInputicon); shows folder list; "Remove from folder" when already grouped -
TableList.tsx— Passesfolders+onMoveToFolderthrough to items -
Sidebar.tsx— Restructured: ungrouped tables → folder groups → "External" labeled section; "+ New Folder" button
-
commands.rs—get_record_detailreturnsRecordDetailPayload(table + fields + options + record) -
capabilities/default.json—core:window:allow-create,core:window:allow-set-title,"record-*"window pattern - New:
RecordDetailWindow.tsx— Standalone OS window; fetches record on mount; auto-saves on field change; delete button closes window -
main.tsx—?mode=recordURL param routing rendersRecordDetailWindowinstead ofApp -
MainTableView.tsx—onOpenRecordWindowprop; passed asonDoubleClickRecord(falls back toonExpandRecord)
- New:
src/lib/theme.ts—initTheme(),setTheme(),getTheme()withlocalStoragepersistence -
styles.css—[data-theme="light"]CSS variable overrides for all surfaces -
main.tsx— CallsinitTheme()before React render to prevent flash -
SettingsModal.tsx— Appearance section: Dark / Light toggle buttons -
workspaceStore.ts—themestate +setThemeaction
-
TableHeaderCell.tsx—.col-resize-handlediv at right edge;onMouseDownstarts drag; setscursor: col-resizeon body during drag; cleans up onmouseup -
TableGrid.tsx—colWidthsstate (Record<fieldId, number>);<colgroup>+<col>elements with explicit widths;table-layout: fixed; widths persisted tolocalStorage(slate-col-w-{fieldId}); min 60px enforced -
styles.css—.col-resize-handleabsolute-positioned 5px handle; blue hover accent via--accent-primary
-
csv_service.rs—export_json():rfd::FileDialogwith.jsonfilter; builds array ofserde_json::Mapkeyed bydisplay_name; writes viaserde_json::to_string_pretty -
commands.rs—export_jsoncommand -
lib.rs— Command registered -
tauri.ts—exportJson(tableId)IPC wrapper -
workspaceStore.ts—exportJsonTableaction -
TableToolbar.tsx— "JSON" button alongside existing "CSV" button;onExportJsonprop -
MainTableView.tsx+App.tsx—onExportJsonprop wired through
-
db/init.rs—record_notestable (id, table_id, record_id, body, created_at) + index on (table_id, record_id) -
models.rs—RecordNotestruct withSerialize/Deserialize - New:
services/note_service.rs—list_notes,create_note,delete_note -
services/mod.rs—pub mod note_serviceadded -
commands.rs—list_record_notes,create_record_note,delete_record_notecommands -
lib.rs— Three commands registered -
types/slate.ts—RecordNoteinterface -
tauri.ts—listRecordNotes,createRecordNote,deleteRecordNoteIPC wrappers -
ExpandedRecordModal.tsx—tableIdprop added; notes section below fields: list with per-note delete, textarea input, Cmd+Enter to submit, Send button -
App.tsx—tableId={activeTableId ?? ""}wired toExpandedRecordModal -
styles.css—.record-notes-section,.record-notes-title,.record-notes-list,.record-note-item,.record-note-body,.record-note-meta,.record-note-time,.record-note-input-row,.record-note-inputclasses
-
db/mod.rs— Added"tags"tois_supported_field_type()(was missing, caused "unsupported Field Type" error when creating Tags columns) -
App.tsx— Removedwindow.confirmfromonDisconnectTablehandler (Tauri v2 WKWebView silently returnsfalsefromwindow.confirm; now callsdisconnectExternalDbdirectly, matching the Settings panel behavior)
| Phase | Status |
|---|---|
| 1 — Field Types (incl. Tags) | ✅ Complete |
| 2 — Sort / Filter / Column Controls (incl. drag-and-drop reorder) | ✅ Complete |
| 3 — Named Views (Grid, Gallery, Kanban, Calendar) | ✅ Complete |
| 4 — Record UX (incl. bulk delete) | ✅ Complete |
| 5 — Import / Export | ✅ Complete |
| 6 — UX Completions (Grouped Grid, Calendar, Bulk Ops, Cmd+K) | ✅ Complete |
| 7 — Computed Fields (Lookup, Rollup, Formula) | ✅ Complete |
| 8 — Form View + Backups | ✅ Complete |
| 9 — Settings, Folders, Record Window, Light Mode | ✅ Complete |
| 10 — Column Resize, JSON Export, Record Notes | ✅ Complete |