(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
This commit is contained in:
George Gevoian
2022-07-06 00:41:09 -07:00
parent 8bab8c18fa
commit a051830aeb
31 changed files with 452 additions and 308 deletions

View File

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

View File

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

View File

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

View File

@@ -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[];

View File

@@ -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);
// 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);
// 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 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 {
}
}
/**
* Creates an observable array of tables, sorted by tableId.
*
* An optional `filterFunc` may be specified to filter tables.
*/
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 hidden
* Returns an observable array of user tables, sorted by tableId, and excluding hidden/summary
* tables.
*/
function createUserTablesArray(tablesModel: MetaTableModel<TableRec>): KoArray<TableRec> {
const rowSource = new rowset.FilteredRowSource(r => !isHiddenTable(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));
}
/**
* Helper to create an observable array of tables, sorted by tableId, and excluding summary tables.
* Returns an observable array of raw data tables, sorted by tableId, and excluding 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 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));
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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