(core) Formula UI redesign

Summary:
Redesigning column type section to make it more user-friendly. Introducing column behavior concept.
Column can be either:
- Empty Formula Column: initial state (user can convert to Formula/Data Column)
- Data Column: non formula column with or without trigger (with option to add trigger, or convert to formula)
- Formula Column: pure formula column, with an option to convert to data column with a trigger.

Test Plan: Existing tests.

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D3092
This commit is contained in:
Jarosław Sadziński
2021-11-05 11:25:05 +01:00
parent 877542225d
commit e8e614c584
12 changed files with 532 additions and 126 deletions

View File

@@ -38,6 +38,7 @@ export type IconName = "ChartArea" |
"Copy" |
"CrossBig" |
"CrossSmall" |
"Database" |
"Dots" |
"Download" |
"DragDrop" |
@@ -79,6 +80,7 @@ export type IconName = "ChartArea" |
"Repl" |
"ResizePanel" |
"RightAlign" |
"Script" |
"Search" |
"Settings" |
"Share" |
@@ -133,6 +135,7 @@ export const IconList: IconName[] = ["ChartArea",
"Copy",
"CrossBig",
"CrossSmall",
"Database",
"Dots",
"Download",
"DragDrop",
@@ -174,6 +177,7 @@ export const IconList: IconName[] = ["ChartArea",
"Repl",
"ResizePanel",
"RightAlign",
"Script",
"Search",
"Settings",
"Share",

View File

@@ -100,6 +100,16 @@ export const bigBasicButtonLink = tbind(button, null, {link: true, large: true})
export const primaryButtonLink = tbind(button, null, {link: true, primary: true});
export const bigPrimaryButtonLink = tbind(button, null, {link: true, large: true, primary: true});
// Button that looks like a link (have no background and no border).
export const textButton = styled(cssButton, `
border: none;
padding: 0px;
background-color: inherit !important;
&:disabled {
color: ${colors.inactiveCursor};
}
`);
const cssButtonLink = styled('a', `
display: inline-block;
&, &:hover, &:focus {

View File

@@ -1,14 +1,14 @@
import {Command} from 'app/client/components/commands';
import {NeedUpgradeError, reportError} from 'app/client/models/errors';
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
import {cssSelectBtn} from 'app/client/ui2018/select';
import {IconName} from 'app/client/ui2018/IconList';
import {icon} from 'app/client/ui2018/icons';
import {commonUrls} from 'app/common/gristUrls';
import {Computed, dom, DomElementArg, DomElementMethod, MaybeObsArray, MutableObsArray, Observable,
styled} from 'grainjs';
import { Command } from 'app/client/components/commands';
import { NeedUpgradeError, reportError } from 'app/client/models/errors';
import { cssCheckboxSquare, cssLabel, cssLabelText } from 'app/client/ui2018/checkbox';
import { colors, testId, vars } from 'app/client/ui2018/cssVars';
import { IconName } from 'app/client/ui2018/IconList';
import { icon } from 'app/client/ui2018/icons';
import { cssSelectBtn } from 'app/client/ui2018/select';
import { commonUrls } from 'app/common/gristUrls';
import { BindableValue, Computed, dom, DomElementArg, DomElementMethod, IDomArgs,
MaybeObsArray, MutableObsArray, Observable, styled } from 'grainjs';
import * as weasel from 'popweasel';
import {cssCheckboxSquare, cssLabel, cssLabelText} from 'app/client/ui2018/checkbox';
export interface IOptionFull<T> {
value: T;
@@ -304,6 +304,71 @@ export function autocomplete(
});
}
/**
* Creates simple (not reactive) static menu that looks like a select-box.
* Primary usage is for menus, where you want to control how the options and a default
* label will look. Label is not updated or changed when one of the option is clicked, for those
* use cases use a select component.
* Icons are optional, can use custom elements instead of labels and options.
*
* Usage:
*
* selectMenu(selectTitle("Title", "Script"), () => [
* selectOption(() => ..., "Option1", "Database"),
* selectOption(() => ..., "Option2", "Script"),
* ]);
*
* // Control disabled state (if the menu will be opened or not)
*
* const disabled = observable(false);
* selectMenu(selectTitle("Title", "Script"), () => [
* selectOption(() => ..., "Option1", "Database"),
* selectOption(() => ..., "Option2", "Script"),
* ], disabled);
*
*/
export function selectMenu(
label: DomElementArg,
items: () => DomElementArg[],
...args: IDomArgs<HTMLDivElement>
) {
return cssSelectBtn(
label,
icon('Dropdown'),
menu(
items,
{
...weasel.defaultMenuOptions,
menuCssClass: cssSelectMenuElem.className + ' grist-floating-menu',
stretchToSelector : `.${cssSelectBtn.className}`,
trigger : [(triggerElem, ctl) => {
const isDisabled = () => triggerElem.classList.contains('disabled');
dom.onElem(triggerElem, 'click', () => isDisabled() || ctl.toggle());
dom.onKeyElem(triggerElem as HTMLElement, 'keydown', {
ArrowDown: () => isDisabled() || ctl.open(),
ArrowUp: () => isDisabled() || ctl.open()
});
}]
},
),
...args,
);
}
export function selectTitle(label: BindableValue<string>, iconName?: BindableValue<IconName>) {
return cssOptionRow(
iconName ? dom.domComputed(iconName, (name) => cssOptionRowIcon(name)) : null,
dom.text(label)
);
}
export function selectOption(
action: (item: HTMLElement) => void,
label: BindableValue<string>,
iconName?: BindableValue<IconName>) {
return menuItem(action, selectTitle(label, iconName));
}
export const menuSubHeader = styled('div', `
font-size: ${vars.xsmallFontSize};
text-transform: uppercase;
@@ -404,13 +469,13 @@ const cssOptionIcon = styled(icon, `
margin: -3px 8px 0 2px;
`);
const cssOptionRow = styled('span', `
export const cssOptionRow = styled('span', `
display: flex;
align-items: center;
width: 100%;
`);
const cssOptionRowIcon = styled(cssOptionIcon, `
export const cssOptionRowIcon = styled(cssOptionIcon, `
margin: 0 8px 0 0;
flex: none;