(core) Removing the new menu flag

Summary: Enabling the `GRIST_NEW_COLUMN_MENU` flag by default and removing it.

Test Plan: Existing

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D4098
This commit is contained in:
Jarosław Sadziński 2023-11-06 16:42:04 +01:00
parent 9262e1f1ef
commit 3c219e05f6
15 changed files with 1415 additions and 475 deletions

View File

@ -44,7 +44,6 @@ const {
buildAddColumnMenu, buildAddColumnMenu,
buildColumnContextMenu, buildColumnContextMenu,
buildMultiColumnMenu, buildMultiColumnMenu,
buildOldAddColumnMenu,
calcFieldsCondition, calcFieldsCondition,
freezeAction, freezeAction,
} = require('app/client/ui/GridViewMenus'); } = require('app/client/ui/GridViewMenus');
@ -56,7 +55,6 @@ const {NEW_FILTER_JSON} = require('app/client/models/ColumnFilter');
const {CombinedStyle} = require("app/client/models/Styles"); const {CombinedStyle} = require("app/client/models/Styles");
const {buildRenameColumn} = require('app/client/ui/ColumnTitle'); const {buildRenameColumn} = require('app/client/ui/ColumnTitle');
const {makeT} = require('app/client/lib/localization'); const {makeT} = require('app/client/lib/localization');
const {GRIST_NEW_COLUMN_MENU} = require("../models/features");
const t = makeT('GridView'); const t = makeT('GridView');
@ -311,14 +309,14 @@ GridView.gridCommands = {
editField: function() { closeRegisteredMenu(); this.scrollToCursor(true); this.activateEditorAtCursor(); }, editField: function() { closeRegisteredMenu(); this.scrollToCursor(true); this.activateEditorAtCursor(); },
insertFieldBefore: function(maybeKeyboardEvent) { insertFieldBefore: function(maybeKeyboardEvent) {
if (GRIST_NEW_COLUMN_MENU() && !maybeKeyboardEvent) { if (!maybeKeyboardEvent) {
this._openInsertColumnMenu(this.cursor.fieldIndex()); this._openInsertColumnMenu(this.cursor.fieldIndex());
} else { } else {
this.insertColumn(null, {index: this.cursor.fieldIndex()}); this.insertColumn(null, {index: this.cursor.fieldIndex()});
} }
}, },
insertFieldAfter: function(maybeKeyboardEvent) { insertFieldAfter: function(maybeKeyboardEvent) {
if (GRIST_NEW_COLUMN_MENU() && !maybeKeyboardEvent) { if (!maybeKeyboardEvent) {
this._openInsertColumnMenu(this.cursor.fieldIndex() + 1); this._openInsertColumnMenu(this.cursor.fieldIndex() + 1);
} else { } else {
this.insertColumn(null, {index: this.cursor.fieldIndex() + 1}); this.insertColumn(null, {index: this.cursor.fieldIndex() + 1});
@ -1302,8 +1300,7 @@ GridView.prototype.buildDom = function() {
testId('column-menu-trigger'), testId('column-menu-trigger'),
), ),
dom('div.selection'), dom('div.selection'),
// FIXME: remove once New Column menu is enabled by default. this._buildInsertColumnMenu({field}),
GRIST_NEW_COLUMN_MENU() ? this._buildInsertColumnMenu({field}) : null,
); );
}), }),
this.isPreview ? null : kd.maybe(() => !this.gristDoc.isReadonlyKo(), () => ( this.isPreview ? null : kd.maybe(() => !this.gristDoc.isReadonlyKo(), () => (
@ -2004,66 +2001,50 @@ GridView.prototype._scrollColumnIntoView = function(colIndex) {
* the GridView. * the GridView.
*/ */
GridView.prototype._buildInsertColumnMenu = function(options = {}) { GridView.prototype._buildInsertColumnMenu = function(options = {}) {
if (GRIST_NEW_COLUMN_MENU()) { const {field} = options;
const {field} = options; const triggers = [];
const triggers = []; if (!field) { triggers.push('click'); }
if (!field) { triggers.push('click'); }
return [ return [
field ? kd.toggleClass('field-insert-before', () => field ? kd.toggleClass('field-insert-before', () =>
this._insertColumnIndex() === field._index()) : null, this._insertColumnIndex() === field._index()) : null,
menu( menu(
ctl => { ctl => {
ctl.onDispose(() => this._insertColumnIndex(null)); ctl.onDispose(() => this._insertColumnIndex(null));
let index = this._insertColumnIndex.peek(); let index = this._insertColumnIndex.peek();
if (index === null || index === -1) { if (index === null || index === -1) {
index = undefined; index = undefined;
} }
return [ return [
buildAddColumnMenu(this, index), buildAddColumnMenu(this, index),
elem => { FocusLayer.create(ctl, {defaultFocusElem: elem, pauseMousetrap: true}); }, elem => { FocusLayer.create(ctl, {defaultFocusElem: elem, pauseMousetrap: true}); },
testId('new-columns-menu'), testId('new-columns-menu'),
]; ];
}, },
{ {
modifiers: { modifiers: {
offset: { offset: {
offset: '8,8', offset: '8,8',
},
}, },
selectOnOpen: true, },
trigger: [ selectOnOpen: true,
...triggers, trigger: [
(_, ctl) => { ...triggers,
ctl.autoDispose(this._insertColumnIndex.subscribe((index) => { (_, ctl) => {
if (field?._index() === index || (!field && index === -1)) { ctl.autoDispose(this._insertColumnIndex.subscribe((index) => {
ctl.open(); if (field?._index() === index || (!field && index === -1)) {
} else if (!ctl.isDisposed()) { ctl.open();
ctl.close(); } else if (!ctl.isDisposed()) {
} ctl.close();
})); }
}, }));
], },
} ],
), }
]; ),
} else { ];
// FIXME: remove once New Column menu is enabled by default.
return [
dom.on('click', async ev => {
// If there are no hidden columns, clicking the plus just adds a new column.
// If there are hidden columns, display a dropdown menu.
if (this.viewSection.hiddenColumns().length === 0) {
// Don't open the menu defined below.
ev.stopImmediatePropagation();
await this.insertColumn();
}
}),
menu((() => buildOldAddColumnMenu(this, this.viewSection))),
]
}
} }
GridView.prototype._openInsertColumnMenu = function(columnIndex) { GridView.prototype._openInsertColumnMenu = function(columnIndex) {

View File

@ -25,10 +25,6 @@ export function WHICH_FORMULA_ASSISTANT() {
return getGristConfig().assistantService; return getGristConfig().assistantService;
} }
export function GRIST_NEW_COLUMN_MENU(){
return Boolean(getGristConfig().gristNewColumnMenu);
}
export function PERMITTED_CUSTOM_WIDGETS(): Observable<string[]> { export function PERMITTED_CUSTOM_WIDGETS(): Observable<string[]> {
const G = getBrowserGlobals('document', 'window'); const G = getBrowserGlobals('document', 'window');
if (!G.window.PERMITTED_CUSTOM_WIDGETS) { if (!G.window.PERMITTED_CUSTOM_WIDGETS) {

View File

@ -1,7 +1,6 @@
import {allCommands} from 'app/client/components/commands'; import {allCommands} from 'app/client/components/commands';
import GridView from 'app/client/components/GridView'; import GridView from 'app/client/components/GridView';
import {makeT} from 'app/client/lib/localization'; import {makeT} from 'app/client/lib/localization';
import {ViewSectionRec} from 'app/client/models/DocModel';
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
import {GristTooltips} from 'app/client/ui/GristTooltips'; import {GristTooltips} from 'app/client/ui/GristTooltips';
import {withInfoTooltip} from 'app/client/ui/tooltips'; import {withInfoTooltip} from 'app/client/ui/tooltips';
@ -29,18 +28,6 @@ import isEqual = require('lodash/isEqual');
const t = makeT('GridViewMenus'); const t = makeT('GridViewMenus');
// FIXME: remove once New Column menu is enabled by default.
export function buildOldAddColumnMenu(gridView: GridView, viewSection: ViewSectionRec) {
return [
menuItem(async () => { await gridView.insertColumn(); }, t("Add Column")),
menuDivider(),
...viewSection.hiddenColumns().map((col: any) => menuItem(
async () => {
await gridView.showColumn(col.id());
}, t("Show column {{- label}}", {label: col.label()})))
];
}
export function buildAddColumnMenu(gridView: GridView, index?: number) { export function buildAddColumnMenu(gridView: GridView, index?: number) {
const isSummaryTable = Boolean(gridView.viewSection.table().summarySourceTable()); const isSummaryTable = Boolean(gridView.viewSection.table().summarySourceTable());
return [ return [
@ -66,13 +53,14 @@ function buildHiddenColumnsMenuItems(gridView: GridView, index?: number) {
if (hiddenColumns.length <= 5) { if (hiddenColumns.length <= 5) {
return [ return [
menuDivider(), menuDivider(),
menuSubHeader(t('Hidden Columns'), testId('new-columns-menu-hidden-columns')), menuSubHeader(t('Hidden Columns'), testId('new-columns-menu-hidden-columns-header')),
hiddenColumns.map((col: ColumnRec) => hiddenColumns.map((col: ColumnRec) =>
menuItem( menuItem(
async () => { async () => {
await gridView.showColumn(col.id(), index); await gridView.showColumn(col.id(), index);
}, },
col.label(), col.label(),
testId('new-columns-menu-hidden-column-inlined'),
) )
), ),
]; ];
@ -84,13 +72,18 @@ function buildHiddenColumnsMenuItems(gridView: GridView, index?: number) {
return searchableMenu( return searchableMenu(
hiddenColumns.map((col) => ({ hiddenColumns.map((col) => ({
cleanText: col.label().trim().toLowerCase(), cleanText: col.label().trim().toLowerCase(),
builder: () => menuItemTrimmed(() => gridView.showColumn(col.id(), index), col.label()) builder: () => menuItemTrimmed(
() => gridView.showColumn(col.id(), index),
col.label(),
testId('new-columns-menu-hidden-column-collapsed'),
)
})), })),
{searchInputPlaceholder: t('Search columns')} {searchInputPlaceholder: t('Search columns')}
); );
}, },
{allowNothingSelected: true}, {allowNothingSelected: true},
t('Hidden Columns'), t('Hidden Columns'),
testId('new-columns-menu-hidden-columns-menu')
), ),
]; ];
} }
@ -145,7 +138,10 @@ function buildTimestampMenuItems(gridView: GridView, index?: number) {
t("Apply on record changes"), t("Apply on record changes"),
testId('new-columns-menu-shortcuts-timestamp-change'), testId('new-columns-menu-shortcuts-timestamp-change'),
), ),
], {}, t("Timestamp"), testId('new-columns-menu-shortcuts-timestamp')); ], {},
t("Timestamp"),
testId('new-columns-menu-shortcuts-timestamp')
);
} }
function buildAuthorshipMenuItems(gridView: GridView, index?: number) { function buildAuthorshipMenuItems(gridView: GridView, index?: number) {
@ -291,7 +287,7 @@ function buildUUIDMenuItem(gridView: GridView, index?: number) {
); );
} }
function menuLabelWithToast(label: string, toast: string) { function menuLabelWithBadge(label: string, toast: string) {
return cssListLabel( return cssListLabel(
cssListCol(label), cssListCol(label),
cssListFun(toast)); cssListFun(toast));
@ -332,7 +328,7 @@ function buildLookupSection(gridView: GridView, index?: number){
case 'count': case 'count':
case 'sum': return `SUM(${referenceToSource}.${col.colId()})`; case 'sum': return `SUM(${referenceToSource}.${col.colId()})`;
case 'percent': case 'percent':
return `AVERAGE(map(int, ${referenceToSource}.${col.colId()})) if ${referenceToSource} else 0`; return `AVERAGE(map(int, ${referenceToSource}.${col.colId()})) if ${referenceToSource} else None`;
default: return `${referenceToSource}`; default: return `${referenceToSource}`;
} }
} }
@ -365,8 +361,6 @@ function buildLookupSection(gridView: GridView, index?: number){
}; };
} }
function buildLookupsMenuItems() { function buildLookupsMenuItems() {
// Function that builds a menu for one of our Ref columns, we will show all columns // Function that builds a menu for one of our Ref columns, we will show all columns
// from the referenced table and offer to create a formula column with aggregation in case // from the referenced table and offer to create a formula column with aggregation in case
@ -383,7 +377,7 @@ function buildLookupSection(gridView: GridView, index?: number){
} else { } else {
// For RefList column we will show the column name and the aggregation function which is the first // For RefList column we will show the column name and the aggregation function which is the first
// on of suggested action (and a default action). // on of suggested action (and a default action).
label = menuLabelWithToast(col.label(), suggestAggregation(col)[0]); label = menuLabelWithBadge(col.label(), suggestAggregation(col)[0]);
} }
return { return {
@ -394,14 +388,36 @@ function buildLookupSection(gridView: GridView, index?: number){
function buildItem() { function buildItem() {
if (ref.pureType() === 'Ref') { if (ref.pureType() === 'Ref') {
// Just insert a plain menu item that will insert a formula column with lookup. // Just insert a plain menu item that will insert a formula column with lookup.
return menuItemTrimmed(() => insertPlainLookup(), col.label()); return menuItemTrimmed(
} else { () => insertPlainLookup(), col.label(),
// Built nested menu. testId(`new-columns-menu-lookup-column`),
return menuItemSubmenu( testId(`new-columns-menu-lookup-column-${col.colId()}`),
() => suggestAggregation(col).map((fun) => menuItem(() => insertAggLookup(fun), fun)),
{},
label
); );
} else {
// Depending on the number of aggregation functions we will either create a plain menu item
// or submenu with all the functions.
const functions = suggestAggregation(col);
if (functions.length === 1) {
const action = () => insertAggLookup(functions[0]);
return menuItem(action, label,
testId(`new-columns-menu-lookup-column`),
testId(`new-columns-menu-lookup-column-${col.colId()}`)
);
} else {
return menuItemSubmenu(
() => functions.map((fun) => menuItem(
() => insertAggLookup(fun), fun,
testId(`new-columns-menu-lookup-submenu-function`),
testId(`new-columns-menu-lookup-submenu-function-${fun}`),
)),
{
action: () => insertAggLookup(suggestAggregation(col)[0]),
},
label,
testId(`new-columns-menu-lookup-submenu`),
testId(`new-columns-menu-lookup-submenu-${col.colId()}`),
);
}
} }
} }
@ -451,7 +467,8 @@ function buildLookupSection(gridView: GridView, index?: number){
), ),
{allowNothingSelected: true}, {allowNothingSelected: true},
`${ref.refTable()?.tableNameDef()} [${ref.label()}]`, `${ref.refTable()?.tableNameDef()} [${ref.label()}]`,
testId(`new-columns-menu-lookups-${ref.colId()}`), testId(`new-columns-menu-lookup-${ref.colId()}`),
testId(`new-columns-menu-lookup`),
)); ));
} }
@ -463,6 +480,7 @@ function buildLookupSection(gridView: GridView, index?: number){
} }
function buildReverseLookupsMenuItems() { function buildReverseLookupsMenuItems() {
const getReferencesToThisTable = (): RefTable[] => { const getReferencesToThisTable = (): RefTable[] => {
const {viewSection} = gridView; const {viewSection} = gridView;
const otherTables = gridView.gristDoc.docModel.allTables.all().filter((tab) => const otherTables = gridView.gristDoc.docModel.allTables.all().filter((tab) =>
@ -473,16 +491,16 @@ function buildLookupSection(gridView: GridView, index?: number){
tableName: tab.tableNameDef(), tableName: tab.tableNameDef(),
columns: tab.visibleColumns(), columns: tab.visibleColumns(),
referenceFields: referenceFields:
tab.columns().peek().filter((c) => (c.pureType() === 'Ref' || c.pureType() == 'RefList') && tab.visibleColumns.peek().filter((c) => (c.pureType() === 'Ref' || c.pureType() == 'RefList') &&
c.refTable()?.tableId() === viewSection.tableId()) c.refTable()?.tableId() === viewSection.tableId())
}; };
}) })
.filter((tab) => tab.referenceFields.length > 0); .filter((tab) => tab.referenceFields.length > 0);
}; };
const buildColumn = async (tab: RefTable, col: ColumnRec, refCol: ColumnRec, aggregate: string) => { const insertColumn = async (tab: RefTable, col: ColumnRec, refCol: ColumnRec, aggregate: string) => {
const formula = `${tab.tableId}.lookupRecords(${refCol.colId()}= const formula =
${refCol.pureType() == 'RefList' ? 'CONTAINS($id)' : '$id'})`; `${tab.tableId}.lookupRecords(${refCol.colId()}=${refCol.pureType() == 'RefList' ? 'CONTAINS($id)' : '$id'})`;
await gridView.insertColumn(`${tab.tableId}_${col.label()}`, { await gridView.insertColumn(`${tab.tableId}_${col.label()}`, {
colInfo: { colInfo: {
label: `${tab.tableId}_${col.label()}`, label: `${tab.tableId}_${col.label()}`,
@ -495,41 +513,51 @@ function buildLookupSection(gridView: GridView, index?: number){
}); });
}; };
const buildSubmenuForRevLookup = (tab: RefTable, refCol: any) => { const tablesWithAnyRefColumn = getReferencesToThisTable();
return tablesWithAnyRefColumn.map((tab: RefTable) => tab.referenceFields.map((refCol) => {
const buildSubmenuForRevLookupMenuItem = (col: ColumnRec): SearchableMenuItem => { const buildSubmenuForRevLookupMenuItem = (col: ColumnRec): SearchableMenuItem => {
const suggestedColumns = suggestAggregation(col); const aggregationList = suggestAggregation(col);
const primarySuggestedColumn = suggestedColumns[0]; const firstAggregation = aggregationList[0];
if (!firstAggregation) {
throw new Error(`No aggregation suggested for column ${col.label()}`);
}
return { return {
cleanText: col.label().trim().toLowerCase(), cleanText: col.label().trim().toLowerCase(),
builder: () => { builder: () => {
if (suggestedColumns.length === 1) { const content = menuLabelWithBadge(col.label(), firstAggregation);
return menuItem(() => buildColumn(tab, col, refCol, primarySuggestedColumn), // In case we have only one suggested column we will just insert it, and there is no,
menuLabelWithToast(col.label(), primarySuggestedColumn)); // need for submenu.
if (aggregationList.length === 1) {
const action = () => insertColumn(tab, col, refCol, firstAggregation);
return menuItem(action, content, testId('new-columns-menu-revlookup-column'));
} else { } else {
return menuItemSubmenu((ctl) => // We have some other suggested columns, we will build submenu for them.
suggestedColumns.map(fun => const submenu = () => {
menuItem(async () => const items = aggregationList.map((fun) => {
buildColumn(tab, col, refCol, fun), t(fun))) const action = () => insertColumn(tab, col, refCol, fun);
, {}, menuLabelWithToast(col.label(), primarySuggestedColumn)); return menuItem(action, fun, testId('new-columns-menu-revlookup-column-function'));
});
return items;
};
const options = {};
return menuItemSubmenu(
submenu,
options,
content,
testId('new-columns-menu-revlookup-submenu'),
);
} }
} }
}; };
}; };
const label = `${tab.tableName} [← ${refCol.label()}]`;
return menuItemSubmenu( const options = {allowNothingSelected: true};
() => const submenu = () => {
searchableMenu( const subItems = tab.columns.map(buildSubmenuForRevLookupMenuItem);
tab.columns.map(col => buildSubmenuForRevLookupMenuItem(col)), return searchableMenu(subItems, {searchInputPlaceholder: t('Search columns')});
{searchInputPlaceholder: t('Search columns')} };
), return menuItemSubmenu(submenu, options, label, testId('new-columns-menu-revlookup'));
{allowNothingSelected: true}, `${tab.tableName} [← ${refCol.label()}]`); }));
};
const tablesWithAnyRefColumn = getReferencesToThisTable();
return tablesWithAnyRefColumn.map((tab: RefTable) => tab.referenceFields.map((refCol) =>
buildSubmenuForRevLookup(tab, refCol)
));
} }
const lookupMenu = buildLookupsMenuItems(); const lookupMenu = buildLookupsMenuItems();

View File

@ -701,9 +701,6 @@ export interface GristLoadConfig {
permittedCustomWidgets?: IAttachedCustomWidget[]; permittedCustomWidgets?: IAttachedCustomWidget[];
// Feature flag for the new column menu.
gristNewColumnMenu?: boolean;
// Used to determine which disclosure links should be provided to user of // Used to determine which disclosure links should be provided to user of
// formula assistance. // formula assistance.
assistantService?: 'OpenAI' | undefined; assistantService?: 'OpenAI' | undefined;

View File

@ -80,7 +80,6 @@ export function makeGristConfig(options: MakeGristConfigOptions): GristLoadConfi
featureFormulaAssistant: Boolean(process.env.OPENAI_API_KEY || process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT), featureFormulaAssistant: Boolean(process.env.OPENAI_API_KEY || process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT),
assistantService: process.env.OPENAI_API_KEY ? 'OpenAI' : undefined, assistantService: process.env.OPENAI_API_KEY ? 'OpenAI' : undefined,
permittedCustomWidgets: getPermittedCustomWidgets(server), permittedCustomWidgets: getPermittedCustomWidgets(server),
gristNewColumnMenu: isAffirmative(process.env.GRIST_NEW_COLUMN_MENU),
supportEmail: SUPPORT_EMAIL, supportEmail: SUPPORT_EMAIL,
userLocale: (req as RequestWithLogin | undefined)?.user?.options?.locale, userLocale: (req as RequestWithLogin | undefined)?.user?.options?.locale,
telemetry: server?.getTelemetry().getTelemetryConfig(req as RequestWithLogin | undefined), telemetry: server?.getTelemetry().getTelemetryConfig(req as RequestWithLogin | undefined),

View File

@ -10,7 +10,6 @@ processes = []
APP_STATIC_URL="https://{APP_NAME}.fly.dev" APP_STATIC_URL="https://{APP_NAME}.fly.dev"
ALLOWED_WEBHOOK_DOMAINS="webhook.site" ALLOWED_WEBHOOK_DOMAINS="webhook.site"
PERMITTED_CUSTOM_WIDGETS="calendar" PERMITTED_CUSTOM_WIDGETS="calendar"
GRIST_NEW_COLUMN_MENU="true"
GRIST_SINGLE_ORG="docs" GRIST_SINGLE_ORG="docs"
PORT = "8080" PORT = "8080"
FLY_DEPLOY_EXPIRATION = "{FLY_DEPLOY_EXPIRATION}" FLY_DEPLOY_EXPIRATION = "{FLY_DEPLOY_EXPIRATION}"

View File

@ -461,6 +461,7 @@ describe('CellColor', function() {
it('should handle correctly default text color', async function() { it('should handle correctly default text color', async function() {
// Create new checkbox column // Create new checkbox column
await driver.find('.mod-add-column').click(); await driver.find('.mod-add-column').click();
await driver.find('.test-new-columns-menu-add-new').click();
await gu.waitForServer(); await gu.waitForServer();
await gu.setType(/Toggle/); await gu.setType(/Toggle/);
@ -497,6 +498,8 @@ describe('CellColor', function() {
// create a new checkbox column // create a new checkbox column
await driver.find('.mod-add-column').click(); await driver.find('.mod-add-column').click();
await driver.find('.test-new-columns-menu-add-new').click();
await gu.waitForServer(); await gu.waitForServer();
await gu.setType(/Toggle/); await gu.setType(/Toggle/);

View File

@ -18,6 +18,7 @@ describe('ColumnOps.ntest', function() {
it("should allow adding and deleting columns", async function() { it("should allow adding and deleting columns", async function() {
await gu.clickColumnMenuItem('Name', 'Insert column to the right'); await gu.clickColumnMenuItem('Name', 'Insert column to the right');
await $('.test-new-columns-menu-add-new').click();
await gu.waitForServer(); await gu.waitForServer();
// Newly created columns labels become editable automatically. The next line checks that the // Newly created columns labels become editable automatically. The next line checks that the
// label is editable and then closes the editor. // label is editable and then closes the editor.
@ -78,7 +79,7 @@ describe('ColumnOps.ntest', function() {
// Then show it using the add column menu // Then show it using the add column menu
await $('.mod-add-column').scrollIntoView(true); await $('.mod-add-column').scrollIntoView(true);
await $(".mod-add-column").click(); await $(".mod-add-column").click();
await gu.actions.selectFloatingOption('Show column Name'); await showColumn('Name');
await gu.waitForServer(); await gu.waitForServer();
await assert.isPresent(gu.getColumnHeader('Name'), true); await assert.isPresent(gu.getColumnHeader('Name'), true);
}); });
@ -86,13 +87,14 @@ describe('ColumnOps.ntest', function() {
it("[+] button show add column directly if no hidden columns", async function() { it("[+] button show add column directly if no hidden columns", async function() {
await $('.mod-add-column').scrollIntoView(true); await $('.mod-add-column').scrollIntoView(true);
await $(".mod-add-column").click(); await $(".mod-add-column").click();
await gu.actions.selectFloatingOption('Show column Pop'); await showColumn("Pop");
await gu.waitForServer(); await gu.waitForServer();
await assert.isPresent(gu.getColumnHeader("Pop. '000"), true); await assert.isPresent(gu.getColumnHeader("Pop. '000"), true);
await assert.isPresent(gu.getColumnHeader('B'), false); await assert.isPresent(gu.getColumnHeader('B'), false);
await $('.mod-add-column').scrollIntoView(true); await $('.mod-add-column').scrollIntoView(true);
await $(".mod-add-column").click(); await $(".mod-add-column").click();
await $('.test-new-columns-menu-add-new').click();
await gu.waitToPass(() => gu.getColumnHeader('B')); await gu.waitToPass(() => gu.getColumnHeader('B'));
await gu.getOpenEditingLabel(await gu.getColumnHeader('B')).wait().sendKeys($.ENTER); await gu.getOpenEditingLabel(await gu.getColumnHeader('B')).wait().sendKeys($.ENTER);
await gu.waitForServer(); await gu.waitForServer();
@ -273,3 +275,7 @@ describe('ColumnOps.ntest', function() {
}); });
}); });
function showColumn(name) {
return $(`.test-new-columns-menu-hidden-column-inlined:contains(${name})`).click();
}

View File

@ -577,6 +577,7 @@ async function clickAddDescription() {
async function addColumn() { async function addColumn() {
await driver.find(".mod-add-column").click(); await driver.find(".mod-add-column").click();
await driver.find('.test-new-columns-menu-add-new').click();
await gu.waitForServer(); await gu.waitForServer();
} }

File diff suppressed because it is too large Load Diff

View File

@ -129,23 +129,23 @@ describe('MultiColumn', function() {
it('should show proper behavior label', async () => { it('should show proper behavior label', async () => {
await selectColumns('Test1'); await selectColumns('Test1');
assert.equal(await columnBehavior(), 'Empty Column'); assert.equal(await gu.columnBehavior(), 'Empty Column');
await selectColumns('Test1', 'Test3'); await selectColumns('Test1', 'Test3');
assert.equal(await columnBehavior(), 'Empty Columns'); assert.equal(await gu.columnBehavior(), 'Empty Columns');
// Change first to be data column. // Change first to be data column.
await selectColumns('Test1'); await selectColumns('Test1');
await driver.find(".test-field-set-data").click(); await driver.find(".test-field-set-data").click();
await gu.waitForServer(); await gu.waitForServer();
await selectColumns('Test1', 'Test3'); await selectColumns('Test1', 'Test3');
assert.equal(await columnBehavior(), 'Mixed Behavior'); assert.equal(await gu.columnBehavior(), 'Mixed Behavior');
// Change second to be a data column // Change second to be a data column
await selectColumns('Test2'); await selectColumns('Test2');
await driver.find(".test-field-set-data").click(); await driver.find(".test-field-set-data").click();
await gu.waitForServer(); await gu.waitForServer();
await selectColumns('Test1', 'Test2'); await selectColumns('Test1', 'Test2');
assert.equal(await columnBehavior(), 'Data Columns'); assert.equal(await gu.columnBehavior(), 'Data Columns');
// Now make them all formulas // Now make them all formulas
await gu.sendActions([ await gu.sendActions([
['ModifyColumn', 'Table1', 'Test1', {formula: '1', isFormula: true}], ['ModifyColumn', 'Table1', 'Test1', {formula: '1', isFormula: true}],
@ -153,13 +153,13 @@ describe('MultiColumn', function() {
['ModifyColumn', 'Table1', 'Test3', {formula: '1', isFormula: true}], ['ModifyColumn', 'Table1', 'Test3', {formula: '1', isFormula: true}],
]); ]);
await selectColumns('Test1', 'Test3'); await selectColumns('Test1', 'Test3');
assert.equal(await columnBehavior(), 'Formula Columns'); assert.equal(await gu.columnBehavior(), 'Formula Columns');
// Make one of them data column and test that the mix is recognized. // Make one of them data column and test that the mix is recognized.
await selectColumns('Test1'); await selectColumns('Test1');
await gu.changeBehavior('Convert column to data'); await gu.changeBehavior('Convert column to data');
await selectColumns('Test1', 'Test3'); await selectColumns('Test1', 'Test3');
assert.equal(await columnBehavior(), 'Mixed Behavior'); assert.equal(await gu.columnBehavior(), 'Mixed Behavior');
}); });
it('should reset multiple columns', async () => { it('should reset multiple columns', async () => {
@ -170,14 +170,14 @@ describe('MultiColumn', function() {
['ModifyColumn', 'Table1', 'Test3', {formula: '1', isFormula: true}], ['ModifyColumn', 'Table1', 'Test3', {formula: '1', isFormula: true}],
]); ]);
await selectColumns('Test1', 'Test3'); await selectColumns('Test1', 'Test3');
assert.equal(await columnBehavior(), 'Formula Columns'); assert.equal(await gu.columnBehavior(), 'Formula Columns');
await alignment('center'); await alignment('center');
assert.equal(await alignment(), 'center'); assert.equal(await alignment(), 'center');
// Reset all of them // Reset all of them
assert.deepEqual(await gu.availableBehaviorOptions(), ['Convert columns to data', 'Clear and reset']); assert.deepEqual(await gu.availableBehaviorOptions(), ['Convert columns to data', 'Clear and reset']);
await gu.changeBehavior('Clear and reset'); await gu.changeBehavior('Clear and reset');
assert.equal(await columnBehavior(), 'Empty Columns'); assert.equal(await gu.columnBehavior(), 'Empty Columns');
assert.equal(await alignment(), 'left'); assert.equal(await alignment(), 'left');
// Make them all data columns // Make them all data columns
@ -185,17 +185,17 @@ describe('MultiColumn', function() {
await gu.getCell('Test2', 1).click(); await gu.enterCell('a'); await gu.getCell('Test2', 1).click(); await gu.enterCell('a');
await gu.getCell('Test3', 1).click(); await gu.enterCell('a'); await gu.getCell('Test3', 1).click(); await gu.enterCell('a');
await selectColumns('Test1', 'Test3'); await selectColumns('Test1', 'Test3');
assert.equal(await columnBehavior(), 'Data Columns'); assert.equal(await gu.columnBehavior(), 'Data Columns');
await selectColumns('Test1'); await selectColumns('Test1');
assert.equal(await columnBehavior(), 'Data Column'); assert.equal(await gu.columnBehavior(), 'Data Column');
// Reset all of them // Reset all of them
await selectColumns('Test1', 'Test3'); await selectColumns('Test1', 'Test3');
assert.deepEqual(await gu.availableBehaviorOptions(), ['Clear and reset']); assert.deepEqual(await gu.availableBehaviorOptions(), ['Clear and reset']);
await gu.changeBehavior('Clear and reset'); await gu.changeBehavior('Clear and reset');
assert.equal(await columnBehavior(), 'Empty Columns'); assert.equal(await gu.columnBehavior(), 'Empty Columns');
await selectColumns('Test1'); await selectColumns('Test1');
assert.equal(await columnBehavior(), 'Empty Column'); assert.equal(await gu.columnBehavior(), 'Empty Column');
assert.equal(await gu.getCell('Test1', 1).getText(), ''); assert.equal(await gu.getCell('Test1', 1).getText(), '');
assert.equal(await gu.getCell('Test2', 1).getText(), ''); assert.equal(await gu.getCell('Test2', 1).getText(), '');
assert.equal(await gu.getCell('Test3', 1).getText(), ''); assert.equal(await gu.getCell('Test3', 1).getText(), '');
@ -203,12 +203,12 @@ describe('MultiColumn', function() {
it('should convert to data multiple columns', async () => { it('should convert to data multiple columns', async () => {
await selectColumns('Test1', 'Test3'); await selectColumns('Test1', 'Test3');
assert.equal(await columnBehavior(), 'Empty Columns'); assert.equal(await gu.columnBehavior(), 'Empty Columns');
assert.deepEqual(await gu.availableBehaviorOptions(), ['Convert columns to data', 'Clear and reset']); assert.deepEqual(await gu.availableBehaviorOptions(), ['Convert columns to data', 'Clear and reset']);
await gu.changeBehavior('Convert columns to data'); await gu.changeBehavior('Convert columns to data');
assert.equal(await columnBehavior(), 'Data Columns'); assert.equal(await gu.columnBehavior(), 'Data Columns');
await selectColumns('Test1'); await selectColumns('Test1');
assert.equal(await columnBehavior(), 'Data Column'); assert.equal(await gu.columnBehavior(), 'Data Column');
// Now make them all formula columns // Now make them all formula columns
await gu.sendActions([ await gu.sendActions([
@ -217,14 +217,14 @@ describe('MultiColumn', function() {
['ModifyColumn', 'Table1', 'Test3', {formula: '3', isFormula: true}], ['ModifyColumn', 'Table1', 'Test3', {formula: '3', isFormula: true}],
]); ]);
await selectColumns('Test1', 'Test3'); await selectColumns('Test1', 'Test3');
assert.equal(await columnBehavior(), 'Formula Columns'); assert.equal(await gu.columnBehavior(), 'Formula Columns');
// Convert them to data // Convert them to data
assert.deepEqual(await gu.availableBehaviorOptions(), ['Convert columns to data', 'Clear and reset']); assert.deepEqual(await gu.availableBehaviorOptions(), ['Convert columns to data', 'Clear and reset']);
await gu.changeBehavior('Convert columns to data'); await gu.changeBehavior('Convert columns to data');
assert.equal(await columnBehavior(), 'Data Columns'); assert.equal(await gu.columnBehavior(), 'Data Columns');
await selectColumns('Test1'); await selectColumns('Test1');
assert.equal(await columnBehavior(), 'Data Column'); assert.equal(await gu.columnBehavior(), 'Data Column');
// Test that data stays. // Test that data stays.
assert.equal(await gu.getCell('Test1', 1).getText(), '1'); assert.equal(await gu.getCell('Test1', 1).getText(), '1');
assert.equal(await gu.getCell('Test2', 1).getText(), '2'); assert.equal(await gu.getCell('Test2', 1).getText(), '2');
@ -1250,9 +1250,7 @@ async function toggleDerived() {
await gu.waitForServer(); await gu.waitForServer();
} }
async function columnBehavior() {
return (await driver.find(".test-field-behaviour").getText());
}
async function wrapDisabled() { async function wrapDisabled() {
return (await driver.find(".test-tb-wrap-text > div").matches('[class*=disabled]')); return (await driver.find(".test-tb-wrap-text > div").matches('[class*=disabled]'));

View File

@ -20,6 +20,7 @@ describe('ReferenceColumns', function() {
it('should render Row ID values as TableId[RowId]', async function() { it('should render Row ID values as TableId[RowId]', async function() {
await driver.find('.test-right-tab-field').click(); await driver.find('.test-right-tab-field').click();
await driver.find('.mod-add-column').click(); await driver.find('.mod-add-column').click();
await driver.findWait('.test-new-columns-menu-add-new', 100).click();
await gu.waitForServer(); await gu.waitForServer();
await gu.setType(/Reference/); await gu.setType(/Reference/);
await gu.waitForServer(); await gu.waitForServer();

View File

@ -253,6 +253,8 @@ describe('ReferenceList', function() {
// Create a new Reference List column. // Create a new Reference List column.
await driver.find('.test-right-tab-field').click(); await driver.find('.test-right-tab-field').click();
await driver.find('.mod-add-column').click(); await driver.find('.mod-add-column').click();
await driver.findWait('.test-new-columns-menu-add-new', 100).click();
await gu.waitForServer(); await gu.waitForServer();
await gu.setType(/Reference List/); await gu.setType(/Reference List/);
await gu.waitForServer(); await gu.waitForServer();

View File

@ -133,6 +133,7 @@ describe('TextEditor.ntest', function() {
async function addColumnRightOf(index) { async function addColumnRightOf(index) {
// Add a column. We have to hover over the column header first. // Add a column. We have to hover over the column header first.
await gu.openColumnMenu({col: index}, 'Insert column to the right'); await gu.openColumnMenu({col: index}, 'Insert column to the right');
await driver.find('.test-new-columns-menu-add-new').click();
await gu.waitForServer(); await gu.waitForServer();
await gu.sendKeys($.ESCAPE); await gu.sendKeys($.ESCAPE);
} }

View File

@ -16,7 +16,6 @@ import {CommandName} from 'app/client/components/commandList';
import {csvDecodeRow} from 'app/common/csvFormat'; import {csvDecodeRow} from 'app/common/csvFormat';
import { AccessLevel } from 'app/common/CustomWidget'; import { AccessLevel } from 'app/common/CustomWidget';
import { decodeUrl } from 'app/common/gristUrls'; import { decodeUrl } from 'app/common/gristUrls';
import { isAffirmative } from "app/common/gutil";
import { FullUser, UserProfile } from 'app/common/LoginSessionAPI'; import { FullUser, UserProfile } from 'app/common/LoginSessionAPI';
import { resetOrg } from 'app/common/resetOrg'; import { resetOrg } from 'app/common/resetOrg';
import { UserAction } from 'app/common/DocActions'; import { UserAction } from 'app/common/DocActions';
@ -689,20 +688,26 @@ export async function enterFormula(formula: string) {
} }
/** /**
* Check that formula editor is shown and its value matches the given regexp. * Check that formula editor is shown and returns its value.
* By default returns only text that is visible to the user, pass false to get all text.
*/ */
export async function getFormulaText() { export async function getFormulaText(onlyVisible = true): Promise<string> {
assert.equal(await driver.findWait('.test-formula-editor', 500).isDisplayed(), true); assert.equal(await driver.findWait('.test-formula-editor', 500).isDisplayed(), true);
return await driver.find('.code_editor_container').getText(); if (onlyVisible) {
return await driver.find('.code_editor_container').getText();
} else {
return await driver.executeScript(
() => (document as any).querySelector(".code_editor_container").innerText
);
}
} }
/** /**
* Check that formula editor is shown and its value matches the given regexp. * Check that formula editor is shown and its value matches the given regexp.
*/ */
export async function checkFormulaEditor(value: RegExp|string) { export async function checkFormulaEditor(value: RegExp|string) {
assert.equal(await driver.findWait('.test-formula-editor', 500).isDisplayed(), true);
const valueRe = typeof value === 'string' ? exactMatch(value) : value; const valueRe = typeof value === 'string' ? exactMatch(value) : value;
assert.match(await driver.find('.code_editor_container').getText(), valueRe); assert.match(await getFormulaText(), valueRe);
} }
/** /**
@ -1291,6 +1296,40 @@ export async function begin(invariant: () => any = () => true) {
}; };
} }
/**
* A hook that can be used to clear a state after suite is finished and current test passed.
* If under debugging session and NO_CLEANUP env variable is set it will skip this cleanup and allow you
* to examine the state of the database or browser.
*/
export function afterCleanup(test: () => void | Promise<void>) {
after(function() {
if (process.env.NO_CLEANUP) {
function anyTestFailed(suite: Mocha.Suite): boolean {
return suite.tests.some(t => t.state === 'failed') || suite.suites.some(anyTestFailed);
}
if (this.currentTest?.parent && anyTestFailed(this.currentTest?.parent)) {
return;
}
}
return test();
});
}
/**
* A hook that can be used to clear state after each test that has passed.
* If under debugging session and NO_CLEANUP env variable is set it will skip this cleanup and allow you
* to examine the state of the database or browser.
*/
export function afterEachCleanup(test: () => void | Promise<void>) {
afterEach(function() {
if (this.currentTest?.state !== 'passed' && !this.currentTest?.pending && process.env.NO_CLEANUP) {
return;
}
return test();
});
}
/** /**
* Simulates a transaction on the GristDoc. Use with cautions, as there is no guarantee it will undo correctly * Simulates a transaction on the GristDoc. Use with cautions, as there is no guarantee it will undo correctly
* in a case of failure. * in a case of failure.
@ -1546,6 +1585,7 @@ export function openColumnMenu(col: IColHeader|string, option?: string): WebElem
export async function deleteColumn(col: IColHeader|string) { export async function deleteColumn(col: IColHeader|string) {
await openColumnMenu(col, 'Delete column'); await openColumnMenu(col, 'Delete column');
await waitForServer(); await waitForServer();
await wipeToasts();
} }
export type ColumnType = export type ColumnType =
@ -2339,9 +2379,7 @@ export function hexToRgb(hex: string) {
export async function addColumn(name: string, type?: string) { export async function addColumn(name: string, type?: string) {
await scrollIntoView(await driver.find('.active_section .mod-add-column')); await scrollIntoView(await driver.find('.active_section .mod-add-column'));
await driver.find('.active_section .mod-add-column').click(); await driver.find('.active_section .mod-add-column').click();
if (isAffirmative(process.env.GRIST_NEW_COLUMN_MENU)) { await driver.findWait('.test-new-columns-menu-add-new', 100).click();
await driver.findWait('.test-new-columns-menu-add-new', 100).click();
}
// If we are on a summary table, we could be see a menu helper // If we are on a summary table, we could be see a menu helper
const menu = (await driver.findAll('.grist-floating-menu'))[0]; const menu = (await driver.findAll('.grist-floating-menu'))[0];
if (menu) { if (menu) {
@ -2360,7 +2398,11 @@ export async function addColumn(name: string, type?: string) {
export async function showColumn(name: string) { export async function showColumn(name: string) {
await scrollIntoView(await driver.find('.active_section .mod-add-column')); await scrollIntoView(await driver.find('.active_section .mod-add-column'));
await driver.find('.active_section .mod-add-column').click(); await driver.find('.active_section .mod-add-column').click();
await driver.findContent('.grist-floating-menu li', `Show column ${name}`).click(); if (await driver.findContent('.test-new-columns-menu-hidden-column-inlined', `${name}`).isPresent()) {
await driver.findContent('.test-new-columns-menu-hidden-column-inlined', `${name}`).click();
} else {
await driver.findContent('.test-new-columns-menu-hidden-column-collapsed', `${name}`).click();
}
await waitForServer(); await waitForServer();
} }
@ -2739,6 +2781,40 @@ export async function onNewTab(action: () => Promise<void>) {
await driver.switchTo().window(tabs[tabs.length - 2]); await driver.switchTo().window(tabs[tabs.length - 2]);
} }
/**
* Returns a controller for the current tab.
*/
export async function myTab() {
const tabs = await driver.getAllWindowHandles();
const myTab = tabs[tabs.length - 1];
return {
open() {
return driver.switchTo().window(myTab);
}
};
}
/**
* Duplicate current tab and return a controller for it. Assumes the current tab shows document.
*/
export async function duplicateTab() {
const url = await driver.getCurrentUrl();
await driver.executeScript("window.open('about:blank', '_blank')");
const tabs = await driver.getAllWindowHandles();
const myTab = tabs[tabs.length - 1];
await driver.switchTo().window(myTab);
await driver.get(url);
await waitForDocToLoad();
return {
close() {
return driver.close();
},
open() {
return driver.switchTo().window(myTab);
}
};
}
/** /**
* Scrolls active Grid or Card list view. * Scrolls active Grid or Card list view.
*/ */
@ -3042,6 +3118,10 @@ export async function changeBehavior(option: BehaviorActions|RegExp) {
await waitForServer(); await waitForServer();
} }
export async function columnBehavior() {
return (await driver.find(".test-field-behaviour").getText());
}
/** /**
* Gets all available options in the behavior menu. * Gets all available options in the behavior menu.
*/ */
@ -3426,15 +3506,15 @@ class Clipboard implements IClipboard {
/** /**
* Runs a Grist command in the browser window. * Runs a Grist command in the browser window.
*/ */
export async function sendCommand(name: CommandName) { export async function sendCommand(name: CommandName, argument: any = null) {
await driver.executeAsyncScript((name: any, done: any) => { await driver.executeAsyncScript((name: any, argument: any, done: any) => {
const result = (window as any).gristApp.allCommands[name].run(); const result = (window as any).gristApp.allCommands[name].run(argument);
if (result?.finally) { if (result?.finally) {
result.finally(done); result.finally(done);
} else { } else {
done(); done();
} }
}, name); }, name, argument);
await waitForServer(); await waitForServer();
} }