diff --git a/app/client/components/DataTables.ts b/app/client/components/DataTables.ts index db87d5dc..e3c59a21 100644 --- a/app/client/components/DataTables.ts +++ b/app/client/components/DataTables.ts @@ -20,10 +20,13 @@ export class DataTables extends Disposable { private _view: Observable; constructor(private _gristDoc: GristDoc) { super(); - // Remove tables that we don't have access to. ACL will remove tableId from those tables. - this._tables = Computed.create(this, use => - use(_gristDoc.docModel.rawTables.getObservable()) - .filter(t => Boolean(use(t.tableId)))); + this._tables = Computed.create(this, use => { + const dataTables = use(_gristDoc.docModel.rawDataTables.getObservable()); + const summaryTables = use(_gristDoc.docModel.rawSummaryTables.getObservable()); + // Remove tables that we don't have access to. ACL will remove tableId from those tables. + return [...dataTables, ...summaryTables].filter(t => Boolean(use(t.tableId))); + }); + // Get the user id, to remember selected layout on the next visit. const userId = this._gristDoc.app.topAppModel.appObs.get()?.currentUser?.id ?? 0; this._view = this.autoDispose(localStorageObs(`u=${userId}:raw:viewType`, "list")); @@ -35,7 +38,7 @@ export class DataTables extends Disposable { /*************** List section **********/ testId('list'), cssBetween( - docListHeader('Raw data tables'), + docListHeader('Raw Data Tables'), cssSwitch( buttonSelect( this._view, @@ -54,32 +57,13 @@ export class DataTables extends Disposable { cssItem( testId('table'), cssLeft( - dom.domComputed(tableRec.tableId, (tableId) => - cssGreenIcon( - 'TypeTable', - testId(`table-id-${tableId}`) - ) - ), + dom.domComputed((use) => cssGreenIcon( + use(tableRec.summarySourceTable) !== 0 ? 'PivotLight' : 'TypeTable', + testId(`table-id-${use(tableRec.tableId)}`) + )), ), cssMiddle( - css60( - testId('table-title'), - dom.domComputed(fromKo(tableRec.rawViewSectionRef), vsRef => { - if (!vsRef) { - // Some very old documents might not have rawViewSection. - return dom('span', dom.text(tableRec.tableNameDef)); - } else { - return dom('div', // to disable flex grow in the widget - dom.domComputed(fromKo(tableRec.rawViewSection), vs => - dom.update( - buildTableName(vs, testId('widget-title')), - dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }), - ) - ) - ); - } - }), - ), + css60(this._tableTitle(tableRec), testId('table-title')), css40( cssIdHoverWrapper( cssUpperCase("Table id: "), @@ -122,6 +106,30 @@ export class DataTables extends Disposable { ); } + private _tableTitle(table: TableRec) { + return dom.domComputed((use) => { + const rawViewSectionRef = use(fromKo(table.rawViewSectionRef)); + const isSummaryTable = use(table.summarySourceTable) !== 0; + if (!rawViewSectionRef || isSummaryTable) { + // Some very old documents might not have a rawViewSection, and raw summary + // tables can't currently be renamed. + const tableName = [ + use(table.tableNameDef), isSummaryTable ? use(table.groupDesc) : '' + ].filter(p => Boolean(p?.trim())).join(' '); + return dom('span', tableName); + } else { + return dom('div', // to disable flex grow in the widget + dom.domComputed(fromKo(table.rawViewSection), vs => + dom.update( + buildTableName(vs, testId('widget-title')), + dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }), + ) + ) + ); + } + }); + } + private _menuItems(table: TableRec) { const {isReadonly, docModel} = this._gristDoc; return [ @@ -130,8 +138,8 @@ export class DataTables extends Disposable { 'Remove', testId('menu-remove'), dom.cls('disabled', use => use(isReadonly) || ( - // Can't delete last user table, unless it is a hidden table. - use(docModel.allTables.getObservable()).length <= 1 && !use(table.isHidden) + // Can't delete last visible table, unless it is a hidden table. + use(docModel.visibleTables.getObservable()).length <= 1 && !use(table.isHidden) )) ), dom.maybe(isReadonly, () => menuText('You do not have edit access to this document')), @@ -141,9 +149,9 @@ export class DataTables extends Disposable { private _removeTable(t: TableRec) { const {docModel} = this._gristDoc; function doRemove() { - return docModel.docData.sendAction(['RemoveTable', t.tableId.peek()]); + return docModel.docData.sendAction(['RemoveTable', t.tableId()]); } - confirmModal(`Delete ${t.tableId()} data, and remove it from all pages?`, 'Delete', doRemove); + confirmModal(`Delete ${t.formattedTableName()} data, and remove it from all pages?`, 'Delete', doRemove); } } diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index c46f2dcb..b170874e 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -171,7 +171,7 @@ export class GristDoc extends DisposableWithEvents { this.docInfo = this.docModel.docInfoRow; this.hasDocTour = Computed.create(this, use => - use(this.docModel.allTableIds.getObservable()).includes('GristDocTour')); + use(this.docModel.visibleTableIds.getObservable()).includes('GristDocTour')); const defaultViewId = this.docInfo.newDefaultViewId; @@ -911,7 +911,7 @@ export class GristDoc extends DisposableWithEvents { * Renames table. Method exposed primarily for tests. */ public async renameTable(tableId: string, newTableName: string) { - const tableRec = this.docModel.allTables.all().find(t => t.tableId.peek() === tableId); + const tableRec = this.docModel.visibleTables.all().find(t => t.tableId.peek() === tableId); if (!tableRec) { throw new UserError(`No table with id ${tableId}`); } diff --git a/app/client/components/Importer.ts b/app/client/components/Importer.ts index c66e4e8f..20f9cde2 100644 --- a/app/client/components/Importer.ts +++ b/app/client/components/Importer.ts @@ -182,7 +182,7 @@ export class Importer extends DisposableWithEvents { private _destTables = Computed.create>>(this, (use) => [ {value: NEW_TABLE, label: 'New Table'}, ...(use(this._sourceInfoArray).length > 1 ? [{value: SKIP_TABLE, label: 'Skip'}] : []), - ...use(this._gristDoc.docModel.allTableIds.getObservable()).map((t) => ({value: t, label: t})), + ...use(this._gristDoc.docModel.visibleTableIds.getObservable()).map((id) => ({value: id, label: id})), ]); // Source column labels for the selected import source, keyed by column id. diff --git a/app/client/components/WidgetFrame.ts b/app/client/components/WidgetFrame.ts index 3eb9ad54..38ea79a4 100644 --- a/app/client/components/WidgetFrame.ts +++ b/app/client/components/WidgetFrame.ts @@ -305,7 +305,7 @@ export class GristDocAPIImpl implements GristDocAPI { } public async listTables(): Promise { - // Could perhaps read tableIds from this.gristDoc.docModel.allTableIds.all()? + // Could perhaps read tableIds from this.gristDoc.docModel.visibleTableIds.all()? const tables = await this._doc.docComm.fetchTable('_grist_Tables'); // Tables the user doesn't have access to are just blanked out. return tables[3].tableId.filter(tableId => tableId !== '') as string[]; diff --git a/app/client/models/DocModel.ts b/app/client/models/DocModel.ts index d9837a46..84e244ed 100644 --- a/app/client/models/DocModel.ts +++ b/app/client/models/DocModel.ts @@ -22,7 +22,8 @@ import {urlState} from 'app/client/models/gristUrlState'; import MetaRowModel from 'app/client/models/MetaRowModel'; import MetaTableModel from 'app/client/models/MetaTableModel'; import * as rowset from 'app/client/models/rowset'; -import {isHiddenTable, isRawTable} from 'app/common/isHiddenTable'; +import {isHiddenTable, isSummaryTable} from 'app/common/isHiddenTable'; +import {RowFilterFunc} from 'app/common/RowFilterFunc'; import {schema, SchemaTypes} from 'app/common/schema'; import {ACLRuleRec, createACLRuleRec} from 'app/client/models/entities/ACLRuleRec'; @@ -126,9 +127,11 @@ export class DocModel { public docInfoRow: DocInfoRec; - public allTables: KoArray; - public rawTables: KoArray; - public allTableIds: KoArray; + public visibleTables: KoArray; + public rawDataTables: KoArray; + public rawSummaryTables: KoArray; + + public visibleTableIds: KoArray; // A mapping from tableId to DataTableModel for user-defined tables. public dataTables: {[tableId: string]: DataTableModel} = {}; @@ -161,12 +164,15 @@ export class DocModel { // An observable array of user-visible tables, sorted by tableId, excluding summary tables. // This is a publicly exposed member. - this.allTables = createUserTablesArray(this.tables); - this.rawTables = createRawTablesArray(this.tables); + this.visibleTables = createVisibleTablesArray(this.tables); + + // Observable arrays of raw data and summary tables, sorted by tableId. + this.rawDataTables = createRawDataTablesArray(this.tables); + this.rawSummaryTables = createRawSummaryTablesArray(this.tables); - // An observable array of user-visible tableIds. A shortcut mapped from allTables. - const allTableIds = ko.computed(() => this.allTables.all().map(t => t.tableId())); - this.allTableIds = koArray.syncedKoArray(allTableIds); + // An observable array of user-visible tableIds. A shortcut mapped from visibleTables. + const visibleTableIds = ko.computed(() => this.visibleTables.all().map(t => t.tableId())); + this.visibleTableIds = koArray.syncedKoArray(visibleTableIds); // Create an observable array of RowModels for all the data tables. We'll trigger // onAddTable/onRemoveTable in response to this array's splice events below. @@ -219,24 +225,40 @@ export class DocModel { } } - /** - * Helper to create an observable array of tables, sorted by tableId, and excluding hidden - * tables. + * Creates an observable array of tables, sorted by tableId. + * + * An optional `filterFunc` may be specified to filter tables. */ -function createUserTablesArray(tablesModel: MetaTableModel): KoArray { - const rowSource = new rowset.FilteredRowSource(r => !isHiddenTable(tablesModel.tableData, r)); +function createTablesArray( + tablesModel: MetaTableModel, + filterFunc: RowFilterFunc = (_row) => true +) { + const rowSource = new rowset.FilteredRowSource(filterFunc); rowSource.subscribeTo(tablesModel); // Create an observable RowModel array based on this rowSource, sorted by tableId. return tablesModel._createRowSetModel(rowSource, 'tableId'); } /** - * Helper to create an observable array of tables, sorted by tableId, and excluding summary tables. + * Returns an observable array of user tables, sorted by tableId, and excluding hidden/summary + * tables. */ - function createRawTablesArray(tablesModel: MetaTableModel): KoArray { - const rowSource = new rowset.FilteredRowSource(r => isRawTable(tablesModel.tableData, r)); - rowSource.subscribeTo(tablesModel); - // Create an observable RowModel array based on this rowSource, sorted by tableId. - return tablesModel._createRowSetModel(rowSource, 'tableId'); +function createVisibleTablesArray(tablesModel: MetaTableModel): KoArray { + return createTablesArray(tablesModel, r => !isHiddenTable(tablesModel.tableData, r)); +} + +/** + * Returns an observable array of raw data tables, sorted by tableId, and excluding summary + * tables. + */ +function createRawDataTablesArray(tablesModel: MetaTableModel): KoArray { + return createTablesArray(tablesModel, r => !isSummaryTable(tablesModel.tableData, r)); +} + +/** + * Returns an observable array of raw summary tables, sorted by tableId. + */ + function createRawSummaryTablesArray(tablesModel: MetaTableModel): KoArray { + return createTablesArray(tablesModel, r => isSummaryTable(tablesModel.tableData, r)); } diff --git a/app/client/models/SearchModel.ts b/app/client/models/SearchModel.ts index 3aea418c..6008239b 100644 --- a/app/client/models/SearchModel.ts +++ b/app/client/models/SearchModel.ts @@ -188,7 +188,7 @@ class FinderImpl implements IFinder { // If we are on a raw view page, pretend that we are looking at true pages. if ('data' === this._gristDoc.activeViewId.get()) { // Get all raw sections. - const rawSections = this._gristDoc.docModel.allTables.peek() + const rawSections = this._gristDoc.docModel.visibleTables.peek() // Filter out those we don't have permissions to see (through ACL-tableId will be empty). .filter(t => Boolean(t.tableId.peek())) // sort in order that is the same as on the raw data list page, diff --git a/app/client/models/entities/ColumnRec.ts b/app/client/models/entities/ColumnRec.ts index 53af467e..870de7d3 100644 --- a/app/client/models/entities/ColumnRec.ts +++ b/app/client/models/entities/ColumnRec.ts @@ -124,7 +124,7 @@ export function createColumnRec(this: ColumnRec, docModel: DocModel): void { // Returns the rowModel for the referenced table, or null, if this is not a reference column. this.refTable = ko.pureComputed(() => { const refTableId = getReferencedTableId(this.type() || ""); - return refTableId ? docModel.allTables.all().find(t => t.tableId() === refTableId) || null : null; + return refTableId ? docModel.visibleTables.all().find(t => t.tableId() === refTableId) || null : null; }); // Helper for Reference/ReferenceList columns, which returns a formatter according to the visibleCol diff --git a/app/client/models/entities/PageRec.ts b/app/client/models/entities/PageRec.ts index 7e5f130c..6ad94463 100644 --- a/app/client/models/entities/PageRec.ts +++ b/app/client/models/entities/PageRec.ts @@ -13,7 +13,7 @@ export function createPageRec(this: PageRec, docModel: DocModel): void { const name = this.view().name(); const isTableHidden = () => { const viewId = this.view().id(); - const tables = docModel.rawTables.all(); + const tables = docModel.rawDataTables.all(); const primaryTable = tables.find(t => t.primaryViewId() === viewId); return !!primaryTable && primaryTable.isHidden(); }; diff --git a/app/client/models/entities/TableRec.ts b/app/client/models/entities/TableRec.ts index 99a3a541..bc1438e0 100644 --- a/app/client/models/entities/TableRec.ts +++ b/app/client/models/entities/TableRec.ts @@ -30,6 +30,9 @@ export interface TableRec extends IRowModel<"_grist_Tables"> { tableName: modelUtil.KoSaveableObservable; // Table name with a default value (which is tableId). tableNameDef: modelUtil.KoSaveableObservable; + // Like tableNameDef, but formatted to be more suitable for displaying to + // users (e.g. including group columns for summary tables). + formattedTableName: ko.PureComputed; // If user can select this table in various places. // Note: Some hidden tables can still be visible on RawData view. isHidden: ko.Computed; @@ -114,4 +117,9 @@ export function createTableRec(this: TableRec, docModel: DocModel): void { return table.tableId() || ''; }) ); + this.formattedTableName = ko.pureComputed(() => { + return this.summarySourceTable() + ? `${this.tableNameDef()} ${this.groupDesc()}` + : this.tableNameDef(); + }); } diff --git a/app/client/ui/PageWidgetPicker.ts b/app/client/ui/PageWidgetPicker.ts index 3719da7f..92618bf4 100644 --- a/app/client/ui/PageWidgetPicker.ts +++ b/app/client/ui/PageWidgetPicker.ts @@ -131,7 +131,7 @@ export function buildPageWidgetPicker( onSave: ISaveFunc, options: IOptions = {}) { - const tables = fromKo(docModel.allTables.getObservable()); + const tables = fromKo(docModel.visibleTables.getObservable()); const columns = fromKo(docModel.columns.createAllRowsModel('parentPos').getObservable()); // default value for when it is omitted diff --git a/app/client/ui/RightPanel.ts b/app/client/ui/RightPanel.ts index 2a3b21ce..8479d650 100644 --- a/app/client/ui/RightPanel.ts +++ b/app/client/ui/RightPanel.ts @@ -300,10 +300,16 @@ export class RightPanel extends Disposable { return dom.maybe(viewConfigTab, (vct) => [ this._disableIfReadonly(), cssLabel(dom.text(use => use(activeSection.isRaw) ? 'DATA TABLE NAME' : 'WIDGET TITLE'), - dom.style('margin-bottom', '14px')), + dom.style('margin-bottom', '14px'), + ), cssRow(cssTextInput( Computed.create(owner, (use) => use(activeSection.titleDef)), val => activeSection.titleDef.saveOnly(val), + dom.boolAttr('disabled', use => { + const isRawTable = use(activeSection.isRaw); + const isSummaryTable = use(use(activeSection.table).summarySourceTable) !== 0; + return isRawTable && isSummaryTable; + }), testId('right-widget-title') )), @@ -753,4 +759,10 @@ const cssListItem = styled('li', ` export const cssTextInput = styled(textInput, ` flex: 1 0 auto; + + &:disabled { + color: ${colors.slate}; + background-color: ${colors.lightGrey}; + pointer-events: none; + } `); diff --git a/app/client/ui/Tools.ts b/app/client/ui/Tools.ts index 7d7f849d..1990551e 100644 --- a/app/client/ui/Tools.ts +++ b/app/client/ui/Tools.ts @@ -51,7 +51,7 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'data'), cssPageLink( cssPageIcon('Database'), - cssLinkText('Raw data'), + cssLinkText('Raw Data'), testId('raw'), urlState().setLinkUrl({docPage: 'data'}) ) diff --git a/app/client/ui2018/IconList.ts b/app/client/ui2018/IconList.ts index c40d9c2d..009ac010 100644 --- a/app/client/ui2018/IconList.ts +++ b/app/client/ui2018/IconList.ts @@ -88,6 +88,7 @@ export type IconName = "ChartArea" | "PinBig" | "PinSmall" | "Pivot" | + "PivotLight" | "Plus" | "Public" | "PublicColor" | @@ -213,6 +214,7 @@ export const IconList: IconName[] = ["ChartArea", "PinBig", "PinSmall", "Pivot", + "PivotLight", "Plus", "Public", "PublicColor", diff --git a/app/client/widgets/FieldBuilder.ts b/app/client/widgets/FieldBuilder.ts index cfa043c1..644ee99f 100644 --- a/app/client/widgets/FieldBuilder.ts +++ b/app/client/widgets/FieldBuilder.ts @@ -289,7 +289,7 @@ export class FieldBuilder extends Disposable { // Builds the reference type table selector. Built when the column is type reference. public _buildRefTableSelect() { const allTables = Computed.create(null, (use) => - use(this._docModel.allTableIds.getObservable()).map(tableId => ({ + use(this._docModel.visibleTableIds.getObservable()).map(tableId => ({ value: tableId, label: tableId, icon: 'FieldTable' as const diff --git a/app/common/isHiddenTable.ts b/app/common/isHiddenTable.ts index e0e319ed..954603fe 100644 --- a/app/common/isHiddenTable.ts +++ b/app/common/isHiddenTable.ts @@ -1,18 +1,19 @@ +import {TableData} from 'app/common/TableData'; import {UIRowId} from 'app/common/UIRowId'; -import {TableData} from "./TableData"; /** - * Return whether a table identified by the rowId of its metadata record, should normally be - * hidden from the user (e.g. as an option in the page-widget picker). + * Return whether a table (identified by the rowId of its metadata record) should + * normally be hidden from the user (e.g. as an option in the page-widget picker). */ export function isHiddenTable(tablesData: TableData, tableRef: UIRowId): boolean { const tableId = tablesData.getValue(tableRef, 'tableId') as string|undefined; - return !isRawTable(tablesData, tableRef) || Boolean(tableId?.startsWith('GristHidden')); + return isSummaryTable(tablesData, tableRef) || Boolean(tableId?.startsWith('GristHidden')); } /** - * Return whether a table identified by the rowId of its metadata record should be visible on Raw Data page. + * Return whether a table (identified by the rowId of its metadata record) is a + * summary table. */ -export function isRawTable(tablesData: TableData, tableRef: UIRowId): boolean { - return tablesData.getValue(tableRef, 'summarySourceTable') === 0; +export function isSummaryTable(tablesData: TableData, tableRef: UIRowId): boolean { + return tablesData.getValue(tableRef, 'summarySourceTable') !== 0; } diff --git a/app/common/schema.ts b/app/common/schema.ts index 698dad7c..41f9c086 100644 --- a/app/common/schema.ts +++ b/app/common/schema.ts @@ -4,7 +4,7 @@ import { GristObjCode } from "app/plugin/GristData"; // tslint:disable:object-literal-key-quotes -export const SCHEMA_VERSION = 29; +export const SCHEMA_VERSION = 30; export const schema = { diff --git a/app/server/lib/initialDocSql.ts b/app/server/lib/initialDocSql.ts index add6f0a5..b5481017 100644 --- a/app/server/lib/initialDocSql.ts +++ b/app/server/lib/initialDocSql.ts @@ -6,7 +6,7 @@ export const GRIST_DOC_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',29,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',30,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_Imports" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "origFileName" TEXT DEFAULT '', "parseFormula" TEXT DEFAULT '', "delimiter" TEXT DEFAULT '', "doublequote" BOOLEAN DEFAULT 0, "escapechar" TEXT DEFAULT '', "quotechar" TEXT DEFAULT '', "skipinitialspace" BOOLEAN DEFAULT 0, "encoding" TEXT DEFAULT '', "hasHeaders" BOOLEAN DEFAULT 0); @@ -42,7 +42,7 @@ export const GRIST_DOC_WITH_TABLE1_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',29,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',30,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0); INSERT INTO _grist_Tables VALUES(1,'Table1',1,0,0,2); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); diff --git a/sandbox/grist/docmodel.py b/sandbox/grist/docmodel.py index d38e7b6d..faaf24d0 100644 --- a/sandbox/grist/docmodel.py +++ b/sandbox/grist/docmodel.py @@ -73,8 +73,13 @@ class MetaTableExtras(object): if rec.summarySourceTable else None) def setAutoRemove(rec, table): - """Marks the table for removal if it's a summary table with no more view sections.""" - table.docmodel.setAutoRemove(rec, rec.summarySourceTable and not rec.viewSections) + """ + Marks the table for removal if it's a summary table with no more (non-raw) view sections. + """ + is_summary_table = rec.summarySourceTable + view_sections_table = table.docmodel.get_table('_grist_Views_section') + has_view_sections = view_sections_table.lookupOne(isRaw=False, tableRef=rec.id) + table.docmodel.setAutoRemove(rec, is_summary_table and not has_view_sections) class _grist_Tables_column(object): diff --git a/sandbox/grist/migrations.py b/sandbox/grist/migrations.py index 045f278d..f693ee05 100644 --- a/sandbox/grist/migrations.py +++ b/sandbox/grist/migrations.py @@ -960,3 +960,57 @@ def migration29(tdset): })) return tdset.apply_doc_actions(doc_actions) + +@migration(schema_version=30) +def migration30(tdset): + """ + Add raw view sections for each summary table. This is similar to migration 26, but for + summary tables instead of user tables. + """ + doc_actions = [] + + tables = list(actions.transpose_bulk_action(tdset.all_tables["_grist_Tables"])) + columns = list(actions.transpose_bulk_action(tdset.all_tables["_grist_Tables_column"])) + + new_view_section_id = next_id(tdset, "_grist_Views_section") + + for table in sorted(tables, key=lambda t: t.tableId): + if not table.summarySourceTable: + continue + + table_columns = [ + col for col in columns + if table.id == col.parentId and is_visible_column(col.colId) + ] + table_columns.sort(key=lambda c: c.parentPos) + fields = { + "parentId": [new_view_section_id] * len(table_columns), + "colRef": [col.id for col in table_columns], + "parentPos": [col.parentPos for col in table_columns], + } + field_ids = [None] * len(table_columns) + + doc_actions += [ + actions.AddRecord( + "_grist_Views_section", new_view_section_id, { + "tableRef": table.id, + "parentId": 0, + "parentKey": "record", + "title": "", + "defaultWidth": 100, + "borderWidth": 1, + } + ), + actions.UpdateRecord( + "_grist_Tables", table.id, { + "rawViewSectionRef": new_view_section_id, + }) + , + actions.BulkAddRecord( + "_grist_Views_section_field", field_ids, fields + ), + ] + + new_view_section_id += 1 + + return tdset.apply_doc_actions(doc_actions) diff --git a/sandbox/grist/schema.py b/sandbox/grist/schema.py index 38bae5fa..a2dfd0c5 100644 --- a/sandbox/grist/schema.py +++ b/sandbox/grist/schema.py @@ -15,7 +15,7 @@ import six import actions -SCHEMA_VERSION = 29 +SCHEMA_VERSION = 30 def make_column(col_id, col_type, formula='', isFormula=False): return { diff --git a/sandbox/grist/summary.py b/sandbox/grist/summary.py index 3cf09816..6fea7928 100644 --- a/sandbox/grist/summary.py +++ b/sandbox/grist/summary.py @@ -205,7 +205,8 @@ class SummaryActions(object): result = self.useractions.doAddTable( encode_summary_table_name(source_table.tableId), [_get_colinfo_dict(ci, with_id=True) for ci in groupby_colinfo + formula_colinfo], - summarySourceTableRef=source_table.id) + summarySourceTableRef=source_table.id, + raw_section=True) summary_table = self.docmodel.tables.table.get_record(result['id']) created = True # Note that in this case, _get_or_add_columns() below should not add any new columns, diff --git a/sandbox/grist/test_column_actions.py b/sandbox/grist/test_column_actions.py index 5e72f8bb..4323c933 100644 --- a/sandbox/grist/test_column_actions.py +++ b/sandbox/grist/test_column_actions.py @@ -228,10 +228,10 @@ class TestColumnActions(test_engine.EngineTestCase): Field(11, colRef=16), Field(12, colRef=17), ]), - Section(5, parentKey="record", tableRef=3, fields=[ - Field(13, colRef=18), - Field(14, colRef=20), - Field(15, colRef=21), + Section(6, parentKey="record", tableRef=3, fields=[ + Field(16, colRef=18), + Field(17, colRef=20), + Field(18, colRef=21), ]), ]), View(2, sections=[ @@ -315,10 +315,10 @@ class TestColumnActions(test_engine.EngineTestCase): Field(10, colRef=15), Field(12, colRef=17), ]), - Section(5, parentKey="record", tableRef=3, fields=[ - Field(13, colRef=18), - Field(14, colRef=20), - Field(15, colRef=21), + Section(6, parentKey="record", tableRef=3, fields=[ + Field(16, colRef=18), + Field(17, colRef=20), + Field(18, colRef=21), ]), ]), View(2, sections=[ @@ -372,9 +372,9 @@ class TestColumnActions(test_engine.EngineTestCase): Field(10, colRef=15), Field(12, colRef=17), ]), - Section(5, parentKey="record", tableRef=4, fields=[ - Field(14, colRef=23), - Field(15, colRef=24), + Section(6, parentKey="record", tableRef=4, fields=[ + Field(17, colRef=23), + Field(18, colRef=24), ]), ]), View(2, sections=[ diff --git a/sandbox/grist/test_summary.py b/sandbox/grist/test_summary.py index 97466299..337a4897 100644 --- a/sandbox/grist/test_summary.py +++ b/sandbox/grist/test_summary.py @@ -126,9 +126,9 @@ class TestSummary(test_engine.EngineTestCase): formula="SUM($group.amount)"), ]) summary_view1 = View(2, sections=[ - Section(2, parentKey="record", tableRef=2, fields=[ - Field(4, colRef=15), - Field(5, colRef=16), + Section(3, parentKey="record", tableRef=2, fields=[ + Field(6, colRef=15), + Field(7, colRef=16), ]) ]) self.assertTables([self.starting_table, summary_table1]) @@ -156,10 +156,10 @@ class TestSummary(test_engine.EngineTestCase): formula="SUM($group.amount)"), ]) summary_view2 = View(3, sections=[ - Section(3, parentKey="record", tableRef=3, fields=[ - Field(6, colRef=17), - Field(7, colRef=19), - Field(8, colRef=20), + Section(5, parentKey="record", tableRef=3, fields=[ + Field(11, colRef=17), + Field(12, colRef=19), + Field(13, colRef=20), ]) ]) self.assertTables([self.starting_table, summary_table1, summary_table2]) @@ -197,11 +197,11 @@ class TestSummary(test_engine.EngineTestCase): formula="SUM($group.amount)"), ]) summary_view3 = View(4, sections=[ - Section(4, parentKey="record", tableRef=4, fields=[ - Field(9, colRef=21), - Field(10, colRef=22), - Field(11, colRef=24), - Field(12, colRef=25), + Section(7, parentKey="record", tableRef=4, fields=[ + Field(18, colRef=21), + Field(19, colRef=22), + Field(20, colRef=24), + Field(21, colRef=25), ]) ]) self.assertTables([self.starting_table, summary_table1, summary_table2, summary_table3]) @@ -281,11 +281,11 @@ class Address: formula="SUM($group.amount)"), ]) summary_view = View(1, sections=[ - Section(1, parentKey="record", tableRef=2, fields=[ - Field(1, colRef=14), - Field(2, colRef=15), - Field(3, colRef=17), - Field(4, colRef=18), + Section(2, parentKey="record", tableRef=2, fields=[ + Field(5, colRef=14), + Field(6, colRef=15), + Field(7, colRef=17), + Field(8, colRef=18), ]) ]) self.assertTables([self.starting_table, summary_table]) @@ -296,21 +296,21 @@ class Address: self.apply_user_action(["CreateViewSection", 1, 0, "record", [12,11], None]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None]) summary_view2 = View(2, sections=[ - Section(2, parentKey="record", tableRef=2, fields=[ - Field(5, colRef=15), - Field(6, colRef=14), - Field(7, colRef=17), - Field(8, colRef=18), - ]) - ]) - summary_view3 = View(3, sections=[ Section(3, parentKey="record", tableRef=2, fields=[ - Field(9, colRef=14), - Field(10, colRef=15), + Field(9, colRef=15), + Field(10, colRef=14), Field(11, colRef=17), Field(12, colRef=18), ]) ]) + summary_view3 = View(3, sections=[ + Section(4, parentKey="record", tableRef=2, fields=[ + Field(13, colRef=14), + Field(14, colRef=15), + Field(15, colRef=17), + Field(16, colRef=18), + ]) + ]) # Verify that we have a new view, but are reusing the table. self.assertTables([self.starting_table, summary_table]) self.assertViews([summary_view, summary_view2, summary_view3]) diff --git a/sandbox/grist/test_summary2.py b/sandbox/grist/test_summary2.py index 1fe88663..dd655bb2 100644 --- a/sandbox/grist/test_summary2.py +++ b/sandbox/grist/test_summary2.py @@ -83,11 +83,13 @@ class TestSummary2(test_engine.EngineTestCase): ]) - # We should now have two sections for table 2 (the one with two group-by fields). + # We should now have three sections for table 2 (the one with two group-by fields). One for + # the raw summary table view, and two for the non-raw views. self.assertTableData('_grist_Views_section', cols="subset", data=[ ["id", "parentId", "tableRef"], - [1, 1, 2], - [5, 5, 2], + [1, 0, 2], + [2, 1, 2], + [9, 5, 2], ], rows=lambda r: r.tableRef.id == 2) self.assertTableData('_grist_Views_section_field', cols="subset", data=[ ["id", "parentId", "colRef"], @@ -95,11 +97,11 @@ class TestSummary2(test_engine.EngineTestCase): [2, 1, 15], [3, 1, 17], [4, 1, 18], - [8, 1, 23], - [16, 5, 14], - [17, 5, 15], - [18, 5, 17], - [19, 5, 18], # new section doesn't automatically get 'average' column + [15, 1, 23], + [17, 5, 24], + [18, 5, 26], + [19, 5, 27], + [20, 5, 28], # new section doesn't automatically get 'average' column ], rows=lambda r: r.parentId.id in {1,5}) @@ -433,11 +435,11 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=2, fields=[ - Field(1, colRef=14), - Field(2, colRef=15), - Field(3, colRef=17), - Field(4, colRef=18), + Section(2, parentKey="record", tableRef=2, fields=[ + Field(5, colRef=14), + Field(6, colRef=15), + Field(7, colRef=17), + Field(8, colRef=18), ]) ])]) self.assertEqual(get_helper_cols('Address'), ['#summary#GristSummary_7_Address']) @@ -451,7 +453,7 @@ class TestSummary2(test_engine.EngineTestCase): ]) # Now change the group-by to just one of the columns ('state') - self.apply_user_action(["UpdateSummaryViewSection", 1, [12]]) + self.apply_user_action(["UpdateSummaryViewSection", 2, [12]]) self.assertTables([ self.starting_table, # Note that Table #2 is gone at this point, since it's unused. @@ -463,10 +465,10 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=3, fields=[ - Field(2, colRef=19), - Field(3, colRef=21), - Field(4, colRef=22), + Section(2, parentKey="record", tableRef=3, fields=[ + Field(6, colRef=19), + Field(7, colRef=21), + Field(8, colRef=22), ]) ])]) self.assertTableData('GristSummary_7_Address2', cols="subset", data=[ @@ -486,7 +488,7 @@ class TestSummary2(test_engine.EngineTestCase): ]) # Change group-by to a different single column ('city') - self.apply_user_action(["UpdateSummaryViewSection", 1, [11]]) + self.apply_user_action(["UpdateSummaryViewSection", 2, [11]]) self.assertTables([ self.starting_table, # Note that Table #3 is gone at this point, since it's unused. @@ -498,10 +500,10 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=4, fields=[ - Field(5, colRef=23), - Field(3, colRef=25), - Field(4, colRef=26), + Section(2, parentKey="record", tableRef=4, fields=[ + Field(15, colRef=23), + Field(7, colRef=25), + Field(8, colRef=26), ]) ])]) self.assertTableData('GristSummary_7_Address', cols="subset", data=[ @@ -525,7 +527,7 @@ class TestSummary2(test_engine.EngineTestCase): ]) # Change group-by to no columns (totals) - self.apply_user_action(["UpdateSummaryViewSection", 1, []]) + self.apply_user_action(["UpdateSummaryViewSection", 2, []]) self.assertTables([ self.starting_table, # Note that Table #4 is gone at this point, since it's unused. @@ -536,9 +538,9 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=5, fields=[ - Field(3, colRef=28), - Field(4, colRef=29), + Section(2, parentKey="record", tableRef=5, fields=[ + Field(7, colRef=28), + Field(8, colRef=29), ]) ])]) self.assertTableData('GristSummary_7_Address2', cols="subset", data=[ @@ -548,7 +550,7 @@ class TestSummary2(test_engine.EngineTestCase): self.assertEqual(get_helper_cols('Address'), ['#summary#GristSummary_7_Address2']) # Back to full circle, but with group-by columns differently arranged. - self.apply_user_action(["UpdateSummaryViewSection", 1, [12,11]]) + self.apply_user_action(["UpdateSummaryViewSection", 2, [12,11]]) self.assertTables([ self.starting_table, # Note that Table #5 is gone at this point, since it's unused. @@ -561,11 +563,11 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=6, fields=[ - Field(5, colRef=30), - Field(6, colRef=31), - Field(3, colRef=33), - Field(4, colRef=34), + Section(2, parentKey="record", tableRef=6, fields=[ + Field(22, colRef=30), + Field(23, colRef=31), + Field(7, colRef=33), + Field(8, colRef=34), ]) ])]) self.assertTableData('GristSummary_7_Address', cols="subset", data=[ @@ -595,23 +597,23 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=6, fields=[ - Field(5, colRef=30), - Field(6, colRef=31), - Field(3, colRef=33), - Field(4, colRef=34), - ]), Section(2, parentKey="record", tableRef=6, fields=[ - Field(7, colRef=31), - Field(8, colRef=30), - Field(9, colRef=33), - Field(10, colRef=34), + Field(22, colRef=30), + Field(23, colRef=31), + Field(7, colRef=33), + Field(8, colRef=34), + ]), + Section(7, parentKey="record", tableRef=6, fields=[ + Field(24, colRef=31), + Field(25, colRef=30), + Field(26, colRef=33), + Field(27, colRef=34), ]) ])]) self.assertEqual(get_helper_cols('Address'), ['#summary#GristSummary_7_Address']) # Change one view section, and ensure there are now two summary tables. - self.apply_user_action(["UpdateSummaryViewSection", 2, []]) + self.apply_user_action(["UpdateSummaryViewSection", 7, []]) self.assertTables([ self.starting_table, Table(6, "GristSummary_7_Address", 0, 1, columns=[ @@ -628,22 +630,22 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=6, fields=[ - Field(5, colRef=30), - Field(6, colRef=31), - Field(3, colRef=33), - Field(4, colRef=34), + Section(2, parentKey="record", tableRef=6, fields=[ + Field(22, colRef=30), + Field(23, colRef=31), + Field(7, colRef=33), + Field(8, colRef=34), ]), - Section(2, parentKey="record", tableRef=7, fields=[ - Field(9, colRef=36), - Field(10, colRef=37), + Section(7, parentKey="record", tableRef=7, fields=[ + Field(26, colRef=36), + Field(27, colRef=37), ]) ])]) self.assertEqual(get_helper_cols('Address'), ['#summary#GristSummary_7_Address', '#summary#GristSummary_7_Address2']) # Delete one view section, and see that the summary table is gone. - self.apply_user_action(["RemoveViewSection", 2]) + self.apply_user_action(["RemoveViewSection", 7]) self.assertTables([ self.starting_table, # Note that Table #7 is gone at this point, since it's now unused. @@ -656,18 +658,18 @@ class TestSummary2(test_engine.EngineTestCase): ]) ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=6, fields=[ - Field(5, colRef=30), - Field(6, colRef=31), - Field(3, colRef=33), - Field(4, colRef=34), + Section(2, parentKey="record", tableRef=6, fields=[ + Field(22, colRef=30), + Field(23, colRef=31), + Field(7, colRef=33), + Field(8, colRef=34), ]) ])]) self.assertEqual(get_helper_cols('Address'), ['#summary#GristSummary_7_Address']) # Change the section to add and then remove the "amount" to the group-by column; check that # column "amount" was correctly restored - self.apply_user_action(["UpdateSummaryViewSection", 1, [11, 12, 13]]) + self.apply_user_action(["UpdateSummaryViewSection", 2, [11, 12, 13]]) self.assertTables([ self.starting_table, Table(7, "GristSummary_7_Address2", 0, 1, columns=[ @@ -679,14 +681,14 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=7, fields=[ - Field(6, colRef=35), - Field(5, colRef=36), - Field(7, colRef=37), - Field(3, colRef=39), + Section(2, parentKey="record", tableRef=7, fields=[ + Field(23, colRef=35), + Field(22, colRef=36), + Field(28, colRef=37), + Field(7, colRef=39), ]) ])]) - self.apply_user_action(["UpdateSummaryViewSection", 1, [11,12]]) + self.apply_user_action(["UpdateSummaryViewSection", 2, [11,12]]) self.assertTables([ self.starting_table, Table(8, "GristSummary_7_Address", 0, 1, columns=[ @@ -699,18 +701,18 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=8, fields=[ - Field(6, colRef=40), - Field(5, colRef=41), - Field(7, colRef=42), - Field(3, colRef=44), + Section(2, parentKey="record", tableRef=8, fields=[ + Field(23, colRef=40), + Field(22, colRef=41), + Field(28, colRef=42), + Field(7, colRef=44), ]) ])]) # Hide a formula and update group by columns; check that the formula columns had not been # deleted self.apply_user_action(['RemoveRecord', '_grist_Views_section_field', 7]) - self.apply_user_action(["UpdateSummaryViewSection", 1, [11]]) + self.apply_user_action(["UpdateSummaryViewSection", 2, [11]]) self.assertTables([ self.starting_table, Table(9, "GristSummary_7_Address2", 0, 1, columns=[ @@ -721,9 +723,9 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=9, fields=[ - Field(6, colRef=45), - Field(3, colRef=48), + Section(2, parentKey="record", tableRef=9, fields=[ + Field(23, colRef=45), + Field(28, colRef=46), ]) ])]) @@ -754,11 +756,11 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=2, fields=[ - Field(1, colRef=14), - Field(2, colRef=16), - Field(3, colRef=17), - Field(4, colRef=18), + Section(2, parentKey="record", tableRef=2, fields=[ + Field(4, colRef=14), + Field(5, colRef=16), + Field(6, colRef=17), + Field(8, colRef=18), ]) ])]) self.assertTableData('GristSummary_7_Address', cols="subset", data=[ @@ -770,7 +772,7 @@ class TestSummary2(test_engine.EngineTestCase): ]) # Change the section to add "city" as a group-by column; check that the formula is gone. - self.apply_user_action(["UpdateSummaryViewSection", 1, [11,12]]) + self.apply_user_action(["UpdateSummaryViewSection", 2, [11,12]]) self.assertTables([ self.starting_table, Table(3, "GristSummary_7_Address2", 0, 1, columns=[ @@ -782,12 +784,12 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=3, fields=[ + Section(2, parentKey="record", tableRef=3, fields=[ # We requested 'city' to come before 'state', check that this is the case. - Field(4, colRef=19), - Field(1, colRef=20), - Field(2, colRef=22), - Field(3, colRef=23), + Field(13, colRef=19), + Field(4, colRef=20), + Field(5, colRef=22), + Field(6, colRef=23), ]) ])]) @@ -830,27 +832,27 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=2, fields=[ - Field(1, colRef=14), - Field(2, colRef=15), - Field(3, colRef=17), - Field(4, colRef=18), + Section(2, parentKey="record", tableRef=2, fields=[ + Field(5, colRef=14), + Field(6, colRef=15), + Field(7, colRef=17), + Field(8, colRef=18), ]) ]), View(2, sections=[ - Section(2, parentKey="record", tableRef=3, fields=[ - Field(5, colRef=20), - Field(6, colRef=21), + Section(4, parentKey="record", tableRef=3, fields=[ + Field(11, colRef=20), + Field(12, colRef=21), ]), - Section(3, parentKey="record", tableRef=2, fields=[ - Field(7, colRef=14), - Field(8, colRef=15), - Field(9, colRef=17), - Field(10, colRef=18), + Section(5, parentKey="record", tableRef=2, fields=[ + Field(13, colRef=14), + Field(14, colRef=15), + Field(15, colRef=17), + Field(16, colRef=18), ]), - Section(4, parentKey="record", tableRef=4, fields=[ - Field(11, colRef=22), - Field(12, colRef=24), - Field(13, colRef=25), + Section(7, parentKey="record", tableRef=4, fields=[ + Field(20, colRef=22), + Field(21, colRef=24), + Field(22, colRef=25), ]) ])]) @@ -869,11 +871,11 @@ class TestSummary2(test_engine.EngineTestCase): ]), ]) self.assertViews([View(1, sections=[ - Section(1, parentKey="record", tableRef=2, fields=[ - Field(1, colRef=14), - Field(2, colRef=15), - Field(3, colRef=17), - Field(4, colRef=18), + Section(2, parentKey="record", tableRef=2, fields=[ + Field(5, colRef=14), + Field(6, colRef=15), + Field(7, colRef=17), + Field(8, colRef=18), ]) ])]) @@ -885,10 +887,10 @@ class TestSummary2(test_engine.EngineTestCase): self.load_sample(self.sample) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None]) - self.apply_user_action(["UpdateRecord", "_grist_Views_section", 1, + self.apply_user_action(["UpdateRecord", "_grist_Views_section", 2, {"sortColRefs": "[15,14,-17]"}]) - # We should have a single summary table, and a single section referring to it. + # We should have a single summary table, and a single (non-raw) section referring to it. self.assertTables([ self.starting_table, Table(2, "GristSummary_7_Address", 0, 1, columns=[ @@ -901,11 +903,12 @@ class TestSummary2(test_engine.EngineTestCase): ]) self.assertTableData('_grist_Views_section', cols="subset", data=[ ["id", "tableRef", "sortColRefs"], - [1, 2, "[15,14,-17]"], + [1, 2, ""], # This is the raw section. + [2, 2, "[15,14,-17]"], ]) # Now change the group-by to just one of the columns ('state') - self.apply_user_action(["UpdateSummaryViewSection", 1, [12]]) + self.apply_user_action(["UpdateSummaryViewSection", 2, [12]]) self.assertTables([ self.starting_table, # Note that Table #2 is gone at this point, since it's unused. @@ -919,7 +922,8 @@ class TestSummary2(test_engine.EngineTestCase): # Verify that sortColRefs refers to new columns. self.assertTableData('_grist_Views_section', cols="subset", data=[ ["id", "tableRef", "sortColRefs"], - [1, 3, "[19,-21]"], + [2, 3, "[19,-21]"], + [3, 3, ""], # This is the raw section. ]) #---------------------------------------------------------------------- @@ -953,8 +957,10 @@ class TestSummary2(test_engine.EngineTestCase): ]) self.assertTableData('_grist_Views_section', cols="subset", data=[ ["id", "parentId", "tableRef"], - [1, 1, 2], - [2, 2, 3], + [1, 0, 2], + [2, 1, 2], + [3, 0, 3], + [4, 2, 3], ]) self.assertTableData('_grist_Views_section_field', cols="subset", data=[ ["id", "parentId", "colRef"], @@ -962,13 +968,20 @@ class TestSummary2(test_engine.EngineTestCase): [2, 1, 15], [3, 1, 17], [4, 1, 18], - [7, 1, 22], - [5, 2, 20], - [6, 2, 21], + [13, 1, 22], + [5, 2, 14], + [6, 2, 15], + [7, 2, 17], + [8, 2, 18], + [14, 2, 22], + [9, 3, 20], + [10, 3, 21], + [11, 4, 20], + [12, 4, 21], ], sort=lambda r: (r.parentId, r.id)) # Now save one section as a separate table, i.e. "detach" it from its source. - self.apply_user_action(["DetachSummaryViewSection", 1]) + self.apply_user_action(["DetachSummaryViewSection", 2]) # Check the table and columns for all the summary tables. self.assertTables([ @@ -992,25 +1005,25 @@ class TestSummary2(test_engine.EngineTestCase): # We should now have two sections for table 2 (the one with two group-by fields). self.assertTableData('_grist_Views_section', cols="subset", rows=lambda r: r.parentId, data=[ ["id", "parentId", "tableRef"], - [1, 1, 4], - [2, 2, 3], - [3, 3, 4], + [2, 1, 4], + [4, 2, 3], + [5, 3, 4], ]) self.assertTableData( '_grist_Views_section_field', cols="subset", rows=lambda r: r.parentId.parentId, data=[ ["id", "parentId", "colRef"], - [1, 1, 24], - [2, 1, 25], - [3, 1, 26], - [4, 1, 27], - [7, 1, 28], - [5, 2, 20], - [6, 2, 21], - [8, 3, 24], - [9, 3, 25], - [10, 3, 26], - [11, 3, 27], - [12, 3, 28], + [5, 2, 24], + [6, 2, 25], + [7, 2, 26], + [8, 2, 27], + [14, 2, 28], + [11, 4, 20], + [12, 4, 21], + [15, 5, 24], + [16, 5, 25], + [17, 5, 26], + [18, 5, 27], + [19, 5, 28], ], sort=lambda r: (r.parentId, r.id)) # Check that the data is as we expect. @@ -1039,7 +1052,7 @@ class TestSummary2(test_engine.EngineTestCase): # Add a summary table and detach it. Then add a summary table of that table. self.load_sample(self.sample) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None]) - self.apply_user_action(["DetachSummaryViewSection", 1]) + self.apply_user_action(["DetachSummaryViewSection", 2]) # Create a summary of the detached table (tableRef 3) by state (colRef 21). self.apply_user_action(["CreateViewSection", 3, 0, "record", [21], None]) diff --git a/sandbox/grist/test_summary_choicelist.py b/sandbox/grist/test_summary_choicelist.py index c556b183..24cac230 100644 --- a/sandbox/grist/test_summary_choicelist.py +++ b/sandbox/grist/test_summary_choicelist.py @@ -310,7 +310,7 @@ class TestSummaryChoiceList(EngineTestCase): self.assertTableData('GristSummary_6_Source2', data=summary_data) # Verify that "DetachSummaryViewSection" useraction works correctly. - self.apply_user_action(["DetachSummaryViewSection", 2]) + self.apply_user_action(["DetachSummaryViewSection", 4]) self.assertTables([ self.starting_table, summary_table1, summary_table3, summary_table4, diff --git a/sandbox/grist/test_table_actions.py b/sandbox/grist/test_table_actions.py index 31a93f35..76e83c32 100644 --- a/sandbox/grist/test_table_actions.py +++ b/sandbox/grist/test_table_actions.py @@ -100,15 +100,15 @@ class TestTableActions(test_engine.EngineTestCase): Field(14, colRef=3), Field(15, colRef=4), ]), - Section(6, parentKey="record", tableRef=3, fields=[ - Field(16, colRef=9), - Field(17, colRef=11), - Field(18, colRef=12), + Section(7, parentKey="record", tableRef=3, fields=[ + Field(19, colRef=9), + Field(20, colRef=11), + Field(21, colRef=12), ]), - Section(7, parentKey="record", tableRef=2, fields=[ - Field(19, colRef=6), - Field(20, colRef=7), - Field(21, colRef=8), + Section(8, parentKey="record", tableRef=2, fields=[ + Field(22, colRef=6), + Field(23, colRef=7), + Field(24, colRef=8), ]), ]), ]) @@ -300,9 +300,9 @@ class TestTableActions(test_engine.EngineTestCase): ]), ]), View(3, sections=[ - Section(7, parentKey="record", tableRef=2, fields=[ - Field(19, colRef=6), - Field(21, colRef=8), + Section(8, parentKey="record", tableRef=2, fields=[ + Field(22, colRef=6), + Field(24, colRef=8), ]), ]), ]) diff --git a/sandbox/grist/test_useractions.py b/sandbox/grist/test_useractions.py index 9eadf1f3..f2460349 100644 --- a/sandbox/grist/test_useractions.py +++ b/sandbox/grist/test_useractions.py @@ -172,9 +172,9 @@ class TestUserActions(test_engine.EngineTestCase): Section(2, parentKey="record", tableRef=1, fields=[ Field(2, colRef=21), ]), - Section(3, parentKey="record", tableRef=2, fields=[ - Field(3, colRef=22), - Field(4, colRef=24), + Section(4, parentKey="record", tableRef=2, fields=[ + Field(5, colRef=22), + Field(6, colRef=24), ]), ]) self.assertTables([self.starting_table, summary_table]) @@ -255,8 +255,8 @@ class TestUserActions(test_engine.EngineTestCase): Column(35, "count", "Int", isFormula=True, formula="len($group)", summarySourceCol=0), ]) self.assertTables([self.starting_table, new_table, new_table2, new_table3, summary_table]) - new_view.sections.append(Section(6, parentKey="record", tableRef=5, fields=[ - Field(16, colRef=35) + new_view.sections.append(Section(7, parentKey="record", tableRef=5, fields=[ + Field(17, colRef=35) ])) self.assertViews([new_view]) @@ -315,21 +315,21 @@ class TestUserActions(test_engine.EngineTestCase): Field(8, colRef=3), Field(9, colRef=4), ]), - Section(4, parentKey="record", tableRef=2, fields=[ - Field(10, colRef=5), - Field(11, colRef=7), - Field(12, colRef=8), + Section(5, parentKey="record", tableRef=2, fields=[ + Field(13, colRef=5), + Field(14, colRef=7), + Field(15, colRef=8), ]), - Section(7, parentKey='record', tableRef=3, fields=[ - Field(18, colRef=10), - Field(19, colRef=11), - Field(20, colRef=12), + Section(8, parentKey='record', tableRef=3, fields=[ + Field(21, colRef=10), + Field(22, colRef=11), + Field(23, colRef=12), ]), ]), View(3, sections=[ - Section(5, parentKey="chart", tableRef=1, fields=[ - Field(13, colRef=2), - Field(14, colRef=3), + Section(6, parentKey="chart", tableRef=1, fields=[ + Field(16, colRef=2), + Field(17, colRef=3), ]), ]) ]) @@ -469,8 +469,8 @@ class TestUserActions(test_engine.EngineTestCase): self.assertTableData('_grist_Tables', cols="subset", data=[ ['id', 'tableId', 'primaryViewId', 'rawViewSectionRef'], [1, 'Z', 1, 2], - [2, 'GristSummary_1_Z', 0, 0], - [3, 'Table1', 0, 6], + [2, 'GristSummary_1_Z', 0, 4], + [3, 'Table1', 0, 7], ]) self.assertTableData('_grist_Views', cols="subset", data=[ ['id', 'name'], @@ -486,9 +486,9 @@ class TestUserActions(test_engine.EngineTestCase): self.assertTableData('_grist_Tables', cols="subset", data=[ ['id', 'tableId', 'primaryViewId', 'rawViewSectionRef'], [1, 'Z', 1, 2], - [2, 'GristSummary_1_Z', 0, 0], - [3, 'Table1', 0, 6], - [4, 'Stations', 4, 9], + [2, 'GristSummary_1_Z', 0, 4], + [3, 'Table1', 0, 7], + [4, 'Stations', 4, 10], ]) self.assertTableData('_grist_Views', cols="subset", data=[ ['id', 'name'], @@ -546,27 +546,27 @@ class TestUserActions(test_engine.EngineTestCase): Field(8, colRef=3), Field(9, colRef=4), ]), - Section(4, parentKey="record", tableRef=2, fields=[ - Field(10, colRef=5), - Field(11, colRef=7), - Field(12, colRef=8), + Section(5, parentKey="record", tableRef=2, fields=[ + Field(13, colRef=5), + Field(14, colRef=7), + Field(15, colRef=8), ]), - Section(7, parentKey='record', tableRef=3, fields=[ - Field(18, colRef=10), - Field(19, colRef=11), - Field(20, colRef=12), + Section(8, parentKey='record', tableRef=3, fields=[ + Field(21, colRef=10), + Field(22, colRef=11), + Field(23, colRef=12), ]), ]), View(3, sections=[ - Section(5, parentKey="chart", tableRef=1, fields=[ - Field(13, colRef=2), - Field(14, colRef=3), + Section(6, parentKey="chart", tableRef=1, fields=[ + Field(16, colRef=2), + Field(17, colRef=3), ]), ]) ]) # Remove a couple of sections. Ensure their fields get removed. - self.apply_user_action(['BulkRemoveRecord', '_grist_Views_section', [4, 7]]) + self.apply_user_action(['BulkRemoveRecord', '_grist_Views_section', [5, 8]]) self.assertViews([ View(1, sections=[ @@ -584,9 +584,9 @@ class TestUserActions(test_engine.EngineTestCase): ]) ]), View(3, sections=[ - Section(5, parentKey="chart", tableRef=1, fields=[ - Field(13, colRef=2), - Field(14, colRef=3), + Section(6, parentKey="chart", tableRef=1, fields=[ + Field(16, colRef=2), + Field(17, colRef=3), ]), ]) ]) @@ -607,13 +607,13 @@ class TestUserActions(test_engine.EngineTestCase): orig_method() self.engine.assert_schema_consistent = types.MethodType(override, self.engine) - # Do a non-sschema action to ensure it doesn't get called. + # Do a non-schema action to ensure it doesn't get called. self.apply_user_action(['UpdateRecord', '_grist_Views', 2, {'name': 'A'}]) self.assertEqual(count_calls[0], 0) # Do a schema action to ensure it gets called: this causes a table rename. # 7 is id of raw view section for the Tabl1 table - self.apply_user_action(['UpdateRecord', '_grist_Views_section', 6, {'title': 'C'}]) + self.apply_user_action(['UpdateRecord', '_grist_Views_section', 7, {'title': 'C'}]) self.assertEqual(count_calls[0], 1) self.assertTableData('_grist_Tables', cols="subset", data=[ diff --git a/sandbox/grist/useractions.py b/sandbox/grist/useractions.py index 5f25a771..79f7e6ad 100644 --- a/sandbox/grist/useractions.py +++ b/sandbox/grist/useractions.py @@ -455,10 +455,17 @@ class UserActions(object): if ( table_id == "_grist_Views_section" and any(rec.isRaw for i, rec in self._bulk_action_iter(table_id, row_ids)) - # Only these fields are allowed to be modified - and not set(column_values) <= {"title", "options", "sortColRefs"} ): - raise ValueError("Cannot modify raw view section") + allowed_fields = {"title", "options", "sortColRefs"} + has_summary_section = any(rec.tableRef.summarySourceTable + for i, rec in self._bulk_action_iter(table_id, row_ids)) + if has_summary_section: + # When a group-by column is removed from a summary source table, the source table reference + # changes; we pre-emptively allow changes to tableRef here to avoid blocking such actions. + allowed_fields.add("tableRef") + + if not set(column_values) <= allowed_fields: + raise ValueError("Cannot modify raw view section") if ( table_id == "_grist_Views_section_field" @@ -1734,12 +1741,12 @@ class UserActions(object): if raw_section: # Create raw view section - raw_section = self._create_plain_view_section( + raw_section = self.create_plain_view_section( result["id"], table_id, self._docmodel.view_sections, "record", - table_title + table_title if not summarySourceTableRef else "" ) if primary_view or raw_section: @@ -1800,7 +1807,7 @@ class UserActions(object): if groupby_colrefs is not None: section = self._summary.create_new_summary_section(table, groupby_cols, view, section_type) else: - section = self._create_plain_view_section( + section = self.create_plain_view_section( table.id, table.tableId, view.viewSections, @@ -1813,7 +1820,7 @@ class UserActions(object): 'sectionRef': section.id } - def _create_plain_view_section(self, tableRef, tableId, view_sections, section_type, title): + def create_plain_view_section(self, tableRef, tableId, view_sections, section_type, title): # If title is the same as tableId leave it empty if title == tableId: title = '' diff --git a/static/icons/icons.css b/static/icons/icons.css index 671d2a82..2b4b76be 100644 --- a/static/icons/icons.css +++ b/static/icons/icons.css @@ -89,6 +89,7 @@ --icon-PinBig: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTExLjgyOTcyODEsLTcuNjAyODA3MjdlLTEzIEw0LjE5Mjk3NDkxLC03LjU3NjE2MTkyZS0xMyBDMy42MjcyODk0OCwtNy41MzE3NTNlLTEzIDMuMjAzMDI1NDEsMC40MTAyNTY0MSAzLjIwMzAyNTQxLDAuOTU3MjY0OTU3IEMzLjIwMzAyNTQxLDEuNTA0MjczNSAzLjYyNzI4OTQ4LDEuOTE0NTI5OTEgNC4xOTI5NzQ5MSwxLjkxNDUyOTkxIEw1LjA0MTUwMzA0LDEuOTE0NTI5OTEgTDIuODQ5NDcyMDIsOS4wOTQwMTcwOSBMMS40MzUyNTg0Niw5LjA5NDAxNzA5IEMwLjg2OTU3MzAzNiw5LjA5NDAxNzA5IDAuNDQ1MzA4OTY3LDkuNTA0MjczNSAwLjQ0NTMwODk2NywxMC4wNTEyODIxIEMwLjQ0NTMwODk2NywxMC41OTgyOTA2IDAuODY5NTczMDM2LDExLjAwODU0NyAxLjQzNTI1ODQ2LDExLjAwODU0NyBMMy41NTY1Nzg4LDExLjAwODU0NyBMNi45NTA2OTEzNSwxMS4wMDg1NDcgTDcuMDIxNDAyMDMsMTYgTDkuMDAxMzAxMDIsMTYgTDkuMDAxMzAxMDIsMTAuOTQwMTcwOSBMMTQuNjU4MTU1MywxMC45NDAxNzA5IEMxNS4zNjUyNjIxLDEwLjk0MDE3MDkgMTUuNjQ4MTA0OCwxMC4yNTY0MTAzIDE1LjY0ODEwNDgsOS45ODI5MDU5OCBDMTUuNjQ4MTA0OCw5LjQzNTg5NzQ0IDE1LjIyMzg0MDcsOS4wMjU2NDEwMyAxNC42NTgxNTUzLDkuMDI1NjQxMDMgTDEzLjI0Mzk0MTcsOS4wMjU2NDEwMyBMMTEuMTIyNjIxNCwxLjkxNDUyOTkxIEwxMS45NzExNDk1LDEuOTE0NTI5OTEgQzEyLjgxOTY3NzYsMS45MTQ1Mjk5MSAxMi45NjEwOTksMS4yMzA3NjkyMyAxMi45NjEwOTksMC45NTcyNjQ5NTcgQzEyLjg5MDM4ODMsMC40Nzg2MzI0NzkgMTIuMzk1NDEzNiwtNy42NjQ5Nzk3NmUtMTMgMTEuODI5NzI4MSwtNy42MDI4MDcyN2UtMTMgWiBNMTEuMDUxOTEwNyw5LjA5NDAxNzA5IEw0Ljk3MDc5MjM3LDkuMDk0MDE3MDkgTDcuMDkyMTEyNzEsMS45ODI5MDU5OCBMOC45MzA1OTAzNCwxLjk4MjkwNTk4IEwxMS4wNTE5MTA3LDkuMDk0MDE3MDkgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-PinSmall: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTcuNSwxMSBMMi41LDExIEMyLjIyMzg1NzYzLDExIDIsMTAuNzc2MTQyNCAyLDEwLjUgQzIsMTAuMjIzODU3NiAyLjIyMzg1NzYzLDEwIDIuNSwxMCBMNC4yNSwxMCBMNS43NSw0IEw0LjUsNCBDNC4yMjM4NTc2Myw0IDQsMy43NzYxNDIzNyA0LDMuNSBDNCwzLjIyMzg1NzYzIDQuMjIzODU3NjMsMyA0LjUsMyBMMTEuNSwzIEMxMS43NzYxNDI0LDMgMTIsMy4yMjM4NTc2MyAxMiwzLjUgQzEyLDMuNzc2MTQyMzcgMTEuNzc2MTQyNCw0IDExLjUsNCBMMTAuMjUsNCBMMTEuNzUsMTAgTDEzLjUsMTAgQzEzLjc3NjE0MjQsMTAgMTQsMTAuMjIzODU3NiAxNCwxMC41IEMxNCwxMC43NzYxNDI0IDEzLjc3NjE0MjQsMTEgMTMuNSwxMSBMOC41LDExIEw4LjUsMTQgTDcuNSwxNCBMNy41LDExIFogTTUuMjgwNzc2NDEsMTAgTDEwLjcxOTIyMzYsMTAgTDkuMjE5MjIzNTksNCBMNi43ODA3NzY0MSw0IEw1LjI4MDc3NjQxLDEwIFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg=='); --icon-Pivot: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMsMSBMMTMsMSBDMTQuMTA0NTY5NSwxIDE1LDEuODk1NDMwNSAxNSwzIEwxNSwxMyBDMTUsMTQuMTA0NTY5NSAxNC4xMDQ1Njk1LDE1IDEzLDE1IEwzLDE1IEMxLjg5NTQzMDUsMTUgMSwxNC4xMDQ1Njk1IDEsMTMgTDEsMyBDMSwxLjg5NTQzMDUgMS44OTU0MzA1LDEgMywxIFogTTUuMzc1LDQuNSBMNS4zNzUsNC43ODU0NjM0MSBMNy42MzQ2NTAxLDguMDIyNzA3MzIgTDUuMzc1LDExLjA5NDUxMjIgTDUuMzc1LDExLjUgTDEwLjYyNSwxMS41IEwxMC42MjUsMTAuMDMyODE3MSBMOS44MjY0OTkwMSwxMC43MjY0MTQ2IEw2LjkxOTczMTYxLDEwLjcyNjQxNDYgTDYuOTE5NzMxNjEsMTAuNDE2OTYzNCBMOC45MDI4MzMsNy42MTU4NTM2NiBMNi45MTk3MzE2MSw0Ljk5MDg1MzY2IEw5Ljc3NDMxMjEzLDQuOTg3MzUzNjYgTDEwLjYyNSw2LjA5NTMxNzA3IEwxMC42MjUsNC41IEw1LjM3NSw0LjUgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); + --icon-PivotLight: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cmVjdCBzdHJva2U9IiM5Nzk3OTciIHg9Ii41IiB5PSIuNSIgd2lkdGg9IjE1IiBoZWlnaHQ9IjE1IiByeD0iMiIvPjxwYXRoIGZpbGw9IiM5Nzk3OTciIGQ9Ik01LjM3NSA0LjVMMTAuNjI1IDQuNSAxMC42MjUgNi4wOTUzMTcwNyA5Ljc3NDMxMjEzIDQuOTg3MzUzNjYgNi45MTk3MzE2MSA0Ljk5MDg1MzY2IDguOTAyODMzIDcuNjE1ODUzNjYgNi45MTk3MzE2MSAxMC40MTY5NjM0IDYuOTE5NzMxNjEgMTAuNzI2NDE0NiA5LjgyNjQ5OTAxIDEwLjcyNjQxNDYgMTAuNjI1IDEwLjAzMjgxNzEgMTAuNjI1IDExLjUgNS4zNzUgMTEuNSA1LjM3NSAxMS4wOTQ1MTIyIDcuNjM0NjUwMSA4LjAyMjcwNzMyIDUuMzc1IDQuNzg1NDYzNDF6Ii8+PC9nPjwvc3ZnPg=='); --icon-Plus: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTguNSw3LjUgTDEzLDcuNSBDMTMuMjc2MTQyNCw3LjUgMTMuNSw3LjcyMzg1NzYzIDEzLjUsOCBDMTMuNSw4LjI3NjE0MjM3IDEzLjI3NjE0MjQsOC41IDEzLDguNSBMOC41LDguNSBMOC41LDEzIEM4LjUsMTMuMjc2MTQyNCA4LjI3NjE0MjM3LDEzLjUgOCwxMy41IEM3LjcyMzg1NzYzLDEzLjUgNy41LDEzLjI3NjE0MjQgNy41LDEzIEw3LjUsOC41IEwzLDguNSBDMi43MjM4NTc2Myw4LjUgMi41LDguMjc2MTQyMzcgMi41LDggQzIuNSw3LjcyMzg1NzYzIDIuNzIzODU3NjMsNy41IDMsNy41IEw3LjUsNy41IEw3LjUsMyBDNy41LDIuNzIzODU3NjMgNy43MjM4NTc2MywyLjUgOCwyLjUgQzguMjc2MTQyMzcsMi41IDguNSwyLjcyMzg1NzYzIDguNSwzIEw4LjUsNy41IFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg=='); --icon-Public: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMCAwSDE2VjE2SDB6Ii8+PGcgc3Ryb2tlPSIjMTZCMzc4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik05Ljc3NSAxMS4yOTNMNy4zMTIgMTAuNTkzQzcuMTIyMjE2NjEgMTAuNTM4OTUxMSA2Ljk4MTk3MzAyIDEwLjM3ODMzNyA2Ljk1NCAxMC4xODNMNi43NDcgOC43MjZDNy44MTQxMjAyNiA4LjIzODMwNjE1IDguNDk5MDAyMDkgNy4xNzMyODE3IDguNSA2TDguNSA0LjYyNkM4LjUyNTE5NzU2IDIuOTczMDAzMjUgNy4yNDI1MDU0NyAxLjU5NDE1MzM3IDUuNTkyIDEuNSA0Ljc4MDUzMjcgMS40NzUxMDMyIDMuOTkzNjIwMiAxLjc4MDE0ODI3IDMuNDEwOTUzMjMgMi4zNDU0Nzg0NiAyLjgyODI4NjI1IDIuOTEwODA4NjQgMi40OTk2MTgxNiAzLjY4ODE1MDk1IDIuNSA0LjVMMi41IDZDMi41MDA5OTc5MSA3LjE3MzI4MTcgMy4xODU4Nzk3NCA4LjIzODMwNjE1IDQuMjUzIDguNzI2TDQuMDQ2IDEwLjE3OUM0LjAxODAyNjk4IDEwLjM3NDMzNyAzLjg3Nzc4MzM5IDEwLjUzNDk1MTEgMy42ODggMTAuNTg5TDEuMjI1IDExLjI4OUMuNzk1OTk1Njg4IDExLjQxMTcwNzIuNTAwMTk4MjMgMTEuODAzNzkxOC41IDEyLjI1TC41IDE0LjUgMTAuNSAxNC41IDEwLjUgMTIuMjU0QzEwLjQ5OTgwMTggMTEuODA3NzkxOCAxMC4yMDQwMDQzIDExLjQxNTcwNzIgOS43NzUgMTEuMjkzek0xMi41IDE0LjVMMTUuNSAxNC41IDE1LjUgMTEuMjgxQzE1LjQ5OTk4NzkgMTAuODIyMzIwNiAxNS4xODc5MzExIDEwLjQyMjQ1OTEgMTQuNzQzIDEwLjMxMUwxMS44MjYgOS41ODJDMTEuNjI4NDcxIDkuNTMyNzA4NDEgMTEuNDgwNTUyNCA5LjM2ODU3NDEgMTEuNDUyIDkuMTY3TDExLjI0NyA3LjcyNkMxMi4zMTQxMjAzIDcuMjM4MzA2MTUgMTIuOTk5MDAyMSA2LjE3MzI4MTcgMTMgNUwxMyAzLjYyNkMxMy4wMjUxOTc2IDEuOTczMDAzMjUgMTEuNzQyNTA1NS41OTQxNTMzNjUgMTAuMDkyLjUgOS41MzQ0NjYxNC40ODI3MzcyNzIgOC45ODMxNjAzOS42MjEyNTYzMDYgOC41LjkiLz48L2c+PC9nPjwvc3ZnPg=='); --icon-PublicColor: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTQiIHdpZHRoPSIxNiI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMCAwSDE2VjE2SDB6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIC0xKSIvPjxnIG9wYWNpdHk9Ii43ODMiPjxwYXRoIGQ9Ik0gMTUuMjc1LDEwLjI5MyAxMy4wODcsOS42NjggQyAxMi43MjY0NzgsOS41NjQ5MTc4IDEyLjQ1MzkyLDkuMjY4ODEwNSAxMi4zODEsOC45MDEgTCAxMi4yNDcsOC4yMjYgQyAxMy4zMTQxMiw3LjczODMwNjEgMTMuOTk5MDAyLDYuNjczMjgxNyAxNCw1LjUgViA0LjEyNiBDIDE0LjAyNTE5OCwyLjQ3MzAwMzMgMTIuNzQyNTA2LDEuMDk0MTUzNCAxMS4wOTIsMSA5Ljg3OTM3MDksMC45NjM0MTA4MiA4Ljc2NDA2NTUsMS42NjA3NzI3IDguMjY2LDIuNzY3IDguNzQzMDQ3OCwzLjQ2MTI3ODUgOC45OTg5MTk4LDQuMjgzNjI0NyA5LDUuMTI2IFYgNi41IEMgOC45OTgyMDc3LDYuODYzNzcwNiA4Ljk0NjA0NTYsNy4yMjU1NDA0IDguODQ1LDcuNTc1IDkuMTA0MjMyNSw3Ljg0Njk0MjMgOS40MTIyMzk1LDguMDY3NzcxMSA5Ljc1Myw4LjIyNiBMIDkuNjE5LDguOSBDIDkuNTQ2MDc5Niw5LjI2NzgxMDUgOS4yNzM1MjE5LDkuNTYzOTE3OCA4LjkxMyw5LjY2NyBMIDguMDcsOS45MDggOS41NSwxMC4zMzEgYyAwLjg1Njk2NCwwLjI0NzU1NCAxLjQ0NzcwNiwxLjAzMDk5OSAxLjQ1LDEuOTIzIFYgMTQuNSBjIC0wLjAwMTcsMC4xNzA3MiAtMC4wMzI3OCwwLjMzOTg3MSAtMC4wOTIsMC41IEggMTUuNSBjIDAuMjc2MTQyLDAgMC41LC0wLjIyMzg1OCAwLjUsLTAuNSB2IC0zLjI0NiBjIC0xLjk4ZS00LC0wLjQ0NjIwOCAtMC4yOTU5OTYsLTAuODM4MjkzIC0wLjcyNSwtMC45NjEgeiIgZmlsbD0iI2U2YTExNyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtMSkiLz48cGF0aCBkPSJNIDkuMjc1LDExLjI5MyA3LjA4NywxMC42NjggQyA2LjcyNjIwMjMsMTAuNTY0NzU0IDYuNDUzNTgzLDEwLjI2ODE5NCA2LjM4MSw5LjkgTCA2LjI0Nyw5LjIyNSBDIDcuMzEzNzkxNCw4LjczNzQ1NjkgNy45OTg2MTEyLDcuNjcyOTE5NSA4LDYuNSBWIDUuMTI2IEMgOC4wMjUxOTc2LDMuNDczMDAzMyA2Ljc0MjUwNTUsMi4wOTQxNTM0IDUuMDkyLDIgNC4yODA1MzI3LDEuOTc1MTAzMiAzLjQ5MzYyMDIsMi4yODAxNDgzIDIuOTEwOTUzMiwyLjg0NTQ3ODUgMi4zMjgyODYzLDMuNDEwODA4NiAxLjk5OTYxODIsNC4xODgxNTA5IDIsNSB2IDEuNSBjIDkuOTc5ZS00LDEuMTczMjgxNyAwLjY4NTg3OTcsMi4yMzgzMDYyIDEuNzUzLDIuNzI2IEwgMy42MTksOS45IEMgMy41NDYwOCwxMC4yNjc4MTEgMy4yNzM1MjE5LDEwLjU2MzkxOCAyLjkxMywxMC42NjcgTCAwLjcyNSwxMS4yOTIgQyAwLjI5NTYzOTI2LDExLjQxNDgwOSAtMi40ODE3NzE2ZS00LDExLjgwNzQyMSAwLDEyLjI1NCBWIDE0LjUgQyAwLDE0Ljc3NjE0MiAwLjIyMzg1NzYzLDE1IDAuNSwxNSBoIDkgQyA5Ljc3NjE0MjQsMTUgMTAsMTQuNzc2MTQyIDEwLDE0LjUgViAxMi4yNTQgQyA5Ljk5OTgwMTgsMTEuODA3NzkyIDkuNzA0MDA0MywxMS40MTU3MDcgOS4yNzUsMTEuMjkzIFoiIGZpbGw9IiNmZmYiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTEpIi8+PC9nPjwvZz48L3N2Zz4='); diff --git a/static/ui-icons/UI/PivotLight.svg b/static/ui-icons/UI/PivotLight.svg new file mode 100644 index 00000000..97b31931 --- /dev/null +++ b/static/ui-icons/UI/PivotLight.svg @@ -0,0 +1,10 @@ + + + + Icons / UI / PivotLight + Created with Sketch. + + + + + \ No newline at end of file diff --git a/test/fixtures/docs/Hello.grist b/test/fixtures/docs/Hello.grist index 501add7e..67e0821e 100644 Binary files a/test/fixtures/docs/Hello.grist and b/test/fixtures/docs/Hello.grist differ