mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) Fixing DELETE and BACKSPACE keys on ChoiceList and RefList editor.
Summary: Choice/Reference List editor wasn't clearing itself when it received an empty string. It led to a bug on the Card widget where pressing those keys resulted in the same behavior as pressing Enter - it just opened the editor. Grid view has it's own implementation for those keys, so it wasn't affected. Test Plan: Added new test. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3908
This commit is contained in:
		
							parent
							
								
									dad41b2567
								
							
						
					
					
						commit
						c592691e31
					
				@ -66,7 +66,7 @@ export class ChoiceListEditor extends NewBaseEditor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // If starting to edit by typing in a string, ignore previous tokens.
 | 
					    // If starting to edit by typing in a string, ignore previous tokens.
 | 
				
			||||||
    const cellValue = decodeObject(options.cellValue);
 | 
					    const cellValue = decodeObject(options.cellValue);
 | 
				
			||||||
    const startLabels: unknown[] = options.editValue || !Array.isArray(cellValue) ? [] : cellValue;
 | 
					    const startLabels: unknown[] = options.editValue !== undefined || !Array.isArray(cellValue) ? [] : cellValue;
 | 
				
			||||||
    const startTokens = startLabels.map(label => new ChoiceItem(String(label), !choiceSet.has(String(label))));
 | 
					    const startTokens = startLabels.map(label => new ChoiceItem(String(label), !choiceSet.has(String(label))));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this._tokenField = TokenField.ctor<ChoiceItem>().create(this, {
 | 
					    this._tokenField = TokenField.ctor<ChoiceItem>().create(this, {
 | 
				
			||||||
 | 
				
			|||||||
@ -73,7 +73,7 @@ export class ReferenceListEditor extends NewBaseEditor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // If starting to edit by typing in a string, ignore previous tokens.
 | 
					    // If starting to edit by typing in a string, ignore previous tokens.
 | 
				
			||||||
    const cellValue = decodeObject(options.cellValue);
 | 
					    const cellValue = decodeObject(options.cellValue);
 | 
				
			||||||
    const startRowIds: unknown[] = options.editValue || !Array.isArray(cellValue) ? [] : cellValue;
 | 
					    const startRowIds: unknown[] = options.editValue !== undefined || !Array.isArray(cellValue) ? [] : cellValue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // If referenced table hasn't loaded yet, hold off on initializing tokens.
 | 
					    // If referenced table hasn't loaded yet, hold off on initializing tokens.
 | 
				
			||||||
    const needReload = (options.editValue === undefined && !this._utils.tableData.isLoaded);
 | 
					    const needReload = (options.editValue === undefined && !this._utils.tableData.isLoaded);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import {arrayRepeat} from 'app/plugin/gutil';
 | 
					import {arrayRepeat} from 'app/plugin/gutil';
 | 
				
			||||||
import * as gu from 'test/nbrowser/gristUtils';
 | 
					import * as gu from 'test/nbrowser/gristUtils';
 | 
				
			||||||
 | 
					import {ColumnType} from 'test/nbrowser/gristUtils';
 | 
				
			||||||
import {setupTestSuite} from 'test/nbrowser/testUtils';
 | 
					import {setupTestSuite} from 'test/nbrowser/testUtils';
 | 
				
			||||||
import {UserAPIImpl} from 'app/common/UserAPI';
 | 
					import {UserAPIImpl} from 'app/common/UserAPI';
 | 
				
			||||||
import {assert, driver, Key} from 'mocha-webdriver';
 | 
					import {assert, driver, Key} from 'mocha-webdriver';
 | 
				
			||||||
@ -9,8 +10,10 @@ let doc: string;
 | 
				
			|||||||
const transparent = 'rgba(0, 0, 0, 0)';
 | 
					const transparent = 'rgba(0, 0, 0, 0)';
 | 
				
			||||||
const blue = '#0000FF';
 | 
					const blue = '#0000FF';
 | 
				
			||||||
const red = '#FF0000';
 | 
					const red = '#FF0000';
 | 
				
			||||||
const types = ['Any', 'Text', 'Integer', 'Numeric', 'Toggle', 'Date', 'DateTime', 'Choice', 'Choice List',
 | 
					const types: Array<ColumnType> = [
 | 
				
			||||||
  'Reference', 'Reference List', 'Attachment'];
 | 
					  'Any', 'Text', 'Integer', 'Numeric', 'Toggle', 'Date', 'DateTime', 'Choice', 'Choice List',
 | 
				
			||||||
 | 
					  'Reference', 'Reference List', 'Attachment'
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('MultiColumn', function() {
 | 
					describe('MultiColumn', function() {
 | 
				
			||||||
  this.timeout(80000);
 | 
					  this.timeout(80000);
 | 
				
			||||||
@ -96,7 +99,7 @@ describe('MultiColumn', function() {
 | 
				
			|||||||
      await gu.assertFillColor(await gu.getCell('Test2', 1), transparent);
 | 
					      await gu.assertFillColor(await gu.getCell('Test2', 1), transparent);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const type of ['Choice', 'Text', 'Reference', 'Numeric']) {
 | 
					    for (const type of ['Choice', 'Text', 'Reference', 'Numeric'] as Array<ColumnType>) {
 | 
				
			||||||
      it(`should reset all columns to first column type for ${type}`, async () => {
 | 
					      it(`should reset all columns to first column type for ${type}`, async () => {
 | 
				
			||||||
        // We start with empty columns, then we will change first one
 | 
					        // We start with empty columns, then we will change first one
 | 
				
			||||||
        // to a data column, select all and then change all other to the same type.
 | 
					        // to a data column, select all and then change all other to the same type.
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										123
									
								
								test/nbrowser/TokenField.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								test/nbrowser/TokenField.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,123 @@
 | 
				
			|||||||
 | 
					import {assert, Key} from 'mocha-webdriver';
 | 
				
			||||||
 | 
					import * as gu from 'test/nbrowser/gristUtils';
 | 
				
			||||||
 | 
					import {setupTestSuite} from 'test/nbrowser/testUtils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('TokenField', function() {
 | 
				
			||||||
 | 
					  this.timeout(20000);
 | 
				
			||||||
 | 
					  const cleanup = setupTestSuite();
 | 
				
			||||||
 | 
					  let session: gu.Session;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before(async function() {
 | 
				
			||||||
 | 
					    session = await gu.session().login();
 | 
				
			||||||
 | 
					    await session.tempDoc(cleanup, "Favorite_Films.grist");
 | 
				
			||||||
 | 
					    await gu.toggleSidePanel('right', 'open');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Prepare test table, as a base for following tests.
 | 
				
			||||||
 | 
					    await gu.addNewPage('Table', 'New Table');
 | 
				
			||||||
 | 
					    await gu.sendKeys('one', Key.ENTER);
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					    await gu.sendKeys('two', Key.ENTER);
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					    await gu.sendKeys('three', Key.ENTER);
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should clear choice list on card view', async function() {
 | 
				
			||||||
 | 
					    // Test for a bug. In Card or Card List widgets, if the cursor is on a Ref List or Choice List field and you
 | 
				
			||||||
 | 
					    // hit Backspace or Delete , the behavior is the same as hitting enter (pulls up list of references/choices to add
 | 
				
			||||||
 | 
					    // one more, does not clear cell).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // The bug was there because choice list editor and ref list editor didn't handle empty string as an edit value,
 | 
				
			||||||
 | 
					    // which is a signal to clear the value. In a grid view, DELETE and BACKSPACE were handled by the grid itself.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const revert = await gu.begin();
 | 
				
			||||||
 | 
					    await gu.getCell('A', 1).click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Convert A column to Choice List.
 | 
				
			||||||
 | 
					    await gu.openColumnPanel();
 | 
				
			||||||
 | 
					    await gu.setType('Choice List', {apply: true});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add a second value to the first row.
 | 
				
			||||||
 | 
					    await gu.getCell('A', 1).click();
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ENTER);
 | 
				
			||||||
 | 
					    await gu.sendKeys('two', Key.ENTER);
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ENTER);
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					    await gu.getCell('A', 1).click();
 | 
				
			||||||
 | 
					    assert.equal(await gu.getCell('A', 1).getText(), 'one\ntwo');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Change it to card view.
 | 
				
			||||||
 | 
					    await gu.changeWidget('Card');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Test that DELETE opens the editor and clears the value.
 | 
				
			||||||
 | 
					    // Clicking on the cell twice will put it in edit mode, so we will first click other cell.
 | 
				
			||||||
 | 
					    await gu.getDetailCell('B', 1).click();
 | 
				
			||||||
 | 
					    await gu.getDetailCell('A', 1).click();
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.DELETE);
 | 
				
			||||||
 | 
					    await gu.checkTokenEditor('');
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ESCAPE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Now test BACKSPACE.
 | 
				
			||||||
 | 
					    await gu.getDetailCell('B', 1).click();
 | 
				
			||||||
 | 
					    await gu.getDetailCell('A', 1).click();
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.BACK_SPACE);
 | 
				
			||||||
 | 
					    await gu.checkTokenEditor('');
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ESCAPE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Value should still be there.
 | 
				
			||||||
 | 
					    assert.equal(await gu.getDetailCell('A', 1).getText(), 'one\ntwo');
 | 
				
			||||||
 | 
					    // But ENTER works fine, it just opens the editor.
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ENTER);
 | 
				
			||||||
 | 
					    await gu.checkTokenEditor('one\ntwo');
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ESCAPE);
 | 
				
			||||||
 | 
					    // Any other key also works
 | 
				
			||||||
 | 
					    await gu.sendKeys('a');
 | 
				
			||||||
 | 
					    await gu.checkTokenEditor('a');
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ESCAPE);
 | 
				
			||||||
 | 
					    await revert();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should clear ref list on card view', async function() {
 | 
				
			||||||
 | 
					    await gu.getCell(0, 1).click();
 | 
				
			||||||
 | 
					    await gu.changeBehavior('Clear and reset');
 | 
				
			||||||
 | 
					    // This is an empty column, so no transformation is needed.
 | 
				
			||||||
 | 
					    await gu.setType('Reference List', {apply: false});
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					    await gu.setRefTable('Films');
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					    await gu.setRefShowColumn('Title');
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add two films.
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ENTER);
 | 
				
			||||||
 | 
					    await gu.sendKeys('Toy', Key.ENTER);
 | 
				
			||||||
 | 
					    await gu.sendKeys('Alien', Key.ENTER);
 | 
				
			||||||
 | 
					    // Save.
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ENTER);
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Make sure it works in Grid view.
 | 
				
			||||||
 | 
					    await gu.getCell(0, 1).click();
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.DELETE);
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					    assert.equal(await gu.getCell(0, 1).getText(), '');
 | 
				
			||||||
 | 
					    await gu.undo();
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.BACK_SPACE);
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					    assert.equal(await gu.getCell(0, 1).getText(), '');
 | 
				
			||||||
 | 
					    await gu.undo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Now make sure it works in Card view.
 | 
				
			||||||
 | 
					    await gu.changeWidget('Card');
 | 
				
			||||||
 | 
					    assert.equal(await gu.getDetailCell('A', 1).getText(), 'Toy Story\nAlien');
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.DELETE);
 | 
				
			||||||
 | 
					    await gu.checkTokenEditor('');
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ESCAPE);
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.BACK_SPACE);
 | 
				
			||||||
 | 
					    await gu.checkTokenEditor('');
 | 
				
			||||||
 | 
					    await gu.sendKeys(Key.ESCAPE);
 | 
				
			||||||
 | 
					    await gu.waitForServer();
 | 
				
			||||||
 | 
					    // Nothing should have changed.
 | 
				
			||||||
 | 
					    assert.equal(await gu.getDetailCell('A', 1).getText(), 'Toy Story\nAlien');
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -654,6 +654,20 @@ export async function checkTextEditor(value: RegExp|string) {
 | 
				
			|||||||
  assert.match(await driver.find('.celleditor_text_editor').value(), valueRe);
 | 
					  assert.match(await driver.find('.celleditor_text_editor').value(), valueRe);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Checks that token editor in a cell has a correct value. Converts all tokens to text including the input field
 | 
				
			||||||
 | 
					 * and joins them with newlines.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function checkTokenEditor(value: RegExp|string) {
 | 
				
			||||||
 | 
					  assert.equal(await driver.findWait('.test-widget-text-editor', 500).isDisplayed(), true);
 | 
				
			||||||
 | 
					  const valueRe = typeof value === 'string' ? exactMatch(value) : value;
 | 
				
			||||||
 | 
					  const allTokens = await driver.findAll(
 | 
				
			||||||
 | 
					    '.test-widget-text-editor .test-tokenfield .test-tokenfield-token', e => e.getText());
 | 
				
			||||||
 | 
					  const inputToken = await driver.find('.test-widget-text-editor .test-tokenfield .test-tokenfield-input').value();
 | 
				
			||||||
 | 
					  const combined = [...allTokens, inputToken].join('\n').trim();
 | 
				
			||||||
 | 
					  assert.match(combined, valueRe);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Enter rows of values into a GridView, starting at the given cell. Values are specified as a
 | 
					 * Enter rows of values into a GridView, starting at the given cell. Values are specified as a
 | 
				
			||||||
 * list of rows, for examples `[['foo'], ['bar']]` will enter two rows, with one value in each.
 | 
					 * list of rows, for examples `[['foo'], ['bar']]` will enter two rows, with one value in each.
 | 
				
			||||||
@ -1181,7 +1195,10 @@ export async function selectWidget(
 | 
				
			|||||||
  await waitForServer();
 | 
					  await waitForServer();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function changeWidget(type: string) {
 | 
					export type WidgetType = 'Table' | 'Card' | 'Card List' | 'Chart' | 'Custom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function changeWidget(type: WidgetType) {
 | 
				
			||||||
  await openWidgetPanel();
 | 
					  await openWidgetPanel();
 | 
				
			||||||
  await driver.findContent('.test-right-panel button', /Change Widget/).click();
 | 
					  await driver.findContent('.test-right-panel button', /Change Widget/).click();
 | 
				
			||||||
  await selectWidget(type);
 | 
					  await selectWidget(type);
 | 
				
			||||||
@ -1406,6 +1423,7 @@ export async function waitForSidePanel() {
 | 
				
			|||||||
export async function openWidgetPanel() {
 | 
					export async function openWidgetPanel() {
 | 
				
			||||||
  await toggleSidePanel('right', 'open');
 | 
					  await toggleSidePanel('right', 'open');
 | 
				
			||||||
  await driver.find('.test-right-tab-pagewidget').click();
 | 
					  await driver.find('.test-right-tab-pagewidget').click();
 | 
				
			||||||
 | 
					  await driver.find(".test-config-widget").click();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -1585,11 +1603,15 @@ export async function deleteColumn(col: IColHeader|string) {
 | 
				
			|||||||
  await waitForServer();
 | 
					  await waitForServer();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ColumnType =
 | 
				
			||||||
 | 
					  'Any' | 'Text' | 'Numeric' | 'Integer' | 'Toggle' | 'Date' | 'DateTime' |
 | 
				
			||||||
 | 
					  'Choice' | 'Choice List' | 'Reference' | 'Reference List' | 'Attachment';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Sets the type of the currently selected field to value.
 | 
					 * Sets the type of the currently selected field to value.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export async function setType(
 | 
					export async function setType(
 | 
				
			||||||
  type: RegExp|string,
 | 
					  type: RegExp|ColumnType,
 | 
				
			||||||
  options: {skipWait?: boolean, apply?: boolean} = {}
 | 
					  options: {skipWait?: boolean, apply?: boolean} = {}
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const {skipWait, apply} = options;
 | 
					  const {skipWait, apply} = options;
 | 
				
			||||||
@ -2957,10 +2979,13 @@ export async function setWidgetUrl(url: string) {
 | 
				
			|||||||
  await waitForServer();
 | 
					  await waitForServer();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type BehaviorActions = 'Clear and reset' | 'Convert column to data' | 'Clear and make into formula' |
 | 
				
			||||||
 | 
					                       'Convert columns to data';
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Opens a behavior menu and clicks one of the option.
 | 
					 * Opens a behavior menu and clicks one of the option.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export async function changeBehavior(option: string|RegExp) {
 | 
					export async function changeBehavior(option: BehaviorActions|RegExp) {
 | 
				
			||||||
 | 
					  await openColumnPanel();
 | 
				
			||||||
  await driver.find('.test-field-behaviour').click();
 | 
					  await driver.find('.test-field-behaviour').click();
 | 
				
			||||||
  await driver.findContent('.grist-floating-menu li', option).click();
 | 
					  await driver.findContent('.grist-floating-menu li', option).click();
 | 
				
			||||||
  await waitForServer();
 | 
					  await waitForServer();
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user