Skip to content

Latest commit

 

History

History
284 lines (227 loc) · 18 KB

File metadata and controls

284 lines (227 loc) · 18 KB

Slate Development Log — Road to Local NocoDB

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)


Phase 1: Rich Field Types ✅ COMPLETE

Expand beyond the 5 current types (text, long_text, date, checkbox, link/URL) to match NocoDB's most-used field types.

Backend ✅

  • db/init.rs — Extend field_type CHECK constraint + SQLite table-recreation migration
  • db/mod.rs — Updated is_supported_field_type() and to_sql_column_type()
  • record_service.rs — Extend json_to_sql() for REAL/INTEGER types
  • db/init.rs — Add app_field_options table
  • New: field_option_service.rs — Full CRUD for select options
  • commands.rs — Added 5 field option commands + updated get_table_snapshot to include options
  • search_service.rs — Include email/phone/url/select in LIKE search

Field Types Added

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)

Frontend ✅

  • src/types/slate.ts — Extended FieldType union 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.tsfieldOptionsByField state + 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)

Phase 2: Column Controls (Sort, Filter, Reorder, Visibility) ✅ COMPLETE

Per-view column controls that make Slate feel like a real query tool.

Sort ✅

  • New: filter_service.rsbuild_sort_clause() builds ORDER BY from SortInput array
  • record_service.rslist_records() accepts sorts: Option<&[SortInput]>
  • commands.rs — Pass sort params through get_table_snapshot
  • TableHeaderCell.tsx — Click cycles asc → desc → none with ▲▼⇅ indicator
  • workspaceStore.tssortsByTable state + setSorts action

Filter ✅

  • filter_service.rsbuild_filter_clause() supports eq/neq/contains/not_contains/is_empty/is_not_empty/gt/lt/gte/lte
  • record_service.rs — Accept filters: Option<&[FilterInput]>
  • commands.rs — Pass filter params through get_table_snapshot
  • New: FilterBar.tsx — Per-filter rows (field + op + value), "+Add filter" button
  • workspaceStore.tsfiltersByTable state + setFilters action

Column Reordering ✅ COMPLETE

  • table_service.rsreorder_fields(table_id, field_ids) updates field_order
  • commands.rsreorder_fields command registered
  • Frontend: Drag-and-drop column headers via @dnd-kit/core + @dnd-kit/sortable
    • TableHeaderCell.tsxuseSortable hook + GripVertical drag handle (hidden until hover); listeners on handle only so sort-click still works
    • TableGrid.tsxDndContext + SortableContext (horizontal); PointerSensor with distance: 5 activation constraint; optimistic local state + DragOverlay ghost label
    • MainTableView.tsx — Hidden-field merge algorithm: visible fields reordered while hidden fields stay in place; full field ID list sent to reorderFields store action

Column Visibility ✅

  • table_service.rstoggle_field_visibility(field_id)
  • commands.rstoggle_field_visibility command registered
  • Frontend: "Fields" panel in toolbar with checkboxes per field
  • MainTableView.tsx — Filters visible fields before passing to grid

Column Resize ✅

  • Frontend only: Draggable column edge; persist widths to localStorage (keyed slate-col-w-{fieldId}); minimum 60px; <colgroup>/<col> via table-layout: fixed; resize handle via .col-resize-handle CSS; mousedown/mousemove/mouseup on document

Phase 3: Named Views System ✅ COMPLETE

Multiple saved views per table, each with its own sorts/filters/field visibility. app_views table already exists in schema.

Backend ✅

  • New: view_service.rs — CRUD on app_views; config_json stores {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

View Types ✅ (Grid, Gallery, Kanban, Calendar complete)

  1. Grid ✅ — Migrated to named view model; "Grid 1" created as default view
  2. Gallery
    • New: GalleryView.tsx — card grid showing primary field + visible fields as label/value rows
  3. Kanban
    • New: KanbanView.tsx — group by any single_select field; drag cards between columns (updates record value in DB)
    • Group-by field selector in kanban toolbar; config persisted to config_json
  4. Calendar
    • New: CalendarView.tsx — month grid; group by any date field; records appear on their date cells

Frontend ✅

  • 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.tsviewsByTable, activeViewIdByTable, hiddenFieldIdsByTable, kanbanGroupByFieldIdByTable state
  • workspaceStore.tssetActiveView, saveActiveViewConfig, setKanbanGroupByField actions

Phase 4: Record UX Improvements ✅ COMPLETE (core features)

Row-level features that complete the database UI feel.

  • Full-screen record expand — Double-click row opens ExpandedRecordModal.tsx as 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/activityrecord_notes SQLite table (id, table_id, record_id, body, created_at); note_service.rs for CRUD; notes section in ExpandedRecordModal.tsx with Cmd+Enter submit; notes delete button per-note

Phase 5: Import / Export ✅ COMPLETE (CSV)

Get data in and out of Slate easily.

  • CSV Importcsv_service::import_csv (Rust): native file picker via rfd, RFC-4180 parser, case-insensitive header→field matching by display_name; frontend "Import" button triggers immediately
  • CSV Exportcsv_service::export_csv (Rust): RFC-4180 escaping, native save dialog via rfd; frontend "Export" button triggers immediately
  • JSON Exportcsv_service::export_json (Rust): serde_json::to_string_pretty, native save dialog; records exported as array of objects keyed by display_name; frontend "JSON" button in TableToolbar

Phase 6: UX Completions ✅ COMPLETE

  • 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_records batch command
  • Command Palette — Cmd+K global overlay; fuzzy search across tables + actions; arrow-key navigation; Escape to close

Phase 7: Computed Fields ✅ COMPLETE

Backend ✅

  • db/init.rsapp_field_computed table + migrate_field_type_constraint_v2() adds lookup, rollup, formula to CHECK constraint
  • models.rsAppField.computed_config: Option<String>
  • metadata_service.rslist_fields + get_field LEFT JOIN app_field_computed
  • table_service.rsis_computed_field_type() helper; create_field writes to app_field_computed; repair_table_storage + delete_field skip computed fields
  • record_service.rsfetch_computed_configsbuild_select_exprsFROM {} r alias → lookup/rollup SQL subqueries → apply_formula_fields (evalexpr post-processing)
  • commands.rscreate_field accepts computed_config: Option<String>
  • Cargo.tomlevalexpr = "11" for formula evaluation

Computed Field Types

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

Frontend ✅

  • src/types/slate.tsisComputedFieldType, COMPUTED_FIELD_TYPES, AppField.computed_config, FieldMutationInput.computed_config
  • src/lib/tauri.tscreateField passes computedConfig
  • workspaceStore.tsaddField action flows computed_config through
  • 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 — Passes tables, fieldsByTable, currentTableId to <AddColumnModal>
  • TableCell.tsx — Read-only .computed-cell rendering for all computed types
  • FieldEditor.tsx — Read-only .computed-field-value display in expand modal

Architecture Notes

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).


Phase 8: Form View + Backups ✅ COMPLETE

Form View ✅

  • ViewType — Added "form" to union type
  • New: FormView.tsx — Card-style entry form; blank defaults; Submit creates record via submitFormRecord; Clear resets; 3-second "✓ Record added" confirmation; computed fields filtered out
  • AddViewModal.tsx — Form added as 5th view type with ClipboardList icon
  • workspaceStore.tssubmitFormRecord action calls createRecord with prefilled values
  • App.tsxviewType === "form" branch renders <FormView>

Backups ✅

  • New: backup_service.rspick_backup_folder (rfd dialog), create_backup (SQLite .backup() API via raw SQL VACUUM INTO), list_backup_files (reads dir sorted by mtime)
  • commands.rspick_backup_folder, create_backup, list_backup_files commands
  • workspaceStore.tsbackupDir, lastBackupAt, backupFiles, backupsLoading state; pickBackupFolder, runBackup actions; loaded in initialize()
  • SettingsModal.tsx — Backups section: folder picker, "Backup Now" button, last backup timestamp, recent backup file list

Phase 9: Settings, Folders, Record Window, Light Mode ✅ COMPLETE

Settings > Databases ✅

  • external_db_service.rslist_external_connections() reads app_meta for ext_db_* keys; returns ExternalConnection summaries with alias, path, table IDs/names
  • SettingsModal.tsx — Databases section: internal DB path, external connection list with disconnect buttons, "Connect Database…" button

Folders / Workspaces ✅

  • db/init.rsapp_folders table + idx_app_folders_order index; migrate_add_folder_id() adds folder_id column to app_tables
  • New: folder_service.rslist_folders, create_folder, rename_folder, delete_folder (ungroups tables, does not delete them), move_table_to_folder, reorder_folders
  • models.rsAppFolder, folder_id: Option<String> on AppTable
  • metadata_service.rslist_tables + get_table include folder_id
  • New: FolderListItem.tsx — Collapsible folder group with chevron, rename/delete buttons, table list; collapse state persisted to localStorage
  • TableListItem.tsx — Move-to-folder popover (FolderInput icon); shows folder list; "Remove from folder" when already grouped
  • TableList.tsx — Passes folders + onMoveToFolder through to items
  • Sidebar.tsx — Restructured: ungrouped tables → folder groups → "External" labeled section; "+ New Folder" button

Record Detail Window ✅

  • commands.rsget_record_detail returns RecordDetailPayload (table + fields + options + record)
  • capabilities/default.jsoncore: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=record URL param routing renders RecordDetailWindow instead of App
  • MainTableView.tsxonOpenRecordWindow prop; passed as onDoubleClickRecord (falls back to onExpandRecord)

Light Mode ✅

  • New: src/lib/theme.tsinitTheme(), setTheme(), getTheme() with localStorage persistence
  • styles.css[data-theme="light"] CSS variable overrides for all surfaces
  • main.tsx — Calls initTheme() before React render to prevent flash
  • SettingsModal.tsx — Appearance section: Dark / Light toggle buttons
  • workspaceStore.tstheme state + setTheme action

Phase 10: Column Resize, JSON Export, Record Notes ✅ COMPLETE

Column Resize ✅

  • TableHeaderCell.tsx.col-resize-handle div at right edge; onMouseDown starts drag; sets cursor: col-resize on body during drag; cleans up on mouseup
  • TableGrid.tsxcolWidths state (Record<fieldId, number>); <colgroup> + <col> elements with explicit widths; table-layout: fixed; widths persisted to localStorage (slate-col-w-{fieldId}); min 60px enforced
  • styles.css.col-resize-handle absolute-positioned 5px handle; blue hover accent via --accent-primary

JSON Export ✅

  • csv_service.rsexport_json(): rfd::FileDialog with .json filter; builds array of serde_json::Map keyed by display_name; writes via serde_json::to_string_pretty
  • commands.rsexport_json command
  • lib.rs — Command registered
  • tauri.tsexportJson(tableId) IPC wrapper
  • workspaceStore.tsexportJsonTable action
  • TableToolbar.tsx — "JSON" button alongside existing "CSV" button; onExportJson prop
  • MainTableView.tsx + App.tsxonExportJson prop wired through

Record Notes ✅

  • db/init.rsrecord_notes table (id, table_id, record_id, body, created_at) + index on (table_id, record_id)
  • models.rsRecordNote struct with Serialize/Deserialize
  • New: services/note_service.rslist_notes, create_note, delete_note
  • services/mod.rspub mod note_service added
  • commands.rslist_record_notes, create_record_note, delete_record_note commands
  • lib.rs — Three commands registered
  • types/slate.tsRecordNote interface
  • tauri.tslistRecordNotes, createRecordNote, deleteRecordNote IPC wrappers
  • ExpandedRecordModal.tsxtableId prop added; notes section below fields: list with per-note delete, textarea input, Cmd+Enter to submit, Send button
  • App.tsxtableId={activeTableId ?? ""} wired to ExpandedRecordModal
  • 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-input classes

Bug Fixes ✅

  • db/mod.rs — Added "tags" to is_supported_field_type() (was missing, caused "unsupported Field Type" error when creating Tags columns)
  • App.tsx — Removed window.confirm from onDisconnectTable handler (Tauri v2 WKWebView silently returns false from window.confirm; now calls disconnectExternalDb directly, matching the Settings panel behavior)

Progress Tracker

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