mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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
This commit is contained in:
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});
|
||||
},
|
||||
withInfoTooltip(
|
||||
t('UUID'),
|
||||
GristTooltips.uuid(),
|
||||
{variant: 'hover'}
|
||||
),
|
||||
testId('new-columns-menu-shortcuts-uuid'),
|
||||
);
|
||||
}
|
||||
@ -523,7 +529,14 @@ function buildLookupSection(gridView: GridView, index?: number){
|
||||
|
||||
return [
|
||||
menuDivider(),
|
||||
menuSubHeader(t("Lookups"), testId('new-columns-menu-lookups')),
|
||||
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",
|
||||
|
@ -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…
Reference in New Issue
Block a user