feat(saved): saved, shared, starred, scheduled and widget analyses, export, SQL handoff

Extension 1.4.0: saved analyses of every kind re-run as the viewer
(fields they cannot analyze are dropped), owner-or-shared READ through
core record locks with owner-only writes (and a database CHECK so a
private row can never lose its read owner), personal stars, schedules
that run as the owner and mail only recipients who could run the analysis
themselves, CSV/XLSX export, the SQL Workbench handoff, an analysis
dashboard widget, and an Analyses home built from standard widgets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
james.maesandClaude Opus 5.5 committed 2026-10-13 21:23:23 -05:00
1 parent 43c7571864
commit d2c9871d98
42 files changed
+3786 -296

No files matched your search

@@ -67,9 +67,13 @@ class DataAnalysisPossibleValuesTest
void watchSavedAnalysisDependsOnTable() throws Exception
{
QInstance instance = TestSupport.instance();
QRecord orders = TestSupport.insert(DataAnalysisTables.SAVED, TestSupport.saved("alice", false));
for(String name : List.of("orders", "customers"))
{
instance.addTable(new QTableMetaData().withName(name).withField(new QFieldMetaData("table_name", QFieldType.STRING)));
}
QRecord orders = TestSupport.insert(DataAnalysisTables.SAVED, TestSupport.saved("alice", false).withValue("table_name", "orders"));
QRecord customers = TestSupport.insert(DataAnalysisTables.SAVED, TestSupport.saved("alice", false).withValue("table_name", "customers"));
TestSupport.insert(DataAnalysisTables.SAVED, TestSupport.saved("bob", true));
TestSupport.insert(DataAnalysisTables.SAVED, TestSupport.saved("bob", false).withValue("table_name", "orders"));
QContext.init(instance, TestSupport.user("alice").withPermissions("data_analysis.saved.read", "data_analysis.watch.edit"));
QFieldMetaData field = instance.getTable(DataAnalysisTables.WATCH).getField("saved_id");
assertThat(search(field, Map.of("table_name", "orders"))).extracting(value -> (Object) value.getId()).containsExactly(orders.getValueInteger("id"));