(core) Updating RawData views

Summary:
- Better focus on the widget title
- Adding columns only to the current view section
- New popup with options when user wants to delete a page
- New dialog to enter table name
- New table as a widget doesn't create a separate page
- Removing a table doesn't remove the primary view

Test Plan: Updated and new tests

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3410
pull/203/head
Jarosław Sadziński 2 years ago
parent 97f3a8805c
commit f194d6861b

@ -653,15 +653,22 @@ GridView.prototype.addNewColumn = function() {
.then(() => this.scrollPaneRight()); .then(() => this.scrollPaneRight());
}; };
GridView.prototype.insertColumn = function(index) { GridView.prototype.insertColumn = async function(index) {
var pos = tableUtil.fieldInsertPositions(this.viewSection.viewFields(), index)[0]; const pos = tableUtil.fieldInsertPositions(this.viewSection.viewFields(), index)[0];
var action = ['AddColumn', null, {"_position": pos}]; var action = ['AddColumn', null, {"_position": pos}];
return this.tableModel.sendTableAction(action) await this.gristDoc.docData.bundleActions('Insert column', async () => {
.bind(this).then(function() { const colInfo = await this.tableModel.sendTableAction(action);
this.selectColumn(index); if (!this.viewSection.isRaw.peek()){
this.currentEditingColumnIndex(index); const fieldInfo = {
// this.columnConfigTab.show(); colRef: colInfo.colRef,
parentPos: pos,
parentId: this.viewSection.id.peek()
};
await this.gristDoc.docModel.viewFields.sendTableAction(['AddRecord', null, fieldInfo]);
}
}); });
this.selectColumn(index);
this.currentEditingColumnIndex(index);
}; };
GridView.prototype.scrollPaneRight = function() { GridView.prototype.scrollPaneRight = function() {

@ -45,6 +45,7 @@ import {IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
import {IPageWidgetLink, linkFromId, selectBy} from 'app/client/ui/selectBy'; import {IPageWidgetLink, linkFromId, selectBy} from 'app/client/ui/selectBy';
import {startWelcomeTour} from 'app/client/ui/welcomeTour'; import {startWelcomeTour} from 'app/client/ui/welcomeTour';
import {isNarrowScreen, mediaSmall, testId} from 'app/client/ui2018/cssVars'; import {isNarrowScreen, mediaSmall, testId} from 'app/client/ui2018/cssVars';
import {invokePrompt} from 'app/client/ui2018/modals';
import {IconName} from 'app/client/ui2018/IconList'; import {IconName} from 'app/client/ui2018/IconList';
import {FieldEditor} from "app/client/widgets/FieldEditor"; import {FieldEditor} from "app/client/widgets/FieldEditor";
import {MinimalActionGroup} from 'app/common/ActionGroup'; import {MinimalActionGroup} from 'app/common/ActionGroup';
@ -500,7 +501,9 @@ export class GristDoc extends DisposableWithEvents {
* Sends an action to create a new empty table and switches to that table's primary view. * Sends an action to create a new empty table and switches to that table's primary view.
*/ */
public async addEmptyTable(): Promise<void> { public async addEmptyTable(): Promise<void> {
const tableInfo = await this.docData.sendAction(['AddEmptyTable']); const name = await this._promptForName();
if (name === undefined) { return; }
const tableInfo = await this.docData.sendAction(['AddEmptyTable', name || null]);
await this.openDocPage(this.docModel.tables.getRowModel(tableInfo.id).primaryViewId()); await this.openDocPage(this.docModel.tables.getRowModel(tableInfo.id).primaryViewId());
} }
@ -510,10 +513,16 @@ export class GristDoc extends DisposableWithEvents {
public async addWidgetToPage(val: IPageWidget) { public async addWidgetToPage(val: IPageWidget) {
const docData = this.docModel.docData; const docData = this.docModel.docData;
const viewName = this.viewModel.name.peek(); const viewName = this.viewModel.name.peek();
let tableId: string|null|undefined;
if (val.table === 'New Table') {
tableId = await this._promptForName();
if (tableId === undefined) {
return;
}
}
const res = await docData.bundleActions( const res = await docData.bundleActions(
`Added new linked section to view ${viewName}`, `Added new linked section to view ${viewName}`,
() => this.addWidgetToPageImpl(val) () => this.addWidgetToPageImpl(val, tableId ?? null)
); );
// The newly-added section should be given focus. // The newly-added section should be given focus.
@ -523,13 +532,12 @@ export class GristDoc extends DisposableWithEvents {
/** /**
* The actual implementation of addWidgetToPage * The actual implementation of addWidgetToPage
*/ */
public async addWidgetToPageImpl(val: IPageWidget) { public async addWidgetToPageImpl(val: IPageWidget, tableId: string|null = null) {
const viewRef = this.activeViewId.get(); const viewRef = this.activeViewId.get();
const tableRef = val.table === 'New Table' ? 0 : val.table;
const link = linkFromId(val.link); const link = linkFromId(val.link);
const tableRef = val.table === 'New Table' ? 0 : val.table;
const result = await this.docData.sendAction( const result = await this.docData.sendAction(
['CreateViewSection', tableRef, viewRef, val.type, val.summarize ? val.columns : null] ['CreateViewSection', tableRef, viewRef, val.type, val.summarize ? val.columns : null, tableId]
); );
if (val.type === 'chart') { if (val.type === 'chart') {
await this._ensureOneNumericSeries(result.sectionRef); await this._ensureOneNumericSeries(result.sectionRef);
@ -549,13 +557,15 @@ export class GristDoc extends DisposableWithEvents {
*/ */
public async addNewPage(val: IPageWidget) { public async addNewPage(val: IPageWidget) {
if (val.table === 'New Table') { if (val.table === 'New Table') {
const result = await this.docData.sendAction(['AddEmptyTable']); const name = await this._promptForName();
if (name === undefined) { return; }
const result = await this.docData.sendAction(['AddEmptyTable', name]);
await this.openDocPage(result.views[0].id); await this.openDocPage(result.views[0].id);
} else { } else {
let result: any; let result: any;
await this.docData.bundleActions(`Add new page`, async () => { await this.docData.bundleActions(`Add new page`, async () => {
result = await this.docData.sendAction( result = await this.docData.sendAction(
['CreateViewSection', val.table, 0, val.type, val.summarize ? val.columns : null] ['CreateViewSection', val.table, 0, val.type, val.summarize ? val.columns : null, null]
); );
if (val.type === 'chart') { if (val.type === 'chart') {
await this._ensureOneNumericSeries(result.sectionRef); await this._ensureOneNumericSeries(result.sectionRef);
@ -917,6 +927,10 @@ export class GristDoc extends DisposableWithEvents {
} }
} }
private async _promptForName() {
return await invokePrompt("Table name", "Create", '', "Default table name");
}
private async _replaceViewSection(section: ViewSectionRec, newVal: IPageWidget) { private async _replaceViewSection(section: ViewSectionRec, newVal: IPageWidget) {
const docModel = this.docModel; const docModel = this.docModel;

@ -186,7 +186,7 @@ async function createNewViewSections(docData: GristDoc['docData'], viewSections:
// Helper to create an action that add widget to the view with viewId. // Helper to create an action that add widget to the view with viewId.
function newViewSectionAction(widget: IPageWidget, viewId: number) { function newViewSectionAction(widget: IPageWidget, viewId: number) {
return ['CreateViewSection', widget.table, viewId, widget.type, widget.summarize ? widget.columns : null]; return ['CreateViewSection', widget.table, viewId, widget.type, widget.summarize ? widget.columns : null, null];
} }
/** /**

@ -167,11 +167,16 @@ export function buildPageWidgetPicker(
link: value.link.get(), link: value.link.get(),
section: value.section.get(), section: value.section.get(),
}); });
// If savePromise throws an error, before or after timeout, we let the error propagate as it if (value.table.get() === 'New Table') {
// should be handle by the caller. // Adding empty table will show a prompt, so we don't want to wait for it.
if (await isLongerThan(savePromise, DELAY_BEFORE_SPINNER_MS)) { await savePromise;
const label = getWidgetTypes(type).label; } else {
await spinnerModal(`Building ${label} widget`, savePromise); // If savePromise throws an error, before or after timeout, we let the error propagate as it
// should be handle by the caller.
if (await isLongerThan(savePromise, DELAY_BEFORE_SPINNER_MS)) {
const label = getWidgetTypes(type).label;
await spinnerModal(`Building ${label} widget`, savePromise);
}
} }
} }

@ -1,15 +1,19 @@
import { createGroup } from "app/client/components/commands"; import {createGroup} from 'app/client/components/commands';
import { duplicatePage } from "app/client/components/duplicatePage"; import {duplicatePage} from 'app/client/components/duplicatePage';
import { GristDoc } from "app/client/components/GristDoc"; import {GristDoc} from 'app/client/components/GristDoc';
import { PageRec } from "app/client/models/DocModel"; import {PageRec} from 'app/client/models/DocModel';
import { urlState } from "app/client/models/gristUrlState"; import {urlState} from 'app/client/models/gristUrlState';
import * as MetaTableModel from "app/client/models/MetaTableModel"; import * as MetaTableModel from 'app/client/models/MetaTableModel';
import { find as findInTree, fromTableData, TreeItemRecord, TreeRecord, import {find as findInTree, fromTableData, TreeItemRecord, TreeRecord,
TreeTableData} from "app/client/models/TreeModel"; TreeTableData} from 'app/client/models/TreeModel';
import { TreeViewComponent } from "app/client/ui/TreeViewComponent"; import {TreeViewComponent} from 'app/client/ui/TreeViewComponent';
import { buildPageDom, PageActions } from "app/client/ui2018/pages"; import {labeledCircleCheckbox} from 'app/client/ui2018/checkbox';
import { mod } from 'app/common/gutil'; import {colors} from 'app/client/ui2018/cssVars';
import { Computed, Disposable, dom, fromKo, observable, Observable } from "grainjs"; import {cssLink} from 'app/client/ui2018/links';
import {ISaveModalOptions, saveModal} from 'app/client/ui2018/modals';
import {buildPageDom, PageActions} from 'app/client/ui2018/pages';
import {mod} from 'app/common/gutil';
import {Computed, Disposable, dom, DomContents, fromKo, makeTestId, observable, Observable, styled} from 'grainjs';
// build dom for the tree view of pages // build dom for the tree view of pages
export function buildPagesDom(owner: Disposable, activeDoc: GristDoc, isOpen: Observable<boolean>) { export function buildPagesDom(owner: Disposable, activeDoc: GristDoc, isOpen: Observable<boolean>) {
@ -49,16 +53,18 @@ export function buildPagesDom(owner: Disposable, activeDoc: GristDoc, isOpen: Ob
return dom('div', dom.create(TreeViewComponent, model, {isOpen, selected, isReadonly: activeDoc.isReadonly})); return dom('div', dom.create(TreeViewComponent, model, {isOpen, selected, isReadonly: activeDoc.isReadonly}));
} }
function buildDomFromTable(pagesTable: MetaTableModel<PageRec>, activeDoc: GristDoc, id: number) { const testId = makeTestId('test-removepage-');
function buildDomFromTable(pagesTable: MetaTableModel<PageRec>, activeDoc: GristDoc, pageId: number) {
const {isReadonly} = activeDoc; const {isReadonly} = activeDoc;
const pageName = pagesTable.rowModels[id].view.peek().name; const pageName = pagesTable.rowModels[pageId].view.peek().name;
const viewId = pagesTable.rowModels[id].view.peek().id.peek(); const viewId = pagesTable.rowModels[pageId].view.peek().id.peek();
const docData = pagesTable.tableData.docData;
const actions: PageActions = { const actions: PageActions = {
onRename: (newName: string) => newName.length && pageName.saveOnly(newName), onRename: (newName: string) => newName.length && pageName.saveOnly(newName),
onRemove: () => docData.sendAction(['RemoveRecord', '_grist_Views', viewId]), onRemove: () => removeView(activeDoc, viewId, pageName.peek()),
// TODO: duplicate should prompt user for confirmation // TODO: duplicate should prompt user for confirmation
onDuplicate: () => duplicatePage(activeDoc, id), onDuplicate: () => duplicatePage(activeDoc, pageId),
// Can't remove last visible page // Can't remove last visible page
isRemoveDisabled: () => activeDoc.docModel.visibleDocPages.peek().length <= 1, isRemoveDisabled: () => activeDoc.docModel.visibleDocPages.peek().length <= 1,
isReadonly isReadonly
@ -67,6 +73,46 @@ function buildDomFromTable(pagesTable: MetaTableModel<PageRec>, activeDoc: Grist
return buildPageDom(fromKo(pageName), actions, urlState().setLinkUrl({docPage: viewId})); return buildPageDom(fromKo(pageName), actions, urlState().setLinkUrl({docPage: viewId}));
} }
function removeView(activeDoc: GristDoc, viewId: number, pageName: string) {
const docData = activeDoc.docData;
// Create a set with tables on other pages (but not on this one).
const tablesOnOtherViews = new Set(activeDoc.docModel.viewSections.rowModels
.filter(vs => !vs.isRaw.peek() && vs.parentId.peek() !== viewId)
.map(vs => vs.tableRef.peek()));
// Check if this page is a last page for some tables.
const notVisibleTables = [...new Set(activeDoc.docModel.viewSections.rowModels
.filter(vs => vs.parentId.peek() === viewId) // Get all sections on this view
.filter(vs => !vs.table.peek().summarySourceTable.peek()) // Sections that have normal tables
.filter(vs => !tablesOnOtherViews.has(vs.tableRef.peek())) // That aren't on other views
.filter(vs => vs.table.peek().tableId.peek()) // Which we can access (has tableId)
.map(vs => vs.table.peek()))]; // Return tableRec object, and remove duplicates.
const removePage = () => [['RemoveRecord', '_grist_Views', viewId]];
const removeAll = () => [
...removePage(),
...notVisibleTables.map(t => ['RemoveTable', t.tableId.peek()])
];
if (notVisibleTables.length) {
const tableNames = notVisibleTables.map(t => t.tableNameDef.peek());
buildPrompt(tableNames, async (option) => {
// Errors are handled in the dialog.
if (option === 'data') {
await docData.sendActions(removeAll(), `Remove page ${pageName} with tables ${tableNames}`);
} else if (option === 'page') {
await docData.sendActions(removePage(), `Remove only page ${pageName}`);
} else {
// This should not happen, as save should be disabled when no option is selected.
}
});
} else {
return docData.sendActions(removePage(), `Remove only page ${pageName}`);
}
}
type RemoveOption = '' | 'data' | 'page';
// Select another page in cyclic ordering of pages. Order is downard if given a positive `delta`, // Select another page in cyclic ordering of pages. Order is downard if given a positive `delta`,
// upward otherwise. // upward otherwise.
function otherPage(currentPage: TreeItemRecord, delta: number) { function otherPage(currentPage: TreeItemRecord, delta: number) {
@ -75,3 +121,94 @@ function otherPage(currentPage: TreeItemRecord, delta: number) {
const docPage = records[index].viewRef; const docPage = records[index].viewRef;
return urlState().pushUrl({docPage}); return urlState().pushUrl({docPage});
} }
function buildPrompt(tableNames: string[], onSave: (option: RemoveOption) => Promise<any>) {
saveModal((ctl, owner): ISaveModalOptions => {
const selected = Observable.create<RemoveOption>(owner, '');
const saveDisabled = Computed.create(owner, use => use(selected) === '');
const saveFunc = () => onSave(selected.get());
return {
title: `The following table${tableNames.length > 1 ? 's' : ''} will no longer be visible`,
body: dom('div',
testId('popup'),
buildWarning(tableNames),
cssOptions(
buildOption(selected, 'data', `Delete data and this page.`),
buildOption(selected, 'page',
[
`Delete this page, but do not delete data. `,
`Table will remain available in `,
cssLink(urlState().setHref({docPage: 'data'}), 'raw data page', { target: '_blank'}),
`.`
]),
)
),
saveDisabled,
saveLabel: 'Delete',
saveFunc,
width: 'fixed-wide',
extraButtons: [],
};
});
}
function buildOption(value: Observable<RemoveOption>, id: RemoveOption, content: DomContents) {
const selected = Computed.create(null, use => use(value) === id)
.onWrite(val => val ? value.set(id) : void 0);
return dom.update(
labeledCircleCheckbox(selected, content, dom.autoDispose(selected)),
testId(`option-${id}`),
cssBlockCheckbox.cls(''),
cssBlockCheckbox.cls('-block', selected),
);
}
function buildWarning(tables: string[]) {
return cssWarning(
dom.forEach(tables, (t) => cssTableName(t, testId('table')))
);
}
const cssOptions = styled('div', `
display: flex;
flex-direction: column;
gap: 10px;
`);
// We need to reset top and left of ::before element, as it is wrongly set
// on the inline checkbox.
// To simulate radio button behavior, we will block user input after option is selected, because
// checkbox doesn't support two-way binding.
const cssBlockCheckbox = styled('div', `
display: flex;
padding: 10px 8px;
border: 1px solid ${colors.mediumGrey};
border-radius: 3px;
cursor: pointer;
& input::before, & input::after {
top: unset;
left: unset;
}
&:hover {
border-color: ${colors.lightGreen};
}
&-block {
pointer-events: none;
}
&-block a {
pointer-events: all;
}
`);
const cssWarning = styled('div', `
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
`);
const cssTableName = styled('div', `
background: #eee;
padding: 3px 6px;
border-radius: 4px;
`);

@ -60,16 +60,18 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
// User input for table name. // User input for table name.
const inputTableName = Observable.create(ctrl, tableName); const inputTableName = Observable.create(ctrl, tableName);
// User input for widget title. // User input for widget title.
const inputWidgetTitle = Observable.create(ctrl, vs.title.peek()); const inputWidgetTitle = Observable.create(ctrl, vs.title.peek() ?? '');
// Placeholder for widget title: // Placeholder for widget title:
// - when widget title is empty shows a default widget title (what would be shown when title is empty) // - when widget title is empty shows a default widget title (what would be shown when title is empty)
// - when widget title is set, shows just a text to override it. // - when widget title is set, shows just a text to override it.
const inputWidgetPlaceholder = !vs.title.peek() ? 'Override widget title' : vs.defaultWidgetTitle.peek(); const inputWidgetPlaceholder = !vs.title.peek() ? 'Override widget title' : vs.defaultWidgetTitle.peek();
const disableSave = Computed.create(ctrl, (use) => const disableSave = Computed.create(ctrl, (use) => {
(use(inputTableName) === tableName || use(inputTableName).trim() === '') && const newTableName = use(inputTableName)?.trim() ?? '';
use(inputWidgetTitle) === vs.title.peek() const newWidgetTitle = use(inputWidgetTitle)?.trim() ?? '';
); // Can't save when table name is empty or there wasn't any change.
return !newTableName || (newTableName === tableName && newWidgetTitle === use(vs.title));
});
const modalCtl = ModalControl.create(ctrl, () => ctrl.close()); const modalCtl = ModalControl.create(ctrl, () => ctrl.close());
@ -88,9 +90,10 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
}; };
const saveWidgetTitle = async () => { const saveWidgetTitle = async () => {
const newTitle = inputWidgetTitle.get()?.trim() ?? '';
// If value was changed. // If value was changed.
if (inputWidgetTitle.get() !== vs.title.peek()) { if (newTitle !== vs.title.peek()) {
await vs.title.saveOnly(inputWidgetTitle.get()); await vs.title.saveOnly(newTitle);
} }
}; };
const doSave = modalCtl.doWork(() => Promise.all([ const doSave = modalCtl.doWork(() => Promise.all([
@ -99,23 +102,20 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
]), {close: true}); ]), {close: true});
function initialFocus() { function initialFocus() {
// Set focus on a thing user is likely to change. const isRawView = !widgetInput;
// Initial focus is set on tableName unless: const isWidgetTitleEmpty = !vs.title.peek();
// - if this is a summary table - as it is not editable, function focus(inputEl?: HTMLInputElement) {
// - if widgetTitle is not empty - so user wants to change it further, inputEl?.focus();
// - if widgetTitle is empty but the default widget name will have type suffix (like Table1 (Card)), so it won't inputEl?.select();
// be a table name - so user doesn't want the default value. }
if ( if (isSummary) {
!widgetInput || focus(widgetInput);
isSummary || } else if (isRawView) {
vs.title.peek() || focus(tableInput);
( } else if (isWidgetTitleEmpty) {
!vs.title.peek() && focus(tableInput);
vs.defaultWidgetTitle.peek().toUpperCase() !== tableRec.tableName.peek().toUpperCase() } else {
)) { focus(widgetInput);
widgetInput?.focus();
} else if (!isSummary) {
tableInput?.focus();
} }
} }

@ -34,6 +34,8 @@ export const cssLabel = styled('label', `
} }
`); `);
// TODO: the !important markings are to trump bootstrap, and should be removed when it's gone. // TODO: the !important markings are to trump bootstrap, and should be removed when it's gone.
export const cssCheckboxSquare = styled('input', ` export const cssCheckboxSquare = styled('input', `
-webkit-appearance: none; -webkit-appearance: none;

@ -7,7 +7,6 @@ import { dom, IDomArgs, Observable, styled } from 'grainjs';
* Styling for a simple green <A HREF> link. * Styling for a simple green <A HREF> link.
*/ */
// Match the font-weight of buttons.
export const cssLink = styled('a', ` export const cssLink = styled('a', `
color: ${colors.lightGreen}; color: ${colors.lightGreen};
--icon-color: ${colors.lightGreen}; --icon-color: ${colors.lightGreen};

@ -1,10 +1,11 @@
import {FocusLayer} from 'app/client/lib/FocusLayer'; import {FocusLayer} from 'app/client/lib/FocusLayer';
import {reportError} from 'app/client/models/errors'; import {reportError} from 'app/client/models/errors';
import {cssInput} from 'app/client/ui/MakeCopyMenu';
import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons'; import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons';
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars'; import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';
import {loadingSpinner} from 'app/client/ui2018/loaders'; import {loadingSpinner} from 'app/client/ui2018/loaders';
import {waitGrainObs} from 'app/common/gutil'; import {waitGrainObs} from 'app/common/gutil';
import {Computed, Disposable, dom, DomContents, DomElementArg, MultiHolder, Observable, styled} from 'grainjs'; import {Computed, Disposable, dom, DomContents, DomElementArg, input, MultiHolder, Observable, styled} from 'grainjs';
// IModalControl is passed into the function creating the body of the modal. // IModalControl is passed into the function creating the body of the modal.
export interface IModalControl { export interface IModalControl {
@ -287,6 +288,93 @@ export function confirmModal(
})); }));
} }
/**
* Creates a simple prompt modal (replacement for the native one).
* Closed via clicking anywhere outside the modal or Cancel button.
*
* Example usage:
* promptModal(
* "Enter your name",
* (name: string) => alert(`Hello ${name}`),
* "Ok" // Confirm button name,
* "John doe", // Initial text (can be empty or undefined)
* "Enter your name", // input placeholder
* () => console.log('User cancelled') // Called when user cancels, or clicks outside.
* )
*
* @param title: Prompt text.
* @param onConfirm: Handler for Confirm button.
* @param btnText: Text of the confirm button.
* @param initial: Initial value in the input element.
* @param placeholder: Placeholder for the input element.
* @param onCancel: Optional cancel handler.
*/
export function promptModal(
title: string,
onConfirm: (text: string) => Promise<unknown>,
btnText: string,
initial?: string,
placeholder?: string,
onCancel?: () => void
): void {
saveModal((ctl, owner): ISaveModalOptions => {
let confirmed = false;
const text = Observable.create(owner, initial ?? '');
const txtInput = input(text, { onInput : true }, { placeholder }, cssInput.cls(''), testId('modal-prompt'));
const options: ISaveModalOptions = {
title,
body: txtInput,
saveLabel: btnText,
saveFunc: () => {
// Mark that confirm was invoked.
confirmed = true;
return onConfirm(text.get() || '');
},
width: 'normal'
};
owner.onDispose(() => {
if (confirmed) { return; }
onCancel?.();
});
setTimeout(() => txtInput.focus(), 10);
return options;
});
}
/**
* Wraps prompt modal in a promise that is resolved either when user confirms or cancels.
* When user cancels the returned value is always undefined.
*
* Example usage:
* async handler() {
* const name = await invokePrompt("Please enter your name");
* if (name !== undefined) alert(`Hello ${name}`);
* }
*
* @param title: Prompt text.
* @param btnText: Text of the confirm button, default is "Ok".
* @param initial: Initial value in the input element.
* @param placeholder: Placeholder for the input element.
*/
export function invokePrompt(
title: string,
btnText?: string,
initial?: string,
placeholder?: string
): Promise<string|undefined> {
let onResolve: (text: string|undefined) => any;
const prom = new Promise<string|undefined>((resolve) => {
onResolve = resolve;
});
promptModal(title, onResolve!, btnText ?? 'Ok', initial, placeholder, () => {
if (onResolve) {
onResolve(undefined);
}
});
return prom;
}
/** /**
* Builds a simple spinner modal. The modal gets removed when `promise` resolves. * Builds a simple spinner modal. The modal gets removed when `promise` resolves.
*/ */

@ -611,7 +611,7 @@ export class ActiveDoc extends EventEmitter {
public addInitialTable(docSession: OptDocSession) { public addInitialTable(docSession: OptDocSession) {
// Use a non-client-specific session, so that this action is not part of anyone's undo history. // Use a non-client-specific session, so that this action is not part of anyone's undo history.
const newDocSession = makeExceptionalDocSession('nascent'); const newDocSession = makeExceptionalDocSession('nascent');
return this.applyUserActions(newDocSession, [["AddEmptyTable"]]); return this.applyUserActions(newDocSession, [["AddEmptyTable", null]]);
} }
/** /**

@ -185,9 +185,10 @@ class TestColumnActions(test_engine.EngineTestCase):
def init_sample_data(self): def init_sample_data(self):
# Add a new view with a section, and a new table to that view, and a summary table. # Add a new view with a section, and a new table to that view, and a summary table.
self.load_sample(self.sample2) self.load_sample(self.sample2)
self.apply_user_action(["CreateViewSection", 1, 0, "record", None]) self.apply_user_action(["CreateViewSection", 1, 0, "record", None, None])
self.apply_user_action(["CreateViewSection", 0, 1, "record", None]) self.apply_user_action(["AddEmptyTable", None])
self.apply_user_action(["CreateViewSection", 1, 1, "record", [12]]) self.apply_user_action(["CreateViewSection", 2, 1, "record", None, None])
self.apply_user_action(["CreateViewSection", 1, 1, "record", [12], None])
self.apply_user_action(["BulkAddRecord", "Table1", [None]*3, { self.apply_user_action(["BulkAddRecord", "Table1", [None]*3, {
"A": ["a", "b", "c"], "A": ["a", "b", "c"],
"B": ["d", "e", "f"], "B": ["d", "e", "f"],

@ -57,7 +57,7 @@ class TestDerived(test_engine.EngineTestCase):
self.load_sample(self.sample) self.load_sample(self.sample)
# Create a derived table summarizing count and total of orders by year. # Create a derived table summarizing count and total of orders by year.
self.apply_user_action(["CreateViewSection", 2, 0, 'record', [10]]) self.apply_user_action(["CreateViewSection", 2, 0, 'record', [10], None])
# Check the results. # Check the results.
self.assertPartialData("GristSummary_6_Orders", ["id", "year", "count", "amount", "group" ], [ self.assertPartialData("GristSummary_6_Orders", ["id", "year", "count", "amount", "group" ], [
@ -134,7 +134,7 @@ class TestDerived(test_engine.EngineTestCase):
""" """
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 2, 0, 'record', [10, 12]]) self.apply_user_action(["CreateViewSection", 2, 0, 'record', [10, 12], None])
self.assertPartialData("GristSummary_6_Orders", [ self.assertPartialData("GristSummary_6_Orders", [
"id", "year", "product", "count", "amount", "group" "id", "year", "product", "count", "amount", "group"
], [ ], [
@ -193,7 +193,7 @@ class TestDerived(test_engine.EngineTestCase):
self.load_sample(self.sample) self.load_sample(self.sample)
# Create a summary on the Customers table. Adding orders involves a lookup for each customer. # Create a summary on the Customers table. Adding orders involves a lookup for each customer.
self.apply_user_action(["CreateViewSection", 1, 0, 'record', [3]]) self.apply_user_action(["CreateViewSection", 1, 0, 'record', [3], None])
self.add_column("GristSummary_9_Customers", "totalAmount", self.add_column("GristSummary_9_Customers", "totalAmount",
formula="sum(sum(Orders.lookupRecords(customer=c).amount) for c in $group)") formula="sum(sum(Orders.lookupRecords(customer=c).amount) for c in $group)")
@ -263,7 +263,7 @@ class TestDerived(test_engine.EngineTestCase):
self.load_sample(self.sample) self.load_sample(self.sample)
# Create a summary table summarizing count and total of orders by year. # Create a summary table summarizing count and total of orders by year.
self.apply_user_action(["CreateViewSection", 2, 0, 'record', [10]]) self.apply_user_action(["CreateViewSection", 2, 0, 'record', [10], None])
self.assertPartialData("GristSummary_6_Orders", ["id", "year", "count", "amount", "group" ], [ self.assertPartialData("GristSummary_6_Orders", ["id", "year", "count", "amount", "group" ], [
[1, 2012, 1, 15.0, [1]], [1, 2012, 1, 15.0, [1]],
[2, 2013, 2, 30.0, [2,3]], [2, 2013, 2, 30.0, [2,3]],

@ -135,7 +135,7 @@ class TestUserActions(test_engine.EngineTestCase):
]) ])
# Add a new column with a formula. # Add a new column with a formula.
self.apply_user_action(['AddColumn', 'Favorites', 'fav_viewers', { self.apply_user_action(['AddVisibleColumn', 'Favorites', 'fav_viewers', {
'formula': '$favorite.viewers' 'formula': '$favorite.viewers'
}]) }])
# Add a field back for the favorites table and set its display formula to the # Add a field back for the favorites table and set its display formula to the

@ -201,7 +201,7 @@ class TestRules(test_engine.EngineTestCase):
# Test that rules are removed with a column when attached to a field. # Test that rules are removed with a column when attached to a field.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(['CreateViewSection', 1, 0, 'record', None]) self.apply_user_action(['CreateViewSection', 1, 0, 'record', None, None])
self.field_add_empty(2) self.field_add_empty(2)
self.field_set_rule(2, 0, "$Stock == 0") self.field_set_rule(2, 0, "$Stock == 0")
before = self.engine.docmodel.columns.lookupOne(colId='gristHelper_ConditionalRule') before = self.engine.docmodel.columns.lookupOne(colId='gristHelper_ConditionalRule')
@ -218,7 +218,7 @@ class TestRules(test_engine.EngineTestCase):
# Test that rules are removed with a field. # Test that rules are removed with a field.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(['CreateViewSection', 1, 0, 'record', None]) self.apply_user_action(['CreateViewSection', 1, 0, 'record', None, None])
self.field_add_empty(2) self.field_add_empty(2)
self.field_set_rule(2, 0, "$Stock == 0") self.field_set_rule(2, 0, "$Stock == 0")
rule_id = self.engine.docmodel.columns.lookupOne(colId='gristHelper_ConditionalRule').id rule_id = self.engine.docmodel.columns.lookupOne(colId='gristHelper_ConditionalRule').id

@ -96,7 +96,7 @@ class TestSummary(test_engine.EngineTestCase):
self.assertViews([]) self.assertViews([])
# Create a view + section for the initial table. # Create a view + section for the initial table.
self.apply_user_action(["CreateViewSection", 1, 0, "record", None]) self.apply_user_action(["CreateViewSection", 1, 0, "record", None, None])
# Verify that we got a new view, with one section, and three fields. # Verify that we got a new view, with one section, and three fields.
self.assertTables([self.starting_table]) self.assertTables([self.starting_table])
@ -112,7 +112,7 @@ class TestSummary(test_engine.EngineTestCase):
self.assertTableData("Address", self.starting_table_data) self.assertTableData("Address", self.starting_table_data)
# Create a "Totals" section, i.e. a summary with no group-by columns. # Create a "Totals" section, i.e. a summary with no group-by columns.
self.apply_user_action(["CreateViewSection", 1, 0, "record", []]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [], None])
# Verify that a new table gets created, and a new view, with a section for that table, # Verify that a new table gets created, and a new view, with a section for that table,
# and some auto-generated summary fields. # and some auto-generated summary fields.
@ -141,7 +141,7 @@ class TestSummary(test_engine.EngineTestCase):
]) ])
# Create a summary section, grouped by the "State" column. # Create a summary section, grouped by the "State" column.
self.apply_user_action(["CreateViewSection", 1, 0, "record", [12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [12], None])
# Verify that a new table gets created again, a new view, and a section for that table. # Verify that a new table gets created again, a new view, and a section for that table.
# Note that we also check that summarySourceTable and summarySourceCol fields are correct. # Note that we also check that summarySourceTable and summarySourceCol fields are correct.
@ -182,7 +182,7 @@ class TestSummary(test_engine.EngineTestCase):
]) ])
# Create a summary section grouped by two columns ("city" and "state"). # Create a summary section grouped by two columns ("city" and "state").
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
# Verify the new table and views. # Verify the new table and views.
summary_table3 = Table(4, "GristSummary_7_Address3", primaryViewId=0, summarySourceTable=1, summary_table3 = Table(4, "GristSummary_7_Address3", primaryViewId=0, summarySourceTable=1,
@ -229,8 +229,8 @@ class TestSummary(test_engine.EngineTestCase):
def test_summary_gencode(self): def test_summary_gencode(self):
self.maxDiff = 1000 # If there is a discrepancy, allow the bigger diff. self.maxDiff = 1000 # If there is a discrepancy, allow the bigger diff.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", []]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [], None])
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
self.assertMultiLineEqual(self.engine.fetch_table_schema(), self.assertMultiLineEqual(self.engine.fetch_table_schema(),
"""import grist """import grist
from functions import * # global uppercase functions from functions import * # global uppercase functions
@ -266,7 +266,7 @@ class Address:
self.load_sample(self.sample) self.load_sample(self.sample)
# Create a summary section grouped by two columns ("city" and "state"). # Create a summary section grouped by two columns ("city" and "state").
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
# Verify the new table and views. # Verify the new table and views.
summary_table = Table(2, "GristSummary_7_Address", primaryViewId=0, summarySourceTable=1, summary_table = Table(2, "GristSummary_7_Address", primaryViewId=0, summarySourceTable=1,
@ -293,8 +293,8 @@ class Address:
# Create twoo other views + view sections with the same breakdown (in different order # Create twoo other views + view sections with the same breakdown (in different order
# of group-by fields, which should still reuse the same table). # of group-by fields, which should still reuse the same table).
self.apply_user_action(["CreateViewSection", 1, 0, "record", [12,11]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [12,11], None])
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
summary_view2 = View(2, sections=[ summary_view2 = View(2, sections=[
Section(2, parentKey="record", tableRef=2, fields=[ Section(2, parentKey="record", tableRef=2, fields=[
Field(5, colRef=15), Field(5, colRef=15),
@ -337,8 +337,8 @@ class Address:
# Load table and create a couple summary sections, for totals, and grouped by "state". # Load table and create a couple summary sections, for totals, and grouped by "state".
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", []]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [], None])
self.apply_user_action(["CreateViewSection", 1, 0, "record", [12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [12], None])
self.assertTables([ self.assertTables([
self.starting_table, self.starting_table,
@ -376,8 +376,8 @@ class Address:
]) ])
# Now create similar summary sections for the new table. # Now create similar summary sections for the new table.
self.apply_user_action(["CreateViewSection", 4, 0, "record", []]) self.apply_user_action(["CreateViewSection", 4, 0, "record", [], None])
self.apply_user_action(["CreateViewSection", 4, 0, "record", [23]]) self.apply_user_action(["CreateViewSection", 4, 0, "record", [23], None])
# Make sure this creates new section rather than reuses similar ones for the wrong table. # Make sure this creates new section rather than reuses similar ones for the wrong table.
self.assertTables([ self.assertTables([
@ -421,7 +421,7 @@ class Address:
# Load sample and create a summary section grouped by two columns ("city" and "state"). # Load sample and create a summary section grouped by two columns ("city" and "state").
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
# Verify that the summary table respects all updates to the source table. # Verify that the summary table respects all updates to the source table.
self._do_test_updates("Address", "GristSummary_7_Address") self._do_test_updates("Address", "GristSummary_7_Address")
@ -536,7 +536,7 @@ class Address:
# Load sample and create a couple of summary sections. # Load sample and create a couple of summary sections.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
# Check what tables we have now. # Check what tables we have now.
self.assertPartialData("_grist_Tables", ["id", "tableId", "summarySourceTable"], [ self.assertPartialData("_grist_Tables", ["id", "tableId", "summarySourceTable"], [
@ -561,8 +561,8 @@ class Address:
# Similar to the above, verify renames, but now with two summary tables. # Similar to the above, verify renames, but now with two summary tables.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
self.apply_user_action(["CreateViewSection", 1, 0, "record", []]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [], None])
self.assertPartialData("_grist_Tables", ["id", "tableId", "summarySourceTable"], [ self.assertPartialData("_grist_Tables", ["id", "tableId", "summarySourceTable"], [
[1, "Address", 0], [1, "Address", 0],
[2, "GristSummary_7_Address", 1], [2, "GristSummary_7_Address", 1],
@ -604,8 +604,8 @@ class Address:
# table, sharing all formulas and differing only in the group-by columns.) # table, sharing all formulas and differing only in the group-by columns.)
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
self.apply_user_action(["CreateViewSection", 1, 0, "record", []]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [], None])
# These are the tables and columns we automatically get. # These are the tables and columns we automatically get.
self.assertTables([ self.assertTables([
@ -667,7 +667,7 @@ class Address:
]) ])
# Add a new summary table, and check that it gets the new formula. # Add a new summary table, and check that it gets the new formula.
self.apply_user_action(["CreateViewSection", 1, 0, "record", [12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [12], None])
self.assertTables([ self.assertTables([
self.starting_table, self.starting_table,
Table(2, "GristSummary_7_Address", 0, 1, columns=[ Table(2, "GristSummary_7_Address", 0, 1, columns=[
@ -710,9 +710,9 @@ class Address:
# Verify that we can convert the type of a column when there is a summary table using that # Verify that we can convert the type of a column when there is a summary table using that
# column to group by. Since converting generates extra summary records, this may cause bugs. # column to group by. Since converting generates extra summary records, this may cause bugs.
self.apply_user_action(["AddEmptyTable"]) self.apply_user_action(["AddEmptyTable", None])
self.apply_user_action(["BulkAddRecord", "Table1", [None]*3, {"A": [10,20,10], "B": [1,2,3]}]) self.apply_user_action(["BulkAddRecord", "Table1", [None]*3, {"A": [10,20,10], "B": [1,2,3]}])
self.apply_user_action(["CreateViewSection", 1, 0, "record", [2]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [2], None])
# Verify metadata and actual data initially. # Verify metadata and actual data initially.
self.assertTables([ self.assertTables([
@ -778,10 +778,10 @@ class Address:
# Verify that we can remove a column when there is a summary table using that column to group # Verify that we can remove a column when there is a summary table using that column to group
# by. (Bug T188.) # by. (Bug T188.)
self.apply_user_action(["AddEmptyTable"]) self.apply_user_action(["AddEmptyTable", None])
self.apply_user_action(["BulkAddRecord", "Table1", [None]*3, self.apply_user_action(["BulkAddRecord", "Table1", [None]*3,
{"A": ['a','b','c'], "B": [1,1,2], "C": [4,5,6]}]) {"A": ['a','b','c'], "B": [1,1,2], "C": [4,5,6]}])
self.apply_user_action(["CreateViewSection", 1, 0, "record", [2,3]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [2,3], None])
# Verify metadata and actual data initially. # Verify metadata and actual data initially.
self.assertTables([ self.assertTables([

@ -27,8 +27,8 @@ class TestSummary2(test_engine.EngineTestCase):
# Start as in test_change_summary_formula() test case; see there for what tables and columns # Start as in test_change_summary_formula() test case; see there for what tables and columns
# we expect to have at this point. # we expect to have at this point.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
self.apply_user_action(["CreateViewSection", 1, 0, "record", []]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [], None])
# Check that we cannot add a non-formula column. # Check that we cannot add a non-formula column.
with self.assertRaisesRegex(ValueError, r'non-formula column'): with self.assertRaisesRegex(ValueError, r'non-formula column'):
@ -37,16 +37,17 @@ class TestSummary2(test_engine.EngineTestCase):
# Add two formula columns: one for 'state' (an existing column name, and a group-by column in # Add two formula columns: one for 'state' (an existing column name, and a group-by column in
# some tables), and one for 'average' (a new column name). # some tables), and one for 'average' (a new column name).
self.apply_user_action(["AddColumn", "GristSummary_7_Address2", "state", self.apply_user_action(["AddVisibleColumn", "GristSummary_7_Address2", "state",
{"formula": "':'.join(sorted(set($group.state)))"}]) {"formula": "':'.join(sorted(set($group.state)))"}])
self.apply_user_action(["AddColumn", "GristSummary_7_Address", "average",
self.apply_user_action(["AddVisibleColumn", "GristSummary_7_Address", "average",
{"formula": "$amount / $count"}]) {"formula": "$amount / $count"}])
# Add two more summary tables: by 'city', and by 'state', and see what columns they get. # Add two more summary tables: by 'city', and by 'state', and see what columns they get.
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11], None])
self.apply_user_action(["CreateViewSection", 1, 0, "record", [12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [12], None])
# And also a summary table for an existing breakdown. # And also a summary table for an existing breakdown.
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
# Check the table and columns for all the summary tables. # Check the table and columns for all the summary tables.
self.assertTables([ self.assertTables([
@ -166,8 +167,8 @@ class TestSummary2(test_engine.EngineTestCase):
# Start as in test_change_summary_formula() test case; see there for what tables and columns # Start as in test_change_summary_formula() test case; see there for what tables and columns
# we expect to have at this point. # we expect to have at this point.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
self.apply_user_action(["CreateViewSection", 1, 0, "record", []]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [], None])
# Check that we cannot rename a summary group-by column. (Perhaps it's better to raise an # Check that we cannot rename a summary group-by column. (Perhaps it's better to raise an
# exception, but currently we translate the invalid request to a no-op.) # exception, but currently we translate the invalid request to a no-op.)
@ -336,8 +337,8 @@ class TestSummary2(test_engine.EngineTestCase):
# (5) no renaming summary tables. # (5) no renaming summary tables.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
self.apply_user_action(["CreateViewSection", 1, 0, "record", []]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [], None])
self.assertTableData('GristSummary_7_Address', cols="all", data=[ self.assertTableData('GristSummary_7_Address', cols="all", data=[
[ "id", "city", "state", "group", "count", "amount" ], [ "id", "city", "state", "group", "count", "amount" ],
@ -418,7 +419,7 @@ class TestSummary2(test_engine.EngineTestCase):
return [c for c in self.engine.tables[table_id].all_columns if c.startswith('#summary#')] return [c for c in self.engine.tables[table_id].all_columns if c.startswith('#summary#')]
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
# We should have a single summary table, and a single section referring to it. # We should have a single summary table, and a single section referring to it.
self.assertTables([ self.assertTables([
@ -582,7 +583,7 @@ class TestSummary2(test_engine.EngineTestCase):
self.assertEqual(get_helper_cols('Address'), ['#summary#GristSummary_7_Address']) self.assertEqual(get_helper_cols('Address'), ['#summary#GristSummary_7_Address'])
# Now add a different view section with the same group-by columns. # Now add a different view section with the same group-by columns.
self.apply_user_action(["CreateViewSection", 1, 1, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 1, "record", [11,12], None])
self.assertTables([ self.assertTables([
self.starting_table, self.starting_table,
Table(6, "GristSummary_7_Address", 0, 1, columns=[ Table(6, "GristSummary_7_Address", 0, 1, columns=[
@ -737,8 +738,8 @@ class TestSummary2(test_engine.EngineTestCase):
# Verify that if we add a group-by column that conflicts with a formula, group-by column wins. # Verify that if we add a group-by column that conflicts with a formula, group-by column wins.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [12], None])
self.apply_user_action(["AddColumn", "GristSummary_7_Address", "city", self.apply_user_action(["AddVisibleColumn", "GristSummary_7_Address", "city",
{"formula": "$state.lower()"}]) {"formula": "$state.lower()"}])
# We should have a single summary table, and a single section referring to it. # We should have a single summary table, and a single section referring to it.
@ -801,10 +802,10 @@ class TestSummary2(test_engine.EngineTestCase):
# Create one view with one summary section, and another view with three sections. # Create one view with one summary section, and another view with three sections.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) # Creates View #1 self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None]) # Creates View #1
self.apply_user_action(["CreateViewSection", 1, 0, "record", []]) # Creates View #2 self.apply_user_action(["CreateViewSection", 1, 0, "record", [], None]) # Creates View #2
self.apply_user_action(["CreateViewSection", 1, 2, "record", [11,12]]) # Refers to View #2 self.apply_user_action(["CreateViewSection", 1, 2, "record", [11,12], None]) # Refers to View #2
self.apply_user_action(["CreateViewSection", 1, 2, "record", [12]]) # Refers to View #2 self.apply_user_action(["CreateViewSection", 1, 2, "record", [12], None]) # Refers to View #2
# We should have a single summary table, and a single section referring to it. # We should have a single summary table, and a single section referring to it.
self.assertTables([ self.assertTables([
@ -883,7 +884,7 @@ class TestSummary2(test_engine.EngineTestCase):
# Verify that we correctly update sort spec when we update a summary view section. # Verify that we correctly update sort spec when we update a summary view section.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) 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", 1,
{"sortColRefs": "[15,14,-17]"}]) {"sortColRefs": "[15,14,-17]"}])
@ -927,10 +928,10 @@ class TestSummary2(test_engine.EngineTestCase):
self.load_sample(self.sample) self.load_sample(self.sample)
# Add a couple of summary tables. # Add a couple of summary tables.
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
self.apply_user_action(["CreateViewSection", 1, 0, "record", []]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [], None])
# Add a formula column # Add a formula column
self.apply_user_action(["AddColumn", "GristSummary_7_Address", "average", self.apply_user_action(["AddVisibleColumn", "GristSummary_7_Address", "average",
{"formula": "$amount / $count"}]) {"formula": "$amount / $count"}])
# Check the table and columns for all the summary tables. # Check the table and columns for all the summary tables.
@ -1037,11 +1038,11 @@ class TestSummary2(test_engine.EngineTestCase):
# Add a summary table and detach it. Then add a summary table of that table. # Add a summary table and detach it. Then add a summary table of that table.
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11,12], None])
self.apply_user_action(["DetachSummaryViewSection", 1]) self.apply_user_action(["DetachSummaryViewSection", 1])
# Create a summary of the detached table (tableRef 3) by state (colRef 21). # Create a summary of the detached table (tableRef 3) by state (colRef 21).
self.apply_user_action(["CreateViewSection", 3, 0, "record", [21]]) self.apply_user_action(["CreateViewSection", 3, 0, "record", [21], None])
# Verify the resulting metadata. # Verify the resulting metadata.
self.assertTables([ self.assertTables([

@ -43,7 +43,7 @@ class TestSummaryChoiceList(EngineTestCase):
self.assertViews([]) self.assertViews([])
# Create a summary section, grouped by the "choices1" column. # Create a summary section, grouped by the "choices1" column.
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11], None])
summary_table1 = Table( summary_table1 = Table(
2, "GristSummary_6_Source", primaryViewId=0, summarySourceTable=1, 2, "GristSummary_6_Source", primaryViewId=0, summarySourceTable=1,
@ -57,7 +57,7 @@ class TestSummaryChoiceList(EngineTestCase):
) )
# Create another summary section, grouped by both choicelist columns. # Create another summary section, grouped by both choicelist columns.
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11, 12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11, 12], None])
summary_table2 = Table( summary_table2 = Table(
3, "GristSummary_6_Source2", primaryViewId=0, summarySourceTable=1, 3, "GristSummary_6_Source2", primaryViewId=0, summarySourceTable=1,
@ -72,7 +72,7 @@ class TestSummaryChoiceList(EngineTestCase):
) )
# Create another summary section, grouped by the non-choicelist column # Create another summary section, grouped by the non-choicelist column
self.apply_user_action(["CreateViewSection", 1, 0, "record", [10]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [10], None])
summary_table3 = Table( summary_table3 = Table(
4, "GristSummary_6_Source3", primaryViewId=0, summarySourceTable=1, 4, "GristSummary_6_Source3", primaryViewId=0, summarySourceTable=1,
@ -86,7 +86,7 @@ class TestSummaryChoiceList(EngineTestCase):
) )
# Create another summary section, grouped by the non-choicelist column and choices1 # Create another summary section, grouped by the non-choicelist column and choices1
self.apply_user_action(["CreateViewSection", 1, 0, "record", [10, 11]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [10, 11], None])
summary_table4 = Table( summary_table4 = Table(
5, "GristSummary_6_Source4", primaryViewId=0, summarySourceTable=1, 5, "GristSummary_6_Source4", primaryViewId=0, summarySourceTable=1,
@ -321,7 +321,7 @@ class TestSummaryChoiceList(EngineTestCase):
self.assertViews([]) self.assertViews([])
# Create a summary section, grouped by the "choices1" column. # Create a summary section, grouped by the "choices1" column.
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11], None])
summary_table = Table( summary_table = Table(
2, "GristSummary_6_Source", primaryViewId=0, summarySourceTable=1, 2, "GristSummary_6_Source", primaryViewId=0, summarySourceTable=1,
@ -359,7 +359,7 @@ class TestSummaryChoiceList(EngineTestCase):
self.load_sample(self.sample) self.load_sample(self.sample)
# Create a summary section, grouped by both choicelist columns. # Create a summary section, grouped by both choicelist columns.
self.apply_user_action(["CreateViewSection", 1, 0, "record", [11, 12]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [11, 12], None])
summary_table = Table( summary_table = Table(
2, "GristSummary_6_Source", primaryViewId=0, summarySourceTable=1, 2, "GristSummary_6_Source", primaryViewId=0, summarySourceTable=1,

@ -30,7 +30,7 @@ class TestSummaryUndo(test_engine.EngineTestCase):
# This tests a particular case of a bug when a summary table wasn't fully updated after UNDO. # This tests a particular case of a bug when a summary table wasn't fully updated after UNDO.
self.load_sample(self.sample) self.load_sample(self.sample)
# Create a summary section, grouped by the "State" column. # Create a summary section, grouped by the "State" column.
self.apply_user_action(["CreateViewSection", 1, 0, "record", [1]]) self.apply_user_action(["CreateViewSection", 1, 0, "record", [1], None])
self.assertTableData('GristSummary_6_Person', cols="subset", data=[ self.assertTableData('GristSummary_6_Person', cols="subset", data=[
[ "id", "state", "count"], [ "id", "state", "count"],
[ 1, "NY", 2], [ 1, "NY", 2],

@ -53,9 +53,9 @@ class TestTableActions(test_engine.EngineTestCase):
self.apply_user_action(["BulkAddRecord", "People", d.row_ids, d.columns]) self.apply_user_action(["BulkAddRecord", "People", d.row_ids, d.columns])
# Add a view with several sections, including a summary table. # Add a view with several sections, including a summary table.
self.apply_user_action(["CreateViewSection", 1, 0, 'record', None]) self.apply_user_action(["CreateViewSection", 1, 0, 'record', None, None])
self.apply_user_action(["CreateViewSection", 1, 3, 'record', [3]]) self.apply_user_action(["CreateViewSection", 1, 3, 'record', [3], None])
self.apply_user_action(["CreateViewSection", 2, 3, 'record', None]) self.apply_user_action(["CreateViewSection", 2, 3, 'record', None, None])
# Verify the new structure of tables and views. # Verify the new structure of tables and views.
self.assertTables([ self.assertTables([
@ -211,7 +211,7 @@ class TestTableActions(test_engine.EngineTestCase):
self.init_sample_data() self.init_sample_data()
# Add a table grouped by a reference column (the 'Ref:Address' column named 'address'). # Add a table grouped by a reference column (the 'Ref:Address' column named 'address').
self.apply_user_action(["CreateViewSection", 2, 0, 'record', [7]]) self.apply_user_action(["CreateViewSection", 2, 0, 'record', [7], None])
self.assertTableData('_grist_Tables_column', cols="subset", data=[ self.assertTableData('_grist_Tables_column', cols="subset", data=[
["id", "colId", "type", "isFormula", "formula" ], ["id", "colId", "type", "isFormula", "formula" ],
[ 13, "address", "Ref:Address", False, "" ], [ 13, "address", "Ref:Address", False, "" ],
@ -258,10 +258,10 @@ class TestTableActions(test_engine.EngineTestCase):
self.init_sample_data() self.init_sample_data()
# Add one more table, and one more view for tables #1 and #4 (those we are about to delete). # Add one more table, and one more view for tables #1 and #4 (those we are about to delete).
self.apply_user_action(["AddEmptyTable"]) self.apply_user_action(["AddEmptyTable", None])
out_actions = self.apply_user_action(["CreateViewSection", 1, 0, 'detail', None]) out_actions = self.apply_user_action(["CreateViewSection", 1, 0, 'detail', None, None])
self.assertEqual(out_actions.retValues[0]["viewRef"], 5) self.assertEqual(out_actions.retValues[0]["viewRef"], 5)
self.apply_user_action(["CreateViewSection", 4, 5, 'detail', None]) self.apply_user_action(["CreateViewSection", 4, 5, 'detail', None, None])
# See what's in TabBar table, to verify after we remove a table. # See what's in TabBar table, to verify after we remove a table.
self.assertTableData('_grist_TabBar', cols="subset", data=[ self.assertTableData('_grist_TabBar', cols="subset", data=[

@ -6,7 +6,7 @@ class TestUndo(test_engine.EngineTestCase):
def test_bad_undo(self): def test_bad_undo(self):
# Sometimes undo can make metadata inconsistent with schema. Check that we disallow it. # Sometimes undo can make metadata inconsistent with schema. Check that we disallow it.
self.load_sample(testsamples.sample_students) self.load_sample(testsamples.sample_students)
out_actions1 = self.apply_user_action(['AddEmptyTable']) out_actions1 = self.apply_user_action(['AddEmptyTable', None])
self.assertPartialData("_grist_Tables", ["id", "tableId", "columns"], [ self.assertPartialData("_grist_Tables", ["id", "tableId", "columns"], [
[1, "Students", [1,2,4,5,6]], [1, "Students", [1,2,4,5,6]],
[2, "Schools", [10,12]], [2, "Schools", [10,12]],
@ -60,7 +60,7 @@ class TestUndo(test_engine.EngineTestCase):
# during undo of imports when the undo could omit part of the action bundle. # during undo of imports when the undo could omit part of the action bundle.
self.load_sample(testsamples.sample_students) self.load_sample(testsamples.sample_students)
out_actions1 = self.apply_user_action(['AddEmptyTable']) out_actions1 = self.apply_user_action(['AddEmptyTable', None])
out_actions2 = self.add_column('Table1', 'D', type='Text') out_actions2 = self.add_column('Table1', 'D', type='Text')
out_actions3 = self.remove_column('Table1', 'D') out_actions3 = self.remove_column('Table1', 'D')
out_actions4 = self.apply_user_action(['RemoveTable', 'Table1']) out_actions4 = self.apply_user_action(['RemoveTable', 'Table1'])

@ -77,10 +77,7 @@ class TestUserActions(test_engine.EngineTestCase):
'type': 'Text' 'type': 'Text'
}], }],
["AddRecord", "_grist_Views_section_field", 3, { ["AddRecord", "_grist_Views_section_field", 3, {
"colRef": 24, "parentId": 1, "parentPos": 3.0 "colRef": 24, "parentId": 2, "parentPos": 3.0
}],
["AddRecord", "_grist_Views_section_field", 4, {
"colRef": 24, "parentId": 2, "parentPos": 4.0
}], }],
["BulkUpdateRecord", "Schools", [1, 2, 3], ["BulkUpdateRecord", "Schools", [1, 2, 3],
{"grist_Transform": ["New York", "Colombia", "New York"]}], {"grist_Transform": ["New York", "Colombia", "New York"]}],
@ -124,7 +121,7 @@ class TestUserActions(test_engine.EngineTestCase):
out_actions = self.remove_column('Schools', 'grist_Transform') out_actions = self.remove_column('Schools', 'grist_Transform')
self.assertPartialOutActions(out_actions, { "stored": [ self.assertPartialOutActions(out_actions, { "stored": [
["BulkRemoveRecord", "_grist_Views_section_field", [3, 4]], ["RemoveRecord", "_grist_Views_section_field", 3],
['RemoveRecord', '_grist_Tables_column', 24], ['RemoveRecord', '_grist_Tables_column', 24],
['RemoveColumn', 'Schools', 'grist_Transform'], ['RemoveColumn', 'Schools', 'grist_Transform'],
]}) ]})
@ -137,7 +134,7 @@ class TestUserActions(test_engine.EngineTestCase):
self.assertTables([self.starting_table]) self.assertTables([self.starting_table])
# Create a view + section for the initial table. # Create a view + section for the initial table.
self.apply_user_action(["CreateViewSection", 1, 0, "record", None]) self.apply_user_action(["CreateViewSection", 1, 0, "record", None, None])
# Verify that we got a new view, with one section, and three fields. # Verify that we got a new view, with one section, and three fields.
self.assertViews([View(1, sections=[ self.assertViews([View(1, sections=[
@ -147,7 +144,7 @@ class TestUserActions(test_engine.EngineTestCase):
]) ]) ]) ])
# Create a new section for the same view, check that only a section is added. # Create a new section for the same view, check that only a section is added.
self.apply_user_action(["CreateViewSection", 1, 1, "record", None]) self.apply_user_action(["CreateViewSection", 1, 1, "record", None, None])
self.assertTables([self.starting_table]) self.assertTables([self.starting_table])
self.assertViews([View(1, sections=[ self.assertViews([View(1, sections=[
Section(1, parentKey="record", tableRef=1, fields=[ Section(1, parentKey="record", tableRef=1, fields=[
@ -159,7 +156,7 @@ class TestUserActions(test_engine.EngineTestCase):
]) ]) ]) ])
# Create another section for the same view, this time summarized. # Create another section for the same view, this time summarized.
self.apply_user_action(["CreateViewSection", 1, 1, "record", [21]]) self.apply_user_action(["CreateViewSection", 1, 1, "record", [21], None])
summary_table = Table(2, "GristSummary_7_Address", 0, summarySourceTable=1, columns=[ summary_table = Table(2, "GristSummary_7_Address", 0, summarySourceTable=1, columns=[
Column(22, "city", "Text", isFormula=False, formula="", summarySourceCol=21), Column(22, "city", "Text", isFormula=False, formula="", summarySourceCol=21),
Column(23, "group", "RefList:Address", isFormula=True, Column(23, "group", "RefList:Address", isFormula=True,
@ -185,7 +182,7 @@ class TestUserActions(test_engine.EngineTestCase):
# Try to create a summary table for an invalid column, and check that it fails. # Try to create a summary table for an invalid column, and check that it fails.
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
self.apply_user_action(["CreateViewSection", 1, 1, "record", [23]]) self.apply_user_action(["CreateViewSection", 1, 1, "record", [23], None])
self.assertTables([self.starting_table, summary_table]) self.assertTables([self.starting_table, summary_table])
self.assertViews([view]) self.assertViews([view])
@ -197,69 +194,55 @@ class TestUserActions(test_engine.EngineTestCase):
self.assertTables([self.starting_table]) self.assertTables([self.starting_table])
self.assertViews([]) self.assertViews([])
# When we create a section/view for new table, we get both a primary view, and the new view we # When we create a section/view for new table, we got the new view we are creating,
# are creating. # without primary view.
self.apply_user_action(["CreateViewSection", 0, 0, "record", None]) self.apply_user_action(["CreateViewSection", 0, 0, "record", None, None])
new_table = Table(2, "Table1", primaryViewId=1, summarySourceTable=0, columns=[ new_table = Table(2, "Table1", primaryViewId=0, summarySourceTable=0, columns=[
Column(22, "manualSort", "ManualSortPos", isFormula=False, formula="", summarySourceCol=0), Column(22, "manualSort", "ManualSortPos", isFormula=False, formula="", summarySourceCol=0),
Column(23, "A", "Any", isFormula=True, formula="", summarySourceCol=0), Column(23, "A", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(24, "B", "Any", isFormula=True, formula="", summarySourceCol=0), Column(24, "B", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(25, "C", "Any", isFormula=True, formula="", summarySourceCol=0), Column(25, "C", "Any", isFormula=True, formula="", summarySourceCol=0),
]) ])
primary_view = View(1, sections=[ new_view = View(1, sections=[
Section(1, parentKey="record", tableRef=2, fields=[ Section(2, parentKey="record", tableRef=2, fields=[
Field(1, colRef=23), Field(4, colRef=23),
Field(2, colRef=24), Field(5, colRef=24),
Field(3, colRef=25), Field(6, colRef=25),
])
])
new_view = View(2, sections=[
Section(3, parentKey="record", tableRef=2, fields=[
Field(7, colRef=23),
Field(8, colRef=24),
Field(9, colRef=25),
]) ])
]) ])
self.assertTables([self.starting_table, new_table]) self.assertTables([self.starting_table, new_table])
self.assertViews([primary_view, new_view]) self.assertViews([new_view])
# Create another section in an existing view for a new table. # Create another section in an existing view for a new table.
self.apply_user_action(["CreateViewSection", 0, 2, "record", None]) self.apply_user_action(["CreateViewSection", 0, 1, "record", None, None])
new_table2 = Table(3, "Table2", primaryViewId=3, summarySourceTable=0, columns=[ new_table2 = Table(3, "Table2", primaryViewId=0, summarySourceTable=0, columns=[
Column(26, "manualSort", "ManualSortPos", isFormula=False, formula="", summarySourceCol=0), Column(26, "manualSort", "ManualSortPos", isFormula=False, formula="", summarySourceCol=0),
Column(27, "A", "Any", isFormula=True, formula="", summarySourceCol=0), Column(27, "A", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(28, "B", "Any", isFormula=True, formula="", summarySourceCol=0), Column(28, "B", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(29, "C", "Any", isFormula=True, formula="", summarySourceCol=0), Column(29, "C", "Any", isFormula=True, formula="", summarySourceCol=0),
]) ])
primary_view2 = View(3, sections=[ new_view.sections.append(
Section(4, parentKey="record", tableRef=3, fields=[ Section(4, parentKey="record", tableRef=3, fields=[
Field(10, colRef=27), Field(10, colRef=27),
Field(11, colRef=28), Field(11, colRef=28),
Field(12, colRef=29), Field(12, colRef=29),
]) ])
])
new_view.sections.append(
Section(6, parentKey="record", tableRef=3, fields=[
Field(16, colRef=27),
Field(17, colRef=28),
Field(18, colRef=29),
])
) )
# Check that we have a new table, only the primary view as new view; and a new section. # Check that we have a new table, only the new view; and a new section.
self.assertTables([self.starting_table, new_table, new_table2]) self.assertTables([self.starting_table, new_table, new_table2])
self.assertViews([primary_view, new_view, primary_view2]) self.assertViews([new_view])
# Check that we can't create a summary of a table grouped by a column that doesn't exist yet. # Check that we can't create a summary of a table grouped by a column that doesn't exist yet.
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
self.apply_user_action(["CreateViewSection", 0, 2, "record", [31]]) self.apply_user_action(["CreateViewSection", 0, 1, "record", [31], None])
self.assertTables([self.starting_table, new_table, new_table2]) self.assertTables([self.starting_table, new_table, new_table2])
self.assertViews([primary_view, new_view, primary_view2]) self.assertViews([new_view])
# But creating a new table and showing totals for it is possible though dumb. # But creating a new table and showing totals for it is possible though dumb.
self.apply_user_action(["CreateViewSection", 0, 2, "record", []]) self.apply_user_action(["CreateViewSection", 0, 1, "record", [], None])
# We expect a new table. # We expect a new table.
new_table3 = Table(4, "Table3", primaryViewId=4, summarySourceTable=0, columns=[ new_table3 = Table(4, "Table3", primaryViewId=0, summarySourceTable=0, columns=[
Column(30, "manualSort", "ManualSortPos", isFormula=False, formula="", summarySourceCol=0), Column(30, "manualSort", "ManualSortPos", isFormula=False, formula="", summarySourceCol=0),
Column(31, "A", "Any", isFormula=True, formula="", summarySourceCol=0), Column(31, "A", "Any", isFormula=True, formula="", summarySourceCol=0),
Column(32, "B", "Any", isFormula=True, formula="", summarySourceCol=0), Column(32, "B", "Any", isFormula=True, formula="", summarySourceCol=0),
@ -271,20 +254,11 @@ class TestUserActions(test_engine.EngineTestCase):
formula="table.getSummarySourceGroup(rec)", summarySourceCol=0), formula="table.getSummarySourceGroup(rec)", summarySourceCol=0),
Column(35, "count", "Int", isFormula=True, formula="len($group)", summarySourceCol=0), Column(35, "count", "Int", isFormula=True, formula="len($group)", summarySourceCol=0),
]) ])
# The primary view of the new table.
primary_view3 = View(4, sections=[
Section(7, parentKey="record", tableRef=4, fields=[
Field(19, colRef=31),
Field(20, colRef=32),
Field(21, colRef=33),
])
])
# And a new view section for the summary.
new_view.sections.append(Section(9, parentKey="record", tableRef=5, fields=[
Field(25, colRef=35)
]))
self.assertTables([self.starting_table, new_table, new_table2, new_table3, summary_table]) self.assertTables([self.starting_table, new_table, new_table2, new_table3, summary_table])
self.assertViews([primary_view, new_view, primary_view2, primary_view3]) new_view.sections.append(Section(6, parentKey="record", tableRef=5, fields=[
Field(16, colRef=35)
]))
self.assertViews([new_view])
#---------------------------------------------------------------------- #----------------------------------------------------------------------
@ -301,10 +275,10 @@ class TestUserActions(test_engine.EngineTestCase):
'size': [1000, 2000, 3000, 4000], 'size': [1000, 2000, 3000, 4000],
}]) }])
# Add a new view; a second section (summary) to it; and a third view. # Add a new view; a second section (summary) to it; and a third view.
self.apply_user_action(['CreateViewSection', 1, 0, 'detail', None]) self.apply_user_action(['CreateViewSection', 1, 0, 'detail', None, None])
self.apply_user_action(['CreateViewSection', 1, 2, 'record', [3]]) self.apply_user_action(['CreateViewSection', 1, 2, 'record', [3], None])
self.apply_user_action(['CreateViewSection', 1, 0, 'chart', None]) self.apply_user_action(['CreateViewSection', 1, 0, 'chart', None, None])
self.apply_user_action(['CreateViewSection', 0, 2, 'record', None]) self.apply_user_action(['CreateViewSection', 0, 2, 'record', None, None])
# Verify the new structure of tables and views. # Verify the new structure of tables and views.
self.assertTables([ self.assertTables([
@ -320,7 +294,7 @@ class TestUserActions(test_engine.EngineTestCase):
Column(7, "count", "Int", True, "len($group)", 0), Column(7, "count", "Int", True, "len($group)", 0),
Column(8, "size", "Numeric", True, "SUM($group.size)", 0), Column(8, "size", "Numeric", True, "SUM($group.size)", 0),
]), ]),
Table(3, 'Table1', 4, 0, columns=[ Table(3, 'Table1', 0, 0, columns=[
Column(9, "manualSort", "ManualSortPos", False, "", 0), Column(9, "manualSort", "ManualSortPos", False, "", 0),
Column(10, "A", "Any", True, "", 0), Column(10, "A", "Any", True, "", 0),
Column(11, "B", "Any", True, "", 0), Column(11, "B", "Any", True, "", 0),
@ -346,10 +320,10 @@ class TestUserActions(test_engine.EngineTestCase):
Field(11, colRef=7), Field(11, colRef=7),
Field(12, colRef=8), Field(12, colRef=8),
]), ]),
Section(8, parentKey='record', tableRef=3, fields=[ Section(7, parentKey='record', tableRef=3, fields=[
Field(21, colRef=10), Field(18, colRef=10),
Field(22, colRef=11), Field(19, colRef=11),
Field(23, colRef=12), Field(20, colRef=12),
]), ]),
]), ]),
View(3, sections=[ View(3, sections=[
@ -357,28 +331,19 @@ class TestUserActions(test_engine.EngineTestCase):
Field(13, colRef=2), Field(13, colRef=2),
Field(14, colRef=3), Field(14, colRef=3),
]), ]),
]), ])
View(4, sections=[
Section(6, parentKey='record', tableRef=3, fields=[
Field(15, colRef=10),
Field(16, colRef=11),
Field(17, colRef=12),
]),
]),
]) ])
self.assertTableData('_grist_TabBar', cols="subset", data=[ self.assertTableData('_grist_TabBar', cols="subset", data=[
["id", "viewRef"], ["id", "viewRef"],
[1, 1], [1, 1],
[2, 2], [2, 2],
[3, 3], [3, 3],
[4, 4],
]) ])
self.assertTableData('_grist_Pages', cols="subset", data=[ self.assertTableData('_grist_Pages', cols="subset", data=[
["id", "viewRef"], ["id", "viewRef"],
[1, 1], [1, 1],
[2, 2], [2, 2],
[3, 3], [3, 3]
[4, 4]
]) ])
#---------------------------------------------------------------------- #----------------------------------------------------------------------
@ -388,7 +353,7 @@ class TestUserActions(test_engine.EngineTestCase):
self.init_views_sample() self.init_views_sample()
# Remove a view. Ensure related items, sections, fields get removed. # Remove a view. Ensure related items, sections, fields get removed.
self.apply_user_action(["BulkRemoveRecord", "_grist_Views", [2,3]]) self.apply_user_action(["BulkRemoveRecord", "_grist_Views", [2, 3]])
# Verify the new structure of tables and views. # Verify the new structure of tables and views.
self.assertTables([ self.assertTables([
@ -399,7 +364,7 @@ class TestUserActions(test_engine.EngineTestCase):
Column(4, "size", "Numeric", False, "", 0), Column(4, "size", "Numeric", False, "", 0),
]), ]),
# Note that the summary table is gone. # Note that the summary table is gone.
Table(3, 'Table1', 4, 0, columns=[ Table(3, 'Table1', 0, 0, columns=[
Column(9, "manualSort", "ManualSortPos", False, "", 0), Column(9, "manualSort", "ManualSortPos", False, "", 0),
Column(10, "A", "Any", True, "", 0), Column(10, "A", "Any", True, "", 0),
Column(11, "B", "Any", True, "", 0), Column(11, "B", "Any", True, "", 0),
@ -413,24 +378,15 @@ class TestUserActions(test_engine.EngineTestCase):
Field(2, colRef=3), Field(2, colRef=3),
Field(3, colRef=4), Field(3, colRef=4),
]), ]),
]), ])
View(4, sections=[
Section(6, parentKey='record', tableRef=3, fields=[
Field(15, colRef=10),
Field(16, colRef=11),
Field(17, colRef=12),
]),
]),
]) ])
self.assertTableData('_grist_TabBar', cols="subset", data=[ self.assertTableData('_grist_TabBar', cols="subset", data=[
["id", "viewRef"], ["id", "viewRef"],
[1, 1], [1, 1],
[4, 4],
]) ])
self.assertTableData('_grist_Pages', cols="subset", data=[ self.assertTableData('_grist_Pages', cols="subset", data=[
["id", "viewRef"], ["id", "viewRef"],
[1, 1], [1, 1],
[4, 4],
]) ])
#---------------------------------------------------------------------- #----------------------------------------------------------------------
@ -444,33 +400,31 @@ class TestUserActions(test_engine.EngineTestCase):
[ 'id', 'tableId', 'primaryViewId' ], [ 'id', 'tableId', 'primaryViewId' ],
[ 1, 'Schools', 1], [ 1, 'Schools', 1],
[ 2, 'GristSummary_7_Schools', 0], [ 2, 'GristSummary_7_Schools', 0],
[ 3, 'Table1', 4], [ 3, 'Table1', 0],
]) ])
self.assertTableData('_grist_Views', cols="subset", data=[ self.assertTableData('_grist_Views', cols="subset", data=[
[ 'id', 'name', 'primaryViewTable' ], [ 'id', 'name', 'primaryViewTable' ],
[ 1, 'Schools', 1], [ 1, 'Schools', 1],
[ 2, 'New page', 0], [ 2, 'New page', 0],
[ 3, 'New page', 0], [ 3, 'New page', 0],
[ 4, 'Table1', 3],
]) ])
# Update the names in a few views, and ensure that primary ones won't cause tables to # Update the names in a few views, and ensure that primary ones won't cause tables to
# get renamed. # get renamed.
self.apply_user_action(['BulkUpdateRecord', '_grist_Views', [2,3,4], self.apply_user_action(['BulkUpdateRecord', '_grist_Views', [2, 3],
{'name': ['A', 'B', 'C']}]) {'name': ['A', 'B']}])
self.assertTableData('_grist_Tables', cols="subset", data=[ self.assertTableData('_grist_Tables', cols="subset", data=[
[ 'id', 'tableId', 'primaryViewId' ], [ 'id', 'tableId', 'primaryViewId' ],
[ 1, 'Schools', 1], [ 1, 'Schools', 1],
[ 2, 'GristSummary_7_Schools', 0], [ 2, 'GristSummary_7_Schools', 0],
[ 3, 'Table1', 4], [ 3, 'Table1', 0],
]) ])
self.assertTableData('_grist_Views', cols="subset", data=[ self.assertTableData('_grist_Views', cols="subset", data=[
[ 'id', 'name', 'primaryViewTable' ], [ 'id', 'name', 'primaryViewTable' ],
[ 1, 'Schools', 1], [ 1, 'Schools', 1],
[ 2, 'A', 0], [ 2, 'A', 0],
[ 3, 'B', 0], [ 3, 'B', 0]
[ 4, 'C', 3]
]) ])
# Now rename a table (by raw view section) and make sure that a view with the same name # Now rename a table (by raw view section) and make sure that a view with the same name
@ -482,14 +436,13 @@ class TestUserActions(test_engine.EngineTestCase):
['id', 'tableId'], ['id', 'tableId'],
[1, 'Bars', 1], [1, 'Bars', 1],
[2, 'GristSummary_4_Bars', 0], [2, 'GristSummary_4_Bars', 0],
[3, 'Table1', 4], [3, 'Table1', 0],
]) ])
self.assertTableData('_grist_Views', cols="subset", data=[ self.assertTableData('_grist_Views', cols="subset", data=[
['id', 'name'], ['id', 'name'],
[1, 'Bars'], [1, 'Bars'],
[2, 'A'], [2, 'A'],
[3, 'B'], [3, 'B']
[4, 'C']
]) ])
# Now rename tables so that two tables will have same names, to test if only the view # Now rename tables so that two tables will have same names, to test if only the view
@ -501,47 +454,57 @@ class TestUserActions(test_engine.EngineTestCase):
['id', 'tableId'], ['id', 'tableId'],
[1, 'A', 1], [1, 'A', 1],
[2, 'GristSummary_1_A', 0], [2, 'GristSummary_1_A', 0],
[3, 'Table1', 4], [3, 'Table1', 0],
]) ])
self.assertTableData('_grist_Views', cols="subset", data=[ self.assertTableData('_grist_Views', cols="subset", data=[
['id', 'name'], ['id', 'name'],
[1, 'A'], [1, 'A'],
[2, 'A'], [2, 'A'],
[3, 'B'], [3, 'B'],
[4, 'C']
]) ])
self.apply_user_action(['UpdateRecord', '_grist_Views_section', 2, self.apply_user_action(['UpdateRecord', '_grist_Views_section', 2,
{'title': 'Z'}]) {'title': 'Z'}])
self.assertTableData('_grist_Tables', cols="subset", data=[ self.assertTableData('_grist_Tables', cols="subset", data=[
['id', 'tableId'], ['id', 'tableId', 'primaryViewId', 'rawViewSectionRef'],
[1, 'Z', 1], [1, 'Z', 1, 2],
[2, 'GristSummary_1_Z', 0], [2, 'GristSummary_1_Z', 0, 0],
[3, 'Table1', 4], [3, 'Table1', 0, 6],
]) ])
self.assertTableData('_grist_Views', cols="subset", data=[ self.assertTableData('_grist_Views', cols="subset", data=[
['id', 'name'], ['id', 'name'],
[1, 'Z'], [1, 'Z'],
[2, 'Z'], [2, 'Z'],
[3, 'B'], [3, 'B'],
[4, 'C']
]) ])
# Add new table, with a view with the same name (Z) and make sure it won't be renamed # Add new table, with a view with the same name (Z) and make sure it won't be renamed
self.apply_user_action(['AddTable', 'Stations', [ self.apply_user_action(['AddTable', 'Stations', [
{'id': 'city', 'type': 'Text'}, {'id': 'city', 'type': 'Text'},
]]) ]])
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],
])
self.assertTableData('_grist_Views', cols="subset", data=[
['id', 'name'],
[1, 'Z'],
[2, 'Z'],
[3, 'B'],
[4, 'Stations'],
])
# Replacing only a page name (though primary) # Replacing only a page name (though primary)
self.apply_user_action(['UpdateRecord', '_grist_Views', 5, {'name': 'Z'}]) self.apply_user_action(['UpdateRecord', '_grist_Views', 4, {'name': 'Z'}])
self.assertTableData('_grist_Views', cols="subset", data=[ self.assertTableData('_grist_Views', cols="subset", data=[
['id', 'name'], ['id', 'name'],
[1, 'Z'], [1, 'Z'],
[2, 'Z'], [2, 'Z'],
[3, 'B'], [3, 'B'],
[4, 'C'], [4, 'Z']
[5, 'Z']
]) ])
# Rename table Z to Schools. Primary view for Stations (Z) should not be renamed. # Rename table Z to Schools. Primary view for Stations (Z) should not be renamed.
@ -560,8 +523,7 @@ class TestUserActions(test_engine.EngineTestCase):
[1, 'Schools'], [1, 'Schools'],
[2, 'Schools'], [2, 'Schools'],
[3, 'B'], [3, 'B'],
[4, 'C'], [4, 'Z']
[5, 'Z']
]) ])
#---------------------------------------------------------------------- #----------------------------------------------------------------------
@ -570,9 +532,6 @@ class TestUserActions(test_engine.EngineTestCase):
# Add a couple of tables and views, to trigger creation of some related items. # Add a couple of tables and views, to trigger creation of some related items.
self.init_views_sample() self.init_views_sample()
# Remove a couple of sections. Ensure their fields get removed.
self.apply_user_action(['BulkRemoveRecord', '_grist_Views_section', [4, 8]])
self.assertViews([ self.assertViews([
View(1, sections=[ View(1, sections=[
Section(1, parentKey="record", tableRef=1, fields=[ Section(1, parentKey="record", tableRef=1, fields=[
@ -587,20 +546,49 @@ class TestUserActions(test_engine.EngineTestCase):
Field(8, colRef=3), Field(8, colRef=3),
Field(9, colRef=4), Field(9, colRef=4),
]), ]),
Section(4, parentKey="record", tableRef=2, fields=[
Field(10, colRef=5),
Field(11, colRef=7),
Field(12, colRef=8),
]),
Section(7, parentKey='record', tableRef=3, fields=[
Field(18, colRef=10),
Field(19, colRef=11),
Field(20, colRef=12),
]),
]), ]),
View(3, sections=[ View(3, sections=[
Section(5, parentKey="chart", tableRef=1, fields=[ Section(5, parentKey="chart", tableRef=1, fields=[
Field(13, colRef=2), Field(13, colRef=2),
Field(14, colRef=3), Field(14, colRef=3),
]), ]),
]), ])
View(4, sections=[ ])
Section(6, parentKey='record', tableRef=3, fields=[
Field(15, colRef=10), # Remove a couple of sections. Ensure their fields get removed.
Field(16, colRef=11), self.apply_user_action(['BulkRemoveRecord', '_grist_Views_section', [4, 7]])
Field(17, colRef=12),
self.assertViews([
View(1, sections=[
Section(1, parentKey="record", tableRef=1, fields=[
Field(1, colRef=2),
Field(2, colRef=3),
Field(3, colRef=4),
]), ]),
]), ]),
View(2, sections=[
Section(3, parentKey="detail", tableRef=1, fields=[
Field(7, colRef=2),
Field(8, colRef=3),
Field(9, colRef=4),
])
]),
View(3, sections=[
Section(5, parentKey="chart", tableRef=1, fields=[
Field(13, colRef=2),
Field(14, colRef=3),
]),
])
]) ])
#---------------------------------------------------------------------- #----------------------------------------------------------------------
@ -625,14 +613,14 @@ class TestUserActions(test_engine.EngineTestCase):
# Do a schema action to ensure it gets called: this causes a table rename. # 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 # 7 is id of raw view section for the Tabl1 table
self.apply_user_action(['UpdateRecord', '_grist_Views_section', 7, {'title': 'C'}]) self.apply_user_action(['UpdateRecord', '_grist_Views_section', 6, {'title': 'C'}])
self.assertEqual(count_calls[0], 1) self.assertEqual(count_calls[0], 1)
self.assertTableData('_grist_Tables', cols="subset", data=[ self.assertTableData('_grist_Tables', cols="subset", data=[
[ 'id', 'tableId', 'primaryViewId' ], [ 'id', 'tableId', 'primaryViewId' ],
[ 1, 'Schools', 1], [ 1, 'Schools', 1],
[ 2, 'GristSummary_7_Schools', 0], [ 2, 'GristSummary_7_Schools', 0],
[ 3, 'C', 4], [ 3, 'C', 0],
]) ])
# Do another schema and non-schema action. # Do another schema and non-schema action.
@ -834,7 +822,6 @@ class TestUserActions(test_engine.EngineTestCase):
[ 1, 0], [ 1, 0],
[ 2, 1], [ 2, 1],
[ 3, 0], [ 3, 0],
[ 4, 0],
]) ])
# Verify that removing page 1 fixes page 2 indentation. # Verify that removing page 1 fixes page 2 indentation.
@ -843,7 +830,6 @@ class TestUserActions(test_engine.EngineTestCase):
['id', 'indentation'], ['id', 'indentation'],
[ 2, 0], [ 2, 0],
[ 3, 0], [ 3, 0],
[ 4, 0],
]) ])
# Removing last page should not fail # Removing last page should not fail
@ -962,7 +948,7 @@ class TestUserActions(test_engine.EngineTestCase):
# Test filters rename # Test filters rename
# Create new view section # Create new view section
self.apply_user_action(["CreateViewSection", 1, 0, "record", None]) self.apply_user_action(["CreateViewSection", 1, 0, "record", None, None])
# Filter it by first column # Filter it by first column
self.apply_user_action(['BulkAddRecord', '_grist_Filters', [None], { self.apply_user_action(['BulkAddRecord', '_grist_Filters', [None], {
@ -1287,7 +1273,7 @@ class TestUserActions(test_engine.EngineTestCase):
# load_sample handles loading basic metadata, but doesn't create any view sections # load_sample handles loading basic metadata, but doesn't create any view sections
self.load_sample(self.sample) self.load_sample(self.sample)
# Create a new table which automatically gets a raw view section # Create a new table which automatically gets a raw view section
self.apply_user_action(["AddEmptyTable"]) self.apply_user_action(["AddEmptyTable", None])
# Note the row IDs of the raw view section (2) and fields (4, 5, 6) # Note the row IDs of the raw view section (2) and fields (4, 5, 6)
self.assertTableData('_grist_Views_section', cols="subset", data=[ self.assertTableData('_grist_Views_section', cols="subset", data=[
@ -1339,7 +1325,7 @@ class TestUserActions(test_engine.EngineTestCase):
def test_update_current_time(self): def test_update_current_time(self):
self.load_sample(self.sample) self.load_sample(self.sample)
self.apply_user_action(["AddEmptyTable"]) self.apply_user_action(["AddEmptyTable", None])
self.add_column('Table1', 'now', isFormula=True, formula='NOW()', type='Any') self.add_column('Table1', 'now', isFormula=True, formula='NOW()', type='Any')
# No records with NOW() in a formula yet, so this action should have no effect at all. # No records with NOW() in a formula yet, so this action should have no effect at all.
@ -1374,7 +1360,7 @@ class TestUserActions(test_engine.EngineTestCase):
check(1) check(1)
# Testing that unrelated actions don't change the time # Testing that unrelated actions don't change the time
self.apply_user_action(["AddEmptyTable"]) self.apply_user_action(["AddEmptyTable", None])
self.add_record("Table2") self.add_record("Table2")
self.apply_user_action(["Calculate"]) # only recalculates for fresh docs self.apply_user_action(["Calculate"]) # only recalculates for fresh docs
check(1) check(1)

@ -916,7 +916,7 @@
{"parentId": 1, "colRef": 34, "parentPos": 1.0}], {"parentId": 1, "colRef": 34, "parentPos": 1.0}],
// Raw data widget // Raw data widget
["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4}], ["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4, "title": ""}],
["AddRecord", "_grist_Views_section_field", 2, {"colRef": 34, "parentId": 2, "parentPos": 2.0}], ["AddRecord", "_grist_Views_section_field", 2, {"colRef": 34, "parentId": 2, "parentPos": 2.0}],
["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}], ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}],
@ -927,10 +927,9 @@
{"colId": "world", "parentPos": 13.0, {"colId": "world", "parentPos": 13.0,
"formula": "rec.hello.upper()", "parentId": 4, "type": "Text", "formula": "rec.hello.upper()", "parentId": 4, "type": "Text",
"isFormula": true, "label": "world", "widgetOptions": ""}], "isFormula": true, "label": "world", "widgetOptions": ""}],
["AddRecord", "_grist_Views_section_field", 3, {"colRef": 35, "parentId": 1, "parentPos": 3.0}], ["AddRecord", "_grist_Views_section_field", 3, {"colRef": 35, "parentId": 2, "parentPos": 3.0}]
["AddRecord", "_grist_Views_section_field", 4, {"colRef": 35, "parentId": 2, "parentPos": 4.0}]
], ],
"direct": [true, true, true, true, true, "direct": [true, true, true, true,
true, true, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true], true, true, true],
"undo": [ "undo": [
@ -947,8 +946,7 @@
["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 0, "rawViewSectionRef": 0}], ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 0, "rawViewSectionRef": 0}],
["RemoveColumn", "Bar", "world"], ["RemoveColumn", "Bar", "world"],
["RemoveRecord", "_grist_Tables_column", 35], ["RemoveRecord", "_grist_Tables_column", 35],
["RemoveRecord", "_grist_Views_section_field", 3], ["RemoveRecord", "_grist_Views_section_field", 3]
["RemoveRecord", "_grist_Views_section_field", 4]
], ],
"retValue": [ "retValue": [
{ {
@ -1257,7 +1255,7 @@
"parentId": 1, "parentKey": "record", "sortColRefs": "[]", "title": ""}], "parentId": 1, "parentKey": "record", "sortColRefs": "[]", "title": ""}],
["BulkAddRecord", "_grist_Views_section_field", [1,2], ["BulkAddRecord", "_grist_Views_section_field", [1,2],
{"parentId": [1,1], "colRef": [31,32], "parentPos": [1.0,2.0]}], {"parentId": [1,1], "colRef": [31,32], "parentPos": [1.0,2.0]}],
["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4}], ["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4, "title": ""}],
["BulkAddRecord", "_grist_Views_section_field", [3, 4], {"colRef": [31, 32], "parentId": [2, 2], "parentPos": [3.0, 4.0]}], ["BulkAddRecord", "_grist_Views_section_field", [3, 4], {"colRef": [31, 32], "parentId": [2, 2], "parentPos": [3.0, 4.0]}],
["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}], ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}],
["BulkRemoveRecord", "_grist_Views_section_field", [1, 3]], ["BulkRemoveRecord", "_grist_Views_section_field", [1, 3]],
@ -2200,7 +2198,7 @@
["AddRecord", "_grist_Views_section", 1, ["AddRecord", "_grist_Views_section", 1,
{"tableRef": 4, "defaultWidth": 100, "borderWidth": 1, {"tableRef": 4, "defaultWidth": 100, "borderWidth": 1,
"parentId": 1, "parentKey": "record", "sortColRefs": "[]", "title": ""}], "parentId": 1, "parentKey": "record", "sortColRefs": "[]", "title": ""}],
["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4}], ["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4, "title": ""}],
["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}], ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}],
["AddTable", "Bar", [ ["AddTable", "Bar", [
{"id": "manualSort", "formula": "", "isFormula": false, "type": "ManualSortPos"}, {"id": "manualSort", "formula": "", "isFormula": false, "type": "ManualSortPos"},
@ -2230,7 +2228,7 @@
"parentId": 2, "parentKey": "record", "sortColRefs": "[]", "title": ""}], "parentId": 2, "parentKey": "record", "sortColRefs": "[]", "title": ""}],
["BulkAddRecord", "_grist_Views_section_field", [1,2,3], ["BulkAddRecord", "_grist_Views_section_field", [1,2,3],
{"parentId": [3,3,3], "colRef": [32,33,34], "parentPos": [1.0,2.0,3.0]}], {"parentId": [3,3,3], "colRef": [32,33,34], "parentPos": [1.0,2.0,3.0]}],
["AddRecord", "_grist_Views_section", 4, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 5}], ["AddRecord", "_grist_Views_section", 4, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 5, "title": ""}],
["BulkAddRecord", "_grist_Views_section_field", [4, 5, 6], {"colRef": [32, 33, 34], "parentId": [4, 4, 4], "parentPos": [4.0, 5.0, 6.0]}], ["BulkAddRecord", "_grist_Views_section_field", [4, 5, 6], {"colRef": [32, 33, 34], "parentId": [4, 4, 4], "parentPos": [4.0, 5.0, 6.0]}],
["UpdateRecord", "_grist_Tables", 5, {"primaryViewId": 2, "rawViewSectionRef": 4}], ["UpdateRecord", "_grist_Tables", 5, {"primaryViewId": 2, "rawViewSectionRef": 4}],
["AddRecord", "Bar", 1, {"foo": 0, "hello": "a", "manualSort": 1.0}], ["AddRecord", "Bar", 1, {"foo": 0, "hello": "a", "manualSort": 1.0}],
@ -2334,7 +2332,7 @@
{"tableRef": 4, "defaultWidth": 100, "borderWidth": 1, {"tableRef": 4, "defaultWidth": 100, "borderWidth": 1,
"parentId": 1, "parentKey": "record", "sortColRefs": "[]", "title": ""}], "parentId": 1, "parentKey": "record", "sortColRefs": "[]", "title": ""}],
// Raw data widget // Raw data widget
["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4}], ["AddRecord", "_grist_Views_section", 2, {"borderWidth": 1, "defaultWidth": 100, "parentKey": "record", "tableRef": 4, "title": ""}],
// As part of adding a table, we also set the primaryViewId. // As part of adding a table, we also set the primaryViewId.
["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}] ["UpdateRecord", "_grist_Tables", 4, {"primaryViewId": 1, "rawViewSectionRef": 2}]
], ],

@ -1004,10 +1004,6 @@ class UserActions(object):
# Remove any views that no longer have view sections. # Remove any views that no longer have view sections.
views_to_remove = [view for view in self._docmodel.views.all views_to_remove = [view for view in self._docmodel.views.all
if not view.viewSections] if not view.viewSections]
# Also delete the primary views for tables being deleted, even if it has remaining sections.
for t in remove_table_recs:
if t.primaryViewId and t.primaryViewId not in views_to_remove:
views_to_remove.append(t.primaryViewId)
self._docmodel.remove(views_to_remove) self._docmodel.remove(views_to_remove)
# Save table IDs, which will be inaccessible once we remove the metadata records. # Save table IDs, which will be inaccessible once we remove the metadata records.
@ -1211,20 +1207,49 @@ class UserActions(object):
ret = self.doAddColumn(table_id, col_id, col_info) ret = self.doAddColumn(table_id, col_id, col_info)
if not transform and table_rec.rawViewSectionRef:
# Add a field for this column to the "raw_data" section for this table.
# TODO: the position of the inserted field or of the inserted column will often be
# bogus, since fields and columns are not the same. This requires better coordination
# with the client-side.
self._docmodel.insert(
table_rec.rawViewSectionRef.fields,
col_info.get('_position'),
colRef=ret['colRef']
)
return ret
@useraction
def AddHiddenColumn(self, table_id, col_id, col_info):
return self.doAddColumn(table_id, col_id, col_info)
@useraction
def AddVisibleColumn(self, table_id, col_id, col_info):
'''Inserts column and adds it as a field to all 'record' views'''
ret = self.AddColumn(table_id, col_id, col_info)
table_rec = self._docmodel.get_table_rec(table_id)
transform = (
col_id is not None and
col_id.startswith((
'gristHelper_Transform',
'gristHelper_Converted',
))
)
# Add a field for this column to the view(s) for this table.
if not transform: if not transform:
# Add a field for this column to the "raw_data" view(s) for this table.
for section in table_rec.viewSections: for section in table_rec.viewSections:
if section.parentKey == 'record': if section.parentKey == 'record' and section != table_rec.rawViewSectionRef:
# TODO: the position of the inserted field or of the inserted column will often be # TODO: the position of the inserted field or of the inserted column will often be
# bogus, since fields and columns are not the same. This requires better coordination # bogus, since fields and columns are not the same. This requires better coordination
# with the client-side. # with the client-side.
self._docmodel.insert(section.fields, col_info.get('_position'), colRef=ret['colRef']) self._docmodel.insert(section.fields, col_info.get('_position'), colRef=ret['colRef'])
return ret return ret
@useraction
def AddHiddenColumn(self, table_id, col_id, col_info):
return self.doAddColumn(table_id, col_id, col_info)
@classmethod @classmethod
def _pick_col_name(cls, table_rec, col_id, old_col_id=None, avoid_extra=None): def _pick_col_name(cls, table_rec, col_id, old_col_id=None, avoid_extra=None):
@ -1594,15 +1619,45 @@ class UserActions(object):
#---------------------------------------- #----------------------------------------
@useraction @useraction
def AddEmptyTable(self): def AddEmptyTable(self, table_id):
""" """
Adds an empty table. Currently it makes up the next available table name, and adds three Adds an empty table. Currently it makes up the next available table name (if not provided),
default columns, also picking default names for them (presumably, A, B, and C). and adds three default columns, also picking default names for them (presumably, A, B, and C).
""" """
return self.AddTable(None, [{'id': None, 'isFormula': True} for x in xrange(3)]) columns = [{'id': None, 'isFormula': True} for x in xrange(3)]
return self.AddTable(table_id, columns)
@useraction @useraction
def AddTable(self, table_id, columns): def AddTable(self, table_id, columns):
return self.doAddTable(
table_id,
columns,
manual_sort=True,
primary_view=True,
raw_section=True)
@useraction
def AddRawTable(self, table_id):
"""
Same as AddEmptyTable but does not create a primary view (and page).
"""
columns = [{'id': None, 'isFormula': True} for x in xrange(3)]
return self.doAddTable(
table_id,
columns,
manual_sort=True,
primary_view=False,
raw_section=True
)
def doAddTable(self, table_id, columns, manual_sort=False, primary_view=False,
raw_section=False, summarySourceTableRef=0):
"""
Add the given table with columns with or without additional views.
"""
# For any columns missing 'isFormula' field, default to False when formula is empty. We will # For any columns missing 'isFormula' field, default to False when formula is empty. We will
# normally default new columns to "empty" (isFormula=True), and AddEmptyTable creates empty # normally default new columns to "empty" (isFormula=True), and AddEmptyTable creates empty
# columns, but an AddTable action created e.g. by an import will default to data columns. # columns, but an AddTable action created e.g. by an import will default to data columns.
@ -1610,35 +1665,14 @@ class UserActions(object):
c.setdefault("isFormula", bool(c.get('formula'))) c.setdefault("isFormula", bool(c.get('formula')))
# Add a manualSort column. # Add a manualSort column.
columns.insert(0, column.MANUAL_SORT_COL_INFO.copy()) if manual_sort:
columns.insert(0, column.MANUAL_SORT_COL_INFO.copy())
# First the tables is created without a primary view assigned as no view for it exists.
result = self.doAddTable(table_id, columns)
# Then its Primary View is created.
primary_view = self.doAddView(result["table_id"], 'raw_data', result["table_id"])
result["views"] = [primary_view]
raw_view_section = self._create_plain_view_section(
result["id"],
result["table_id"],
self._docmodel.view_sections,
"record",
)
self.UpdateRecord('_grist_Tables', result["id"], {
'primaryViewId': primary_view["id"],
'rawViewSectionRef': raw_view_section.id,
})
return result
def doAddTable(self, table_id, columns, summarySourceTableRef=0):
"""
Add the given table with columns without creating views.
"""
# If needed, transform table_id into a valid identifier, and add a suffix to make it unique. # If needed, transform table_id into a valid identifier, and add a suffix to make it unique.
table_title = table_id
table_id = identifiers.pick_table_ident(table_id, avoid=six.viewkeys(self._engine.tables)) table_id = identifiers.pick_table_ident(table_id, avoid=six.viewkeys(self._engine.tables))
if not table_title:
table_title = table_id
# Sanitize and de-duplicate column identifiers. # Sanitize and de-duplicate column identifiers.
col_ids = [c['id'] for c in columns] col_ids = [c['id'] for c in columns]
col_ids = identifiers.pick_col_ident_list(col_ids, avoid={'id'}) col_ids = identifiers.pick_col_ident_list(col_ids, avoid={'id'})
@ -1660,12 +1694,36 @@ class UserActions(object):
label = [c.get('label', col_id) for (c, col_id) in zip(columns, col_ids)], label = [c.get('label', col_id) for (c, col_id) in zip(columns, col_ids)],
widgetOptions = [c.get('widgetOptions', '') for c in columns]) widgetOptions = [c.get('widgetOptions', '') for c in columns])
return { result = {
"id": table_rec.id, "id": table_rec.id,
"table_id": table_id, "table_id": table_id,
"columns": col_ids[1:], # All the column ids, except the auto-added manualSort. "columns": col_ids[1:], # All the column ids, except the auto-added manualSort.
} }
if primary_view:
# Create a primary view
primary_view = self.doAddView(result["table_id"], 'raw_data', table_title)
result["views"] = [primary_view]
if raw_section:
# Create raw view section
raw_section = self._create_plain_view_section(
result["id"],
table_id,
self._docmodel.view_sections,
"record",
table_title
)
if primary_view or raw_section:
self.UpdateRecord('_grist_Tables', result["id"], {
'primaryViewId': primary_view["id"] if primary_view else 0,
'rawViewSectionRef': raw_section.id if raw_section else 0,
})
return result
@useraction @useraction
def RemoveTable(self, table_id): def RemoveTable(self, table_id):
# We can remove a table via either a "RemoveTable" useraction or by removing a table # We can remove a table via either a "RemoveTable" useraction or by removing a table
@ -1694,7 +1752,7 @@ class UserActions(object):
return cols return cols
@useraction @useraction
def CreateViewSection(self, table_ref, view_ref, section_type, groupby_colrefs): def CreateViewSection(self, table_ref, view_ref, section_type, groupby_colrefs, table_id):
""" """
Create a new view section. If table_ref is 0, also creates a new empty table. If view_ref is Create a new view section. If table_ref is 0, also creates a new empty table. If view_ref is
0, also creates a new view that will contain the new section. If groupby_colrefs is None, 0, also creates a new view that will contain the new section. If groupby_colrefs is None,
@ -1705,7 +1763,7 @@ class UserActions(object):
groupby_cols = self._fetch_table_col_recs(table_ref, groupby_colrefs) groupby_cols = self._fetch_table_col_recs(table_ref, groupby_colrefs)
if not table_ref: if not table_ref:
table_ref = self.AddEmptyTable()['id'] table_ref = self.AddRawTable(table_id)['id']
table = self._docmodel.tables.table.get_record(table_ref) table = self._docmodel.tables.table.get_record(table_ref)
if not view_ref: if not view_ref:
@ -1720,6 +1778,7 @@ class UserActions(object):
table.tableId, table.tableId,
view.viewSections, view.viewSections,
section_type, section_type,
''
) )
return { return {
'tableRef': table_ref, 'tableRef': table_ref,
@ -1727,9 +1786,12 @@ class UserActions(object):
'sectionRef': section.id 'sectionRef': section.id
} }
def _create_plain_view_section(self, tableRef, tableId, view_sections, section_type): 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 = ''
section = self._docmodel.add(view_sections, tableRef=tableRef, parentKey=section_type, section = self._docmodel.add(view_sections, tableRef=tableRef, parentKey=section_type,
borderWidth=1, defaultWidth=100)[0] title=title, borderWidth=1, defaultWidth=100)[0]
# TODO: We should address the automatic selection of fields for charts in a better way. # TODO: We should address the automatic selection of fields for charts in a better way.
self._RebuildViewFields(tableId, section.id, self._RebuildViewFields(tableId, section.id,
limit=(2 if section_type == 'chart' else None)) limit=(2 if section_type == 'chart' else None))

@ -838,13 +838,21 @@ export function getPageNames(): Promise<string[]> {
/** /**
* Adds a new empty table using the 'Add New' menu. * Adds a new empty table using the 'Add New' menu.
*/ */
export async function addNewTable() { export async function addNewTable(name?: string) {
await driver.findWait('.test-dp-add-new', 2000).click(); await driver.findWait('.test-dp-add-new', 2000).click();
await driver.find('.test-dp-empty-table').click(); await driver.find('.test-dp-empty-table').click();
if (name) {
const prompt = await driver.find(".test-modal-prompt");
await prompt.doClear();
await prompt.click();
await driver.sendKeys(name);
}
await driver.find(".test-modal-confirm").click();
await waitForServer(); await waitForServer();
} }
export interface PageWidgetPickerOptions { export interface PageWidgetPickerOptions {
tableName?: string;
selectBy?: RegExp|string; // Optional pattern of SELECT BY option to pick. selectBy?: RegExp|string; // Optional pattern of SELECT BY option to pick.
summarize?: (RegExp|string)[]; // Optional list of patterns to match Group By columns. summarize?: (RegExp|string)[]; // Optional list of patterns to match Group By columns.
} }
@ -868,7 +876,7 @@ export async function addNewPage(
} }
// Add a new widget to the current page using the 'Add New' menu. // Add a new widget to the current page using the 'Add New' menu.
export async function addNewSection(typeRe: RegExp, tableRe: RegExp, options?: PageWidgetPickerOptions) { export async function addNewSection(typeRe: RegExp|string, tableRe: RegExp|string, options?: PageWidgetPickerOptions) {
// Click the 'Add widget to page' entry in the 'Add New' menu // Click the 'Add widget to page' entry in the 'Add New' menu
await driver.findWait('.test-dp-add-new', 2000).doClick(); await driver.findWait('.test-dp-add-new', 2000).doClick();
await driver.findWait('.test-dp-add-widget-to-page', 500).doClick(); await driver.findWait('.test-dp-add-widget-to-page', 500).doClick();
@ -918,6 +926,19 @@ export async function selectWidget(
// let's select right type and save // let's select right type and save
await driver.findContent('.test-wselect-type', typeRe).doClick(); await driver.findContent('.test-wselect-type', typeRe).doClick();
await driver.find('.test-wselect-addBtn').doClick(); await driver.find('.test-wselect-addBtn').doClick();
// if we selected a new table, there will be a popup for a name
const prompts = await driver.findAll(".test-modal-prompt");
const prompt = prompts[0];
if (prompt) {
if (options.tableName) {
await prompt.doClear();
await prompt.click();
await driver.sendKeys(options.tableName);
}
await driver.find(".test-modal-confirm").click();
}
await waitForServer(); await waitForServer();
} }
@ -944,11 +965,36 @@ export async function renamePage(oldName: string|RegExp, newName: string) {
/** /**
* Removes a page from the page menu, checks if the page is actually removable. * Removes a page from the page menu, checks if the page is actually removable.
*/ * By default it will remove only page (handling prompt if necessary).
export async function removePage(name: string|RegExp) { */
export async function removePage(name: string|RegExp, options: {
expectPrompt?: boolean, // default undefined
withData?: boolean // default only page,
tables?: string[],
cancel?: boolean,
} = { }) {
await openPageMenu(name); await openPageMenu(name);
assert.equal(await driver.find('.test-docpage-remove').matches('.disabled'), false); assert.equal(await driver.find('.test-docpage-remove').matches('.disabled'), false);
await driver.find('.test-docpage-remove').click(); await driver.find('.test-docpage-remove').click();
const popups = await driver.findAll(".test-removepage-popup");
if (options.expectPrompt === true) {
assert.lengthOf(popups, 1);
} else if (options.expectPrompt === false) {
assert.lengthOf(popups, 0);
}
if (popups.length) {
const popup = popups.shift()!;
if (options.tables) {
const popupTables = await driver.findAll(".test-removepage-table", e => e.getText());
assert.deepEqual(popupTables.sort(), options.tables.sort());
}
await popup.find(`.test-removepage-option-${options.withData ? 'data': 'page'}`).click();
if (options.cancel) {
await driver.find(".test-modal-cancel").click();
} else {
await driver.find(".test-modal-confirm").click();
}
}
await waitForServer(); await waitForServer();
} }

Loading…
Cancel
Save