(core) Show summary tables on Raw Data page

Summary:
Summary tables now have their own raw viewsection, and are shown
under Raw Data Tables on the Raw Data page.

Test Plan: Browser and Python tests.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D3495
pull/221/head
George Gevoian 2 years ago
parent 8bab8c18fa
commit a051830aeb

@ -20,10 +20,13 @@ export class DataTables extends Disposable {
private _view: Observable<string | null>;
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<any>(
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);
}
}

@ -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}`);
}

@ -182,7 +182,7 @@ export class Importer extends DisposableWithEvents {
private _destTables = Computed.create<Array<IOptionFull<DestId>>>(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.

@ -305,7 +305,7 @@ export class GristDocAPIImpl implements GristDocAPI {
}
public async listTables(): Promise<string[]> {
// 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[];

@ -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<TableRec>;
public rawTables: KoArray<TableRec>;
public allTableIds: KoArray<string>;
public visibleTables: KoArray<TableRec>;
public rawDataTables: KoArray<TableRec>;
public rawSummaryTables: KoArray<TableRec>;
public visibleTableIds: KoArray<string>;
// 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<TableRec>): KoArray<TableRec> {
const rowSource = new rowset.FilteredRowSource(r => !isHiddenTable(tablesModel.tableData, r));
function createTablesArray(
tablesModel: MetaTableModel<TableRec>,
filterFunc: RowFilterFunc<rowset.RowId> = (_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<TableRec>): KoArray<TableRec> {
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<TableRec>): KoArray<TableRec> {
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<TableRec>): KoArray<TableRec> {
return createTablesArray(tablesModel, r => !isSummaryTable(tablesModel.tableData, r));
}
/**
* Returns an observable array of raw summary tables, sorted by tableId.
*/
function createRawSummaryTablesArray(tablesModel: MetaTableModel<TableRec>): KoArray<TableRec> {
return createTablesArray(tablesModel, r => isSummaryTable(tablesModel.tableData, r));
}

@ -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,

@ -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

@ -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();
};

@ -30,6 +30,9 @@ export interface TableRec extends IRowModel<"_grist_Tables"> {
tableName: modelUtil.KoSaveableObservable<string>;
// Table name with a default value (which is tableId).
tableNameDef: modelUtil.KoSaveableObservable<string>;
// Like tableNameDef, but formatted to be more suitable for displaying to
// users (e.g. including group columns for summary tables).
formattedTableName: ko.PureComputed<string>;
// If user can select this table in various places.
// Note: Some hidden tables can still be visible on RawData view.
isHidden: ko.Computed<boolean>;
@ -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();
});
}

@ -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

@ -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;
}
`);

@ -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'})
)

@ -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",

@ -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

@ -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;
}

@ -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 = {

@ -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);

@ -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):

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

@ -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 {

@ -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,

@ -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=[

@ -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])

@ -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])

@ -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,

@ -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),
]),
]),
])

@ -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=[

@ -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 = ''

@ -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=');

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
<title>Icons / UI / PivotLight</title>
<desc>Created with Sketch.</desc>
<g id="Icons-/-UI-/-PivotLight" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" stroke="#979797" x="0.5" y="0.5" width="15" height="15" rx="2"></rect>
<polygon id="Path" fill="#979797" points="5.375 4.5 10.625 4.5 10.625 6.09531707 9.77431213 4.98735366 6.91973161 4.99085366 8.902833 7.61585366 6.91973161 10.4169634 6.91973161 10.7264146 9.82649901 10.7264146 10.625 10.0328171 10.625 11.5 5.375 11.5 5.375 11.0945122 7.6346501 8.02270732 5.375 4.78546341"></polygon>
</g>
</svg>

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.
Loading…
Cancel
Save