(core) Raw renames

Summary:
A new way for renaming tables.

  - There is a new popup to rename section (where you can also rename the table)
  - Renaming/Deleting page doesn't modify/delete the table.
  - Renaming table can rename a page if the names match (and the page contains a section with that table).
  - User can rename table in Raw Data UI in two ways - either on the listing or by using the section name popup
  - As before, there is no way to change tableId - it is derived from a table name.
  - When the section name is empty the table name is shown instead.
  - White space for section name is allowed (to discuss) - so the user can just paste '   '.
  - Empty name for a page is not allowed (but white space is).
  - Some bugs related to deleting tables with attached summary tables (and with undoing this operation) were fixed (but not all of them yet).

Test Plan: Updated tests.

Reviewers: georgegevoian

Reviewed By: georgegevoian

Subscribers: georgegevoian

Differential Revision: https://phab.getgrist.com/D3360
This commit is contained in:
Jarosław Sadziński
2022-04-27 19:46:24 +02:00
parent 8a1cca629b
commit 6f00106d7c
37 changed files with 946 additions and 452 deletions

View File

@@ -11,6 +11,18 @@ export function createPageRec(this: PageRec, docModel: DocModel): void {
this.view = refRecord(docModel.views, this.viewRef);
this.isHidden = ko.pureComputed(() => {
const name = this.view().name();
return !name || (name === 'GristDocTour' && !docModel.showDocTourTable);
const isTableHidden = () => {
const viewId = this.view().id();
const tables = docModel.rawTables.all();
const primaryTable = tables.find(t => t.primaryViewId() === viewId);
return !!primaryTable && primaryTable.isHidden();
};
// Page is hidden when any of this is true:
// - It has an empty name (or no name at all)
// - It is GristDocTour (unless user wants to see it)
// - It is a page generated for a hidden table TODO: Follow up - don't create
// pages for hidden tables.
// This is used currently only the left panel, to hide pages from the user.
return !name || (name === 'GristDocTour' && !docModel.showDocTourTable) || isTableHidden();
});
}

View File

@@ -1,9 +1,9 @@
import {KoArray} from 'app/client/lib/koArray';
import {DocModel, IRowModel, recordSet, refRecord, ViewSectionRec} from 'app/client/models/DocModel';
import {ColumnRec, ValidationRec, ViewRec} from 'app/client/models/DocModel';
import * as modelUtil from 'app/client/models/modelUtil';
import {MANUALSORT} from 'app/common/gristTypes';
import * as ko from 'knockout';
import toUpper = require('lodash/toUpper');
import * as randomcolor from 'randomcolor';
// Represents a user-defined table.
@@ -23,10 +23,16 @@ export interface TableRec extends IRowModel<"_grist_Tables"> {
// The list of grouped by columns.
groupByColumns: ko.Computed<ColumnRec[]>;
// The user-friendly name of the table, which is the same as tableId for non-summary tables,
// and is 'tableId[groupByCols...]' for summary tables.
tableTitle: ko.Computed<string>;
// Grouping description.
groupDesc: ko.PureComputed<string>;
// Name of the data table - title of the rawViewSection
// for summary table it is name of primary table.
tableName: modelUtil.KoSaveableObservable<string>;
// Table name with a default value (which is tableId).
tableNameDef: modelUtil.KoSaveableObservable<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>;
tableColor: string;
disableAddRemoveRows: ko.Computed<boolean>;
@@ -40,6 +46,10 @@ export function createTableRec(this: TableRec, docModel: DocModel): void {
this.primaryView = refRecord(docModel.views, this.primaryViewId);
this.rawViewSection = refRecord(docModel.viewSections, this.rawViewSectionRef);
this.summarySource = refRecord(docModel.tables, this.summarySourceTable);
this.isHidden = this.autoDispose(
// This is repeated logic from isHiddenTable.
ko.pureComputed(() => !!this.summarySourceTable() || this.tableId()?.startsWith("GristHidden"))
);
// A Set object of colRefs for all summarySourceCols of this table.
this.summarySourceColRefs = this.autoDispose(ko.pureComputed(() => new Set(
@@ -51,18 +61,12 @@ export function createTableRec(this: TableRec, docModel: DocModel): void {
this.groupByColumns = ko.pureComputed(() => this.columns().all().filter(c => c.summarySourceCol()));
const groupByDesc = ko.pureComputed(() => {
const groupBy = this.groupByColumns();
return groupBy.length ? 'by ' + groupBy.map(c => c.label()).join(", ") : "Totals";
});
// The user-friendly name of the table, which is the same as tableId for non-summary tables,
// and is 'tableId[groupByCols...]' for summary tables.
this.tableTitle = ko.pureComputed(() => {
if (this.summarySourceTable()) {
return toUpper(this.summarySource().tableId()) + " [" + groupByDesc() + "]";
this.groupDesc = ko.pureComputed(() => {
if (!this.summarySourceTable()) {
return '';
}
return toUpper(this.tableId());
const groupBy = this.groupByColumns();
return `[${groupBy.length ? 'by ' + groupBy.map(c => c.label()).join(", ") : "Totals"}]`;
});
// TODO: We should save this value and let users change it.
@@ -74,4 +78,40 @@ export function createTableRec(this: TableRec, docModel: DocModel): void {
this.disableAddRemoveRows = ko.pureComputed(() => Boolean(this.summarySourceTable()));
this.supportsManualSort = ko.pureComputed(() => this.columns().all().some(c => c.colId() === MANUALSORT));
this.tableName = modelUtil.savingComputed({
read: () => {
if (this.isDisposed()) {
return '';
}
if (this.summarySourceTable()) {
return this.summarySource().rawViewSection().title();
} else {
// Need to be extra careful here, rawViewSection might be disposed.
if (this.rawViewSection().isDisposed()) {
return '';
}
return this.rawViewSection().title();
}
},
write: (setter, val) => {
if (this.summarySourceTable()) {
setter(this.summarySource().rawViewSection().title, val);
} else {
setter(this.rawViewSection().title, val);
}
}
});
this.tableNameDef = modelUtil.fieldWithDefault(
this.tableName,
// TableId will be null/undefined when ACL will restrict access to it.
ko.computed(() => {
// During table removal, we could be disposed.
if (this.isDisposed()) {
return '';
}
const table = this.summarySourceTable() ? this.summarySource() : this;
return table.tableId() || '';
})
);
}

View File

@@ -44,8 +44,10 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
table: ko.Computed<TableRec>;
tableTitle: ko.Computed<string>;
// Widget title with a default value
titleDef: modelUtil.KoSaveableObservable<string>;
// Default widget title (the one that is used in titleDef).
defaultWidgetTitle: ko.PureComputed<string>;
// true if this record is its table's rawViewSection, i.e. a 'raw data view'
// in which case the UI prevents various things like hiding columns or changing the widget type.
@@ -166,6 +168,7 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
// List of selected rows
selectedRows: Observable<number[]>;
// Save all filters of fields/columns in the section.
saveFilters(): Promise<void>;
@@ -279,13 +282,25 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
this.table = refRecord(docModel.tables, this.tableRef);
this.tableTitle = this.autoDispose(ko.pureComputed(() => this.table().tableTitle()));
this.titleDef = modelUtil.fieldWithDefault(
this.title,
() => this.table().tableTitle() + (
(this.parentKey() === 'record') ? '' : ` ${getWidgetTypes(this.parentKey.peek() as any).label}`
)
);
// The user-friendly name of the table, which is the same as tableId for non-summary tables,
// and is 'tableId[groupByCols...]' for summary tables.
// Consist of 3 parts
// - TableId (or primary table id for summary tables) capitalized
// - Grouping description (table record contains this for summary tables)
// - Widget type description (if not grid)
// All concatenated separated by space.
this.defaultWidgetTitle = this.autoDispose(ko.pureComputed(() => {
const widgetTypeDesc = this.parentKey() !== 'record' ? `${getWidgetTypes(this.parentKey.peek() as any).label}` : '';
const table = this.table();
return [
table.tableNameDef()?.toUpperCase(), // Due to ACL this can be null.
table.groupDesc(),
widgetTypeDesc
].filter(part => Boolean(part?.trim())).join(' ');
}));
// Widget title.
this.titleDef = modelUtil.fieldWithDefault(this.title, this.defaultWidgetTitle);
// true if this record is its table's rawViewSection, i.e. a 'raw data view'
// in which case the UI prevents various things like hiding columns or changing the widget type.