Skip to content

Commit

Permalink
(core) Rename popup for group by columns
Browse files Browse the repository at this point in the history
Summary:
Showing rename popup on group by column with disabled label section. It only allows to set description.

Unrelated:
- Fixing HostedMetadataManager test

Test Plan: Updated tests

Reviewers: Spoffy

Reviewed By: Spoffy

Subscribers: Spoffy

Differential Revision: https://phab.getgrist.com/D4371
  • Loading branch information
Spoffy authored and berhalak committed Oct 9, 2024
1 parent 0bdc838 commit 5d349e6
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 25 deletions.
8 changes: 5 additions & 3 deletions app/client/components/GridView.js
Original file line number Diff line number Diff line change
Expand Up @@ -1270,12 +1270,12 @@ GridView.prototype.buildDom = function() {
kd.style('minWidth', '100%'),
kd.style('borderLeftWidth', v.borderWidthPx),
kd.foreach(v.viewFields(), field => {
const canRename = ko.pureComputed(() => !field.column().disableEditData());
const isEditingLabel = koUtil.withKoUtils(ko.pureComputed({
read: () => {
const goodIndex = () => editIndex() === field._index();
const isReadonly = () => this.isReadonly || self.isPreview;
const isSummary = () => Boolean(field.column().disableEditData());
return goodIndex() && !isReadonly() && !isSummary();
return goodIndex() && !isReadonly();
},
write: val => {
if (val) {
Expand Down Expand Up @@ -1306,6 +1306,7 @@ GridView.prototype.buildDom = function() {

return dom(
'div.column_name.field',
dom.autoDispose(canRename),
dom.autoDispose(headerTextColor),
dom.autoDispose(headerFillColor),
dom.autoDispose(headerFontBold),
Expand Down Expand Up @@ -1355,7 +1356,8 @@ GridView.prototype.buildDom = function() {
buildRenameColumn({
field,
isEditing: isEditingLabel,
optCommands: renameCommands
optCommands: renameCommands,
canRename,
}),
),
self._showTooltipOnHover(field, isTooltip),
Expand Down
57 changes: 46 additions & 11 deletions app/client/ui/ColumnTitle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,43 @@ import {FocusLayer} from 'app/client/lib/FocusLayer';
import {makeT} from 'app/client/lib/localization';
import {setTestState} from 'app/client/lib/testState';
import {ViewFieldRec} from 'app/client/models/DocModel';
import {LIMITED_COLUMN_OPTIONS} from 'app/client/ui/FieldConfig';
import {autoGrow} from 'app/client/ui/forms';
import {showTransientTooltip} from 'app/client/ui/tooltips';
import {cssInput, cssLabel, cssRenamePopup, cssTextArea} from 'app/client/ui/RenamePopupStyles';
import {hoverTooltip, showTransientTooltip} from 'app/client/ui/tooltips';
import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons';
import {theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {menuCssClass} from 'app/client/ui2018/menus';

import {Computed, dom, makeTestId, Observable, styled} from 'grainjs';
import * as ko from 'knockout';
import {IOpenController, PopupControl, setPopupToCreateDom} from 'popweasel';
import { cssInput, cssLabel, cssRenamePopup, cssTextArea } from 'app/client/ui/RenamePopupStyles';


const testId = makeTestId('test-column-title-');
const t = makeT('ColumnTitle');

interface IColumnTitleOptions {
/**
* The field to rename.
*/
field: ViewFieldRec;
/**
* An observable that triggers the popup to open when set to true.
*/
isEditing: ko.Computed<boolean>;
/**
* Optional commands to bind when the popup is open.
*/
optCommands?: any;
/**
* Optional computed or boolean to determine if the column can be renamed. Defaults to true.
*/
canRename?: ko.Computed<boolean>|boolean;
/**
* Optional computed or boolean to determine if the description can be changed. Defaults to true.
*/
canChangeDesc?: ko.Computed<boolean>|boolean;
}

export function buildRenameColumn(options: IColumnTitleOptions) {
Expand All @@ -49,9 +66,8 @@ export function buildRenameColumn(options: IColumnTitleOptions) {
};
}

function buildColumnRenamePopup(
ctrl: IOpenController, {field, isEditing, optCommands}: IColumnTitleOptions
) {
function buildColumnRenamePopup(ctrl: IOpenController, options: IColumnTitleOptions) {
const {field, isEditing, optCommands} = options;
// Store temporary values for the label and description.
const editedLabel = Observable.create(ctrl, field.displayLabel.peek());
const editedDesc = Observable.create(ctrl, field.description.peek());
Expand Down Expand Up @@ -159,20 +175,30 @@ function buildColumnRenamePopup(

// Create this group and attach it to the popup and both inputs.
const commandGroup = commands.createGroup({...optCommands, ...myCommands}, ctrl, true);
ctrl.autoDispose(commandGroup);

// We will still focus from other elements and restore it on either the label or description input.
let lastFocus: HTMLElement | undefined;
const rememberFocus = (el: HTMLElement) => dom.on('focus', () => lastFocus = el);
const restoreFocus = (el: HTMLElement) => dom.on('focus', () => lastFocus?.focus());

const showDesc = Observable.create(null, Boolean(field.description.peek() !== ''));
const showDesc = Observable.create(ctrl, Boolean(field.description.peek() !== ''));

const defaultTrue = (val: boolean|ko.Computed<boolean>|undefined) => {
return val === undefined ? true : val;
};
const toComputed = (val: boolean|ko.Computed<boolean>) =>
typeof val === 'boolean' ? Computed.create(ctrl, () => val) : Computed.create(ctrl, use => use(val));

const not = (val: Observable<boolean>) => Computed.create(ctrl, (use) => !use(val));

const canRename = toComputed(defaultTrue(options.canRename));
const canChangeDesc = toComputed(defaultTrue(options.canChangeDesc));

let labelInput: HTMLInputElement | undefined;
let descInput: HTMLTextAreaElement | undefined;
return cssRenamePopup(
dom.onDispose(onClose),
dom.autoDispose(commandGroup),
dom.autoDispose(showDesc),
testId('popup'),
dom.cls(menuCssClass),
cssLabel(t("Column label")),
Expand All @@ -184,6 +210,9 @@ function buildColumnRenamePopup(
testId('label'),
commandGroup.attach(),
rememberFocus,
hoverTooltip(LIMITED_COLUMN_OPTIONS, {hidden: canRename}),
dom.boolAttr('disabled', not(canRename)),
dom.style('pointer-events', 'all')
),
cssColId(
t("COLUMN ID: "),
Expand Down Expand Up @@ -219,6 +248,7 @@ function buildColumnRenamePopup(
commandGroup.attach(),
rememberFocus,
autoGrow(editedDesc),
dom.boolAttr('disabled', not(canChangeDesc)),
),
]),
dom.onKeyDown({
Expand Down Expand Up @@ -251,8 +281,13 @@ function buildColumnRenamePopup(
// After showing the popup, focus the label input and select it's content.
elem => { setTimeout(() => {
if (ctrl.isDisposed()) { return; }
labelInput?.focus();
labelInput?.select();
if (canRename.get()) {
labelInput?.focus();
labelInput?.select();
} else if (canChangeDesc.get()) {
descInput?.focus();
descInput?.select();
}
}, 0); },
// Create a FocusLayer to keep focus in this popup while it's active, by default when focus is stolen
// by someone else, we will bring back it to the label element. Clicking anywhere outside the popup
Expand Down
4 changes: 3 additions & 1 deletion app/client/ui/FieldConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import * as ko from 'knockout';

const t = makeT('FieldConfig');

export const LIMITED_COLUMN_OPTIONS = t("Column options are limited in summary tables.");

export function buildNameConfig(
owner: MultiHolder,
origColumn: ColumnRec,
Expand Down Expand Up @@ -86,7 +88,7 @@ export function buildNameConfig(
)
),
dom.maybe(isSummaryTable,
() => cssRow(t("Column options are limited in summary tables.")))
() => cssRow(LIMITED_COLUMN_OPTIONS))
];
}

Expand Down
11 changes: 10 additions & 1 deletion app/client/ui/tooltips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {testId, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {makeLinks} from 'app/client/ui2018/links';
import {menuCssClass} from 'app/client/ui2018/menus';
import {BindableValue, dom, DomContents, DomElementArg, DomElementMethod, styled} from 'grainjs';
import {BindableValue, dom, DomContents, DomElementArg, DomElementMethod, Observable, styled} from 'grainjs';
import Popper from 'popper.js';
import {cssMenu, cssMenuItem, defaultMenuOptions, IPopupOptions, setPopupToCreateDom} from 'popweasel';
import merge = require('lodash/merge');
Expand Down Expand Up @@ -79,6 +79,11 @@ export interface IHoverTipOptions extends ITransientTipOptions {

/** Whether to show the tooltip only when the ref element overflows horizontally. */
overflowOnly?: boolean;

/**
* If set tooltip won't be shown on hover. Default to false.
*/
hidden?: Observable<boolean>;
}

export type ITooltipContent = ITooltipContentFunc | DomContents;
Expand Down Expand Up @@ -250,6 +255,7 @@ export function setHoverTooltip(
}
}
function open() {
if (options.hidden?.get()) { return; }
clearTimer();
tipControl = showTooltip(refElem, ctl => tipContentFunc({...ctl, close}), options);
const tipDom = tipControl.getDom();
Expand All @@ -260,13 +266,16 @@ export function setHoverTooltip(
if (timeoutMs) { resetTimer(close, timeoutMs); }
}
function close() {
if (options.hidden?.get()) { return; }
clearTimer();
tipControl?.close();
tipControl = undefined;
}

// We simulate hover effect by handling mouseenter/mouseleave.
dom.onElem(refElem, 'mouseenter', () => {
if (options.hidden?.get()) { return; }

if (overflowOnly && (refElem as HTMLElement).offsetWidth >= refElem.scrollWidth) {
return;
}
Expand Down
6 changes: 5 additions & 1 deletion app/common/gutil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,11 @@ export function useBindable<T>(use: UseCBOwner, obs: BindableValue<T>): T {
/**
* Useful helper for simple boolean negation.
*/
export const not = (obs: Observable<any>|IKnockoutReadObservable<any>) => (use: UseCBOwner) => !use(obs);
export const not = (obs: Observable<any>|IKnockoutReadObservable<any>|boolean|undefined|null) => (use: UseCBOwner) => {
if (typeof obs === 'boolean') { return !obs; }
if (obs === null || obs === undefined) { return true; }
return !use(obs);
};

/**
* Get a set of up to `count` distinct values of `values`.
Expand Down
2 changes: 1 addition & 1 deletion app/server/lib/HostedStorageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export class HostedStorageManager implements IDocStorageManager {
if (!externalStoreDoc) { this._disableS3 = true; }
const secondsBeforePush = options.secondsBeforePush;
if (options.pushDocUpdateTimes) {
this._metadataManager = new HostedMetadataManager(callbacks.setDocsMetadata);
this._metadataManager = new HostedMetadataManager(callbacks.setDocsMetadata.bind(callbacks));
}
this._uploads = new KeyedOps(key => this._pushToS3(key), {
delayBeforeOperationMs: secondsBeforePush * 1000,
Expand Down
60 changes: 53 additions & 7 deletions test/nbrowser/DescriptionColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('DescriptionColumn', function() {
});

it('should allow to edit description on summary table', async () => {
const revert = await gu.begin();
await gu.toggleSidePanel('left', 'close');
// Add summary table.
await gu.addNewSection('Table', 'Table1', {summarize: ['A']});
Expand All @@ -37,7 +38,45 @@ describe('DescriptionColumn', function() {
assert.equal(await getDescriptionInput().getAttribute('value'), 'testA');
await gu.openColumnPanel('count');
assert.equal(await getDescriptionInput().getAttribute('value'), 'testCount');
await gu.undo(4);
await gu.undo(2);

// Now add description through the modal.
await doubleClickHeader('A', null);
assert.isTrue(await popupVisible());

// Name should be disabled.
assert.equal(await getLabel().getAttribute('disabled'), 'true');

// We see add description button.
await addDescriptionIsVisible();

// We have tooltip explaining what it is.
await getLabel().mouseMove();

// Wait for hover tooltip to show up.
await gu.waitToPass(
async () => assert.isTrue(await driver.find('.test-tooltip').isDisplayed()),
500
);
assert.equal(await driver.find('.test-tooltip').getText(), 'Column options are limited in summary tables.');

// It works.
await clickAddDescription();

// Now we see description field.
await descriptionIsVisible();

// Type something.
await gu.sendKeys('ColumnA description');

// Save it.
await pressSave();

// Make sure it is saved.
await clickTooltip('A');
await gu.waitToPass(async () =>
assert.equal(await driver.find(".test-column-info-tooltip-popup").getText(), 'ColumnA description'));
await revert();
});

it('should switch between close and save', async () => {
Expand Down Expand Up @@ -94,7 +133,7 @@ describe('DescriptionColumn', function() {

// Clear label completely, we have change, but we can't save.
await gu.sendKeys(Key.BACK_SPACE);
assert.isEmpty(await getLabel());
assert.isEmpty(await getLabelText());
assert.isFalse(await closeVisible());
assert.isTrue(await saveVisible());
// But save button is disabled.
Expand Down Expand Up @@ -535,8 +574,12 @@ function getDescriptionInput() {
return driver.find('.test-right-panel .test-column-description');
}

function getLabelText() {
return getLabel().getAttribute('value');
}

function getLabel() {
return driver.findWait(".test-column-title-label", 1000).getAttribute('value');
return driver.findWait(".test-column-title-label", 1000);
}

async function popupVisible() {
Expand All @@ -549,7 +592,7 @@ async function popupVisible() {

async function popupIsAt(col: string) {
// Make sure we are now at column.
assert.equal(await getLabel(), col);
assert.equal(await getLabelText(), col);
// Make sure that popup is near the column.
const headerCRect = await gu.getColumnHeader({col}).getRect();
const popup = await driver.find(".test-column-title-popup").getRect();
Expand All @@ -559,15 +602,18 @@ async function popupIsAt(col: string) {
assert.isBelow(popup.y, headerCRect.y + headerCRect.height + 2);
}

async function doubleClickHeader(col: string) {
async function doubleClickHeader(col: string, focus: 'label'|'description'|null = 'label') {
const header = await gu.getColumnHeader({col});
await header.click();
await header.click();
await waitForFocus('label');
if (focus) {
await waitForFocus(focus);
}
}

async function waitForFocus(field: 'label'|'description') {
await gu.waitToPass(async () => assert.isTrue(await driver.find(`.test-column-title-${field}`).hasFocus()), 200);
await gu.waitToPass(async () => assert.isTrue(
await driver.find(`.test-column-title-${field}`).hasFocus(), `${field} doesn't have focus`), 200);
}

async function waitForTooltip() {
Expand Down

0 comments on commit 5d349e6

Please sign in to comment.