mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(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:
@@ -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);
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user