(core) Add tests and tooltips to new Add Column menu

Summary: Adds tooltips to the menu and tests for recently-added functionality.

Test Plan: Browser tests.

Reviewers: JakubSerafin

Reviewed By: JakubSerafin

Subscribers: JakubSerafin

Differential Revision: https://phab.getgrist.com/D4087
pull/711/head
George Gevoian 7 months ago
parent 69d5ee53a8
commit de33c5a3c6

@ -3,6 +3,8 @@ import GridView from 'app/client/components/GridView';
import {makeT} from 'app/client/lib/localization';
import {ViewSectionRec} from 'app/client/models/DocModel';
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
import {GristTooltips} from 'app/client/ui/GristTooltips';
import {withInfoTooltip} from 'app/client/ui/tooltips';
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {
@ -265,7 +267,11 @@ function buildUUIDMenuItem(gridView: GridView, index?: number) {
await gridView.gristDoc.convertToTrigger(colRef, 'UUID()');
}, {nestInActiveBundle: true});
},
t('UUID'),
withInfoTooltip(
t('UUID'),
GristTooltips.uuid(),
{variant: 'hover'}
),
testId('new-columns-menu-shortcuts-uuid'),
);
}
@ -522,8 +528,15 @@ function buildLookupSection(gridView: GridView, index?: number){
: [lookupMenu, reverseLookupMenu];
return [
menuDivider(),
menuSubHeader(t("Lookups"), testId('new-columns-menu-lookups')),
menuDivider(),
menuSubHeader(
withInfoTooltip(
t('Lookups'),
GristTooltips.lookups(),
{variant: 'hover'}
),
testId('new-columns-menu-lookups'),
),
...menuContent
];
}

@ -35,7 +35,9 @@ export type Tooltip =
| 'workOnACopy'
| 'openAccessRules'
| 'addRowConditionalStyle'
| 'addColumnConditionalStyle';
| 'addColumnConditionalStyle'
| 'uuid'
| 'lookups';
export type TooltipContentFunc = (...domArgs: DomElementArg[]) => DomContents;
@ -98,6 +100,21 @@ see or edit which parts of your document.')
),
...args,
),
uuid: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('A UUID is a randomly-generated string that is useful for unique identifiers and link keys.')),
dom('div',
cssLink({href: commonUrls.helpLinkKeys, target: '_blank'}, t('Learn more.')),
),
...args,
),
lookups: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Lookups return data from related tables.')),
dom('div', t('Use reference columns to relate data in different tables.')),
dom('div',
cssLink({href: commonUrls.helpColRefs, target: '_blank'}, t('Learn more.')),
),
...args,
),
};
export interface BehavioralPromptContent {

@ -386,7 +386,7 @@ export class PageWidgetSelect extends Disposable {
testId('selectby'))
),
GristTooltips.selectBy(),
{tooltipMenuOptions: {attach: null}, domArgs: [
{popupOptions: {attach: null}, domArgs: [
this._behavioralPromptsManager.attachTip('pageWidgetPickerSelectBy', {
popupOptions: {
attach: null,

@ -256,7 +256,7 @@ function menuWorkOnCopy(pageModel: DocPageModel) {
withInfoTooltip(
t("Edit without affecting the original"),
GristTooltips.workOnACopy(),
{tooltipMenuOptions: {attach: null}}
{popupOptions: {attach: null}}
)
),
];

@ -12,7 +12,7 @@ import {makeLinks} from 'app/client/ui2018/links';
import {menuCssClass} from 'app/client/ui2018/menus';
import {dom, DomContents, DomElementArg, DomElementMethod, styled} from 'grainjs';
import Popper from 'popper.js';
import {cssMenu, defaultMenuOptions, IMenuOptions, setPopupToCreateDom} from 'popweasel';
import {cssMenu, cssMenuItem, defaultMenuOptions, IPopupOptions, setPopupToCreateDom} from 'popweasel';
import merge = require('lodash/merge');
export interface ITipOptions {
@ -307,10 +307,41 @@ export function tooltipCloseButton(ctl: ITooltipControl): HTMLElement {
);
}
export interface InfoTooltipOptions {
/** Defaults to `click`. */
variant?: InfoTooltipVariant;
/** Only applicable to the `click` variant. */
popupOptions?: IPopupOptions;
}
export type InfoTooltipVariant = 'click' | 'hover';
/**
* Renders an info icon that shows a tooltip with the specified `content` on click.
* Renders an info icon that shows a tooltip with the specified `content`.
*/
export function infoTooltip(content: DomContents, menuOptions?: IMenuOptions, ...domArgs: DomElementArg[]) {
export function infoTooltip(
content: DomContents,
options: InfoTooltipOptions = {},
...domArgs: DomElementArg[]
) {
const {variant = 'click'} = options;
switch (variant) {
case 'click': {
const {popupOptions} = options;
return buildClickableInfoTooltip(content, popupOptions, domArgs);
}
case 'hover': {
return buildHoverableInfoTooltip(content, domArgs);
}
}
}
function buildClickableInfoTooltip(
content: DomContents,
popupOptions?: IPopupOptions,
...domArgs: DomElementArg[]
) {
return cssInfoTooltipButton('?',
(elem) => {
setPopupToCreateDom(
@ -336,7 +367,7 @@ export function infoTooltip(content: DomContents, menuOptions?: IMenuOptions, ..
testId('info-tooltip-popup'),
);
},
{...defaultMenuOptions, ...{placement: 'bottom-end'}, ...menuOptions},
{...defaultMenuOptions, ...{placement: 'bottom-end'}, ...popupOptions},
);
},
testId('info-tooltip'),
@ -344,22 +375,41 @@ export function infoTooltip(content: DomContents, menuOptions?: IMenuOptions, ..
);
}
function buildHoverableInfoTooltip(content: DomContents, ...domArgs: DomElementArg[]) {
return cssInfoTooltipIcon('?',
hoverTooltip(() => cssInfoTooltipTransientPopup(
content,
cssTooltipCorner(testId('tooltip-origin')),
{tabIndex: '-1'},
testId('info-tooltip-popup'),
), {closeOnClick: false}),
testId('info-tooltip'),
...domArgs,
);
}
export interface WithInfoTooltipOptions {
/** Defaults to `click`. */
variant?: InfoTooltipVariant;
domArgs?: DomElementArg[];
tooltipButtonDomArgs?: DomElementArg[];
tooltipMenuOptions?: IMenuOptions;
iconDomArgs?: DomElementArg[];
/** Only applicable to the `click` variant. */
popupOptions?: IPopupOptions;
}
/**
* Wraps `domContent` with a info tooltip button that displays the provided
* `tooltipContent` on click, and returns the wrapped element.
* Wraps `domContent` with a info tooltip icon that displays the provided
* `tooltipContent` and returns the wrapped element.
*
* The tooltip button is displayed to the right of `domContents`, and displays
* a popup on click. The popup can be dismissed by clicking away from it, clicking
* the close button in the top-right corner, or pressing Enter or Escape.
* a popup on click by default. The popup can be dismissed by clicking away from
* it; clicking the close button in the top-right corner; or pressing Enter or Escape.
*
* You may optionally specify `options.variant`, which controls whether the tooltip
* is shown on hover or on click.
*
* Arguments can be passed to both the top-level wrapped DOM element and the
* tooltip button element with `options.domArgs` and `options.tooltipButtonDomArgs`
* tooltip icon element with `options.domArgs` and `options.tooltipIconDomArgs`
* respectively.
*
* Usage:
@ -374,10 +424,10 @@ export function withInfoTooltip(
tooltipContent: DomContents,
options: WithInfoTooltipOptions = {},
) {
const {domArgs, tooltipButtonDomArgs, tooltipMenuOptions} = options;
const {variant = 'click', domArgs, iconDomArgs, popupOptions} = options;
return cssDomWithTooltip(
domContents,
infoTooltip(tooltipContent, tooltipMenuOptions, tooltipButtonDomArgs),
infoTooltip(tooltipContent, {variant, popupOptions}, iconDomArgs),
...(domArgs ?? [])
);
}
@ -395,7 +445,7 @@ export function descriptionInfoTooltip(
key: 'columnDescription',
openOnClick: true,
};
const builder = () => cssDescriptionInfoTooltip(
const builder = () => cssInfoTooltipTransientPopup(
body,
// Used id test to find the origin of the tooltip regardless webdriver implementation (some of them start)
cssTooltipCorner(testId('tooltip-origin')),
@ -422,7 +472,7 @@ const cssTooltipCorner = styled('div', `
visibility: hidden;
`);
const cssDescriptionInfoTooltip = styled('div', `
const cssInfoTooltipTransientPopup = styled('div', `
position: relative;
white-space: pre-wrap;
text-align: left;
@ -489,7 +539,7 @@ const cssTooltipCloseButton = styled('div', `
}
`);
const cssInfoTooltipButton = styled('div', `
const cssInfoTooltipIcon = styled('div', `
flex-shrink: 0;
display: flex;
align-items: center;
@ -499,9 +549,17 @@ const cssInfoTooltipButton = styled('div', `
border: 1px solid ${theme.controlSecondaryFg};
color: ${theme.controlSecondaryFg};
border-radius: 50%;
cursor: pointer;
user-select: none;
.${cssMenuItem.className}-sel & {
color: ${theme.menuItemSelectedFg};
border-color: ${theme.menuItemSelectedFg};
}
`);
const cssInfoTooltipButton = styled(cssInfoTooltipIcon, `
cursor: pointer;
&:hover {
border: 1px solid ${theme.controlSecondaryHoverFg};
color: ${theme.controlSecondaryHoverFg};

@ -80,6 +80,7 @@ export function searchableMenu(
dom.autoDispose(searchValue),
dom.on('input', (_ev, elem) => { setSearchValue(elem.value); }),
{placeholder: searchInputPlaceholder},
testId('searchable-menu-input'),
),
),
),
@ -97,6 +98,7 @@ export function searchableMenu(
}
});
}),
testId('searchable-menu'),
];
}

@ -80,6 +80,7 @@ export const commonUrls = {
helpCustomWidgets: "https://support.getgrist.com/widget-custom",
helpTelemetryLimited: "https://support.getgrist.com/telemetry-limited",
helpCalendarWidget: "https://support.getgrist.com/widget-calendar",
helpLinkKeys: "https://support.getgrist.com/examples/2021-04-link-keys",
plans: "https://www.getgrist.com/pricing",
sproutsProgram: "https://www.getgrist.com/sprouts-program",
contact: "https://www.getgrist.com/contact",

@ -68,7 +68,7 @@ describe('GridViewNewColumnMenu', function () {
});
describe('column creation', function () {
it('should show rename menu after new column click', async function () {
it('should show rename menu after new column click', async function () {
const menu = await openAddColumnIcon();
await menu.findWait('.test-new-columns-menu-add-new', 100).click();
await driver.findWait('.test-column-title-popup', 100, 'rename menu is not present');
@ -86,6 +86,39 @@ describe('GridViewNewColumnMenu', function () {
assert.lengthOf(columns, 4, 'wrong number of columns');
await gu.undo();
});
it('should support inserting before selected column', async function () {
await gu.openColumnMenu('A', 'Insert column to the left');
await driver.findWait(".test-new-columns-menu", 100);
await gu.sendKeys(Key.ENTER);
await gu.waitForServer();
await driver.findWait('.test-column-title-close', 100).click();
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['D', 'A', 'B', 'C']);
await gu.undo();
});
it('should support inserting after selected column', async function () {
await gu.openColumnMenu('A', 'Insert column to the right');
await driver.findWait(".test-new-columns-menu", 100);
await gu.sendKeys(Key.ENTER);
await gu.waitForServer();
await driver.findWait('.test-column-title-close', 100).click();
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'D', 'B', 'C']);
await gu.undo();
});
it('should support inserting after the last visible column', async function () {
await gu.openColumnMenu('C', 'Insert column to the right');
await driver.findWait(".test-new-columns-menu", 100);
await gu.sendKeys(Key.ENTER);
await gu.waitForServer();
await driver.findWait('.test-column-title-close', 100).click();
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'D']);
await gu.undo();
});
});
describe('hidden columns', function () {
@ -103,13 +136,13 @@ describe('GridViewNewColumnMenu', function () {
await gu.addColumn('Add3');
});
it('1 to 5 hidden columns, secion should be inline', async function () {
it('1 to 5 hidden columns, section should be inline', async function () {
const checkSection = async (...columns: string[]) => {
const menu = await openAddColumnIcon();
await menu.findWait(".test-new-columns-menu-hidden-columns", 100,
'hidden section is not present');
for (const column of columns) {
const isColumnPresent = await menu.find(`.test-new-columns-menu-hidden-columns-${column}`).isPresent();
const isColumnPresent = await menu.findContent('li', column).isPresent();
assert.isTrue(isColumnPresent, `column ${column} is not present`);
}
await closeAddColumnMenu();
@ -123,7 +156,7 @@ describe('GridViewNewColumnMenu', function () {
await gu.moveToHidden('Add1');
await gu.moveToHidden('Add2');
await checkSection('A', 'B', 'C', 'Add1', 'Add2');
await gu.undo(5);
await gu.undo(11);
});
it('inline button should show column at the end of the table', async function () {
@ -156,11 +189,7 @@ describe('GridViewNewColumnMenu', function () {
});
});
describe('shortucts', function () {
describe('Timestamp', function () {
it('created at - should create new column with date triggered on create');
});
describe('shortcuts', function () {
describe('Timestamp', function () {
it('created at - should create new column with date triggered on create', function () {
@ -178,6 +207,84 @@ describe('GridViewNewColumnMenu', function () {
});
});
describe('Detect Duplicates in...', function () {
it('should show columns in a searchable sub-menu', async function () {
const menu = await openAddColumnIcon();
await menu.findWait('.test-new-columns-menu-shortcuts-duplicates', 100).mouseMove();
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-searchable-menu li', (el) => el.getText()),
['A', 'B', 'C']
);
}, 500);
await driver.find('.test-searchable-menu-input').click();
await gu.sendKeys('A');
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-searchable-menu li', (el) => el.getText()),
['A']
);
}, 250);
await gu.sendKeys('BC');
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-searchable-menu li', (el) => el.getText()),
[]
);
}, 250);
await gu.clearInput();
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-searchable-menu li', (el) => el.getText()),
['A', 'B', 'C']
);
}, 250);
});
it('should create new column that checks for duplicates in the specified column', async function () {
const menu = await openAddColumnIcon();
await menu.findWait('.test-new-columns-menu-shortcuts-duplicates', 100).mouseMove();
await driver.findContentWait('.test-searchable-menu li', 'A', 500).click();
await gu.waitForServer();
await gu.sendKeys(Key.ENTER);
// Just checking the formula looks plausible - correctness is best left to a python test.
assert.equal(
await driver.find('.test-formula-editor').getText(),
'True if len(Table1.lookupRecords(A=$A)) > 1 else False'
);
await gu.sendKeys(Key.ESCAPE);
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'Duplicate in A']);
await gu.undo();
});
});
describe('UUID', function () {
it('should create new column that generates a UUID on new record', async function () {
await gu.getCell(2, 1).click();
await gu.sendKeys('A', Key.ENTER);
await gu.waitForServer();
const menu = await openAddColumnIcon();
await menu.findWait('.test-new-columns-menu-shortcuts-uuid', 100).click();
await gu.waitForServer();
const cells1 = await gu.getVisibleGridCells({col: 'UUID', rowNums: [1, 2]});
assert.match(cells1[0], /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
assert.equal(cells1[1], '');
await gu.getCell(2, 2).click();
await gu.sendKeys('B', Key.ENTER);
await gu.waitForServer();
const cells2 = await gu.getVisibleGridCells({col: 'UUID', rowNums: [1, 2, 3]});
assert.match(cells2[0], /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
assert.match(cells2[1], /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
assert.equal(cells2[2], '');
assert.equal(cells1[0], cells2[0]);
await gu.undo(3);
});
});
});

Loading…
Cancel
Save