mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) New Columns Menu
Summary: A menu to be shown when new colum button is added. It's give access to various diffrent shortcuts, like adding new column, unhiding existing ones, fast adding lookup columns or trigger one (authoriship or timestamp). Design document can be found here: https://grist.quip.com/CTgxAQv9Ghjt/Add-Columns-more-easily To turn on this menu flag GRIST_NEW_COLUMN_MENU to 1 Test Plan: UI tests suite under nbrowser/GridViewNewColumnMenu.ts Reviewers: jarek, georgegevoian Reviewed By: georgegevoian Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4074
This commit is contained in:
@@ -34,7 +34,7 @@ const {onDblClickMatchElem} = require('app/client/lib/dblclick');
|
||||
// Grist UI Components
|
||||
const {dom: grainjsDom, Holder, Computed} = require('grainjs');
|
||||
const {closeRegisteredMenu, menu} = require('../ui2018/menus');
|
||||
const {calcFieldsCondition} = require('../ui/GridViewMenus');
|
||||
const {calcFieldsCondition, ColumnAddMenuOld} = require('../ui/GridViewMenus');
|
||||
const {ColumnAddMenu, ColumnContextMenu, MultiColumnMenu, freezeAction} = require('../ui/GridViewMenus');
|
||||
const {RowContextMenu} = require('../ui/RowContextMenu');
|
||||
|
||||
@@ -50,6 +50,8 @@ const {NEW_FILTER_JSON} = require('app/client/models/ColumnFilter');
|
||||
const {CombinedStyle} = require("app/client/models/Styles");
|
||||
const {buildRenameColumn} = require('app/client/ui/ColumnTitle');
|
||||
const {makeT} = require('app/client/lib/localization');
|
||||
const {FieldBuilder} = require("../widgets/FieldBuilder");
|
||||
const {GRIST_NEW_COLUMN_MENU} = require("../models/features");
|
||||
|
||||
const t = makeT('GridView');
|
||||
|
||||
@@ -836,7 +838,7 @@ GridView.prototype.deleteRows = async function(rowIds) {
|
||||
|
||||
GridView.prototype.addNewColumn = function() {
|
||||
this.insertColumn(this.viewSection.viewFields().peekLength)
|
||||
.then(() => this.scrollPaneRight());
|
||||
.then(() => this.scrollPaneRight());
|
||||
};
|
||||
|
||||
GridView.prototype.insertColumn = async function(index) {
|
||||
@@ -857,6 +859,33 @@ GridView.prototype.insertColumn = async function(index) {
|
||||
this.currentEditingColumnIndex(index);
|
||||
};
|
||||
|
||||
if(GRIST_NEW_COLUMN_MENU) {
|
||||
GridView.prototype.addNewColumnWithoutRenamePopup = async function() {
|
||||
const index = this.viewSection.viewFields().peekLength;
|
||||
const pos = tableUtil.fieldInsertPositions(this.viewSection.viewFields(), index)[0];
|
||||
var action = ['AddColumn', null, {"_position": pos}];
|
||||
await this.gristDoc.docData.bundleActions('Insert column', async () => {
|
||||
const colInfo = await this.tableModel.sendTableAction(action);
|
||||
if (!this.viewSection.isRaw.peek()) {
|
||||
const fieldInfo = {
|
||||
colRef: colInfo.colRef,
|
||||
parentPos: pos,
|
||||
parentId: this.viewSection.id.peek()
|
||||
};
|
||||
await this.gristDoc.docModel.viewFields.sendTableAction(['AddRecord', null, fieldInfo]);
|
||||
}
|
||||
});
|
||||
const builder = new FieldBuilder(this.gristDoc, this.viewSection.viewFields().peek()[this.viewSection.viewFields().peekLength - 1], this.cursor);
|
||||
return builder;
|
||||
};
|
||||
|
||||
GridView.prototype.addNewFormulaColumn = async function(formula, name) {
|
||||
const builder = await this.addNewColumnWithoutRenamePopup();
|
||||
await builder.gristDoc.convertToFormula(builder.field.colRef.peek(), formula);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
GridView.prototype.renameColumn = function(index) {
|
||||
this.currentEditingColumnIndex(index);
|
||||
};
|
||||
@@ -1105,6 +1134,28 @@ GridView.prototype.buildDom = function() {
|
||||
}
|
||||
};
|
||||
|
||||
const addColumnMenu = (gridView, viewSection)=> {
|
||||
if(GRIST_NEW_COLUMN_MENU())
|
||||
{
|
||||
return menu(ctl => [ColumnAddMenu(gridView, viewSection), testId('new-columns-menu')]);
|
||||
}
|
||||
else {
|
||||
return [
|
||||
dom.on('click', ev => {
|
||||
// If there are no hidden columns, clicking the plus just adds a new column.
|
||||
// If there are hidden columns, display a dropdown menu.
|
||||
if (viewSection.hiddenColumns().length === 0) {
|
||||
ev.stopImmediatePropagation(); // Don't open the menu defined below
|
||||
this.addNewColumn();
|
||||
}
|
||||
}),
|
||||
menu((ctl => ColumnAddMenuOld(gridView, viewSection)))
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return dom(
|
||||
'div.gridview_data_pane.flexvbox',
|
||||
// offset for frozen columns - how much move them to the left
|
||||
@@ -1298,15 +1349,7 @@ GridView.prototype.buildDom = function() {
|
||||
this._modField = dom('div.column_name.mod-add-column.field',
|
||||
'+',
|
||||
kd.style("width", PLUS_WIDTH + 'px'),
|
||||
dom.on('click', ev => {
|
||||
// If there are no hidden columns, clicking the plus just adds a new column.
|
||||
// If there are hidden columns, display a dropdown menu.
|
||||
if (this.viewSection.hiddenColumns().length === 0) {
|
||||
ev.stopImmediatePropagation(); // Don't open the menu defined below
|
||||
this.addNewColumn();
|
||||
}
|
||||
}),
|
||||
menu((ctl => ColumnAddMenu(this, this.viewSection)))
|
||||
addColumnMenu(this, this.viewSection),
|
||||
)
|
||||
))
|
||||
)
|
||||
|
||||
@@ -47,7 +47,6 @@ import {IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
||||
import {linkFromId, selectBy} from 'app/client/ui/selectBy';
|
||||
import {WebhookPage} from 'app/client/ui/WebhookPage';
|
||||
import {startWelcomeTour} from 'app/client/ui/WelcomeTour';
|
||||
import {AttachedCustomWidgets, IAttachedCustomWidget, IWidgetType} from 'app/common/widgetTypes';
|
||||
import {PlayerState, YouTubePlayer} from 'app/client/ui/YouTubePlayer';
|
||||
import {isNarrowScreen, mediaSmall, mediaXSmall, testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import {IconName} from 'app/client/ui2018/IconList';
|
||||
@@ -69,6 +68,7 @@ import {LocalPlugin} from "app/common/plugin";
|
||||
import {StringUnion} from 'app/common/StringUnion';
|
||||
import {TableData} from 'app/common/TableData';
|
||||
import {DocStateComparison} from 'app/common/UserAPI';
|
||||
import {AttachedCustomWidgets, IAttachedCustomWidget, IWidgetType} from 'app/common/widgetTypes';
|
||||
import {CursorPos} from 'app/plugin/GristAPI';
|
||||
import {
|
||||
bundleChanges,
|
||||
@@ -1090,12 +1090,15 @@ export class GristDoc extends DisposableWithEvents {
|
||||
}
|
||||
|
||||
// Convert column to data column with a trigger formula
|
||||
public async convertToTrigger(colRefs: number, formula: string): Promise<void> {
|
||||
public async convertToTrigger(
|
||||
colRefs: number,
|
||||
formula: string,
|
||||
recalcWhen: RecalcWhen = RecalcWhen.DEFAULT ): Promise<void> {
|
||||
return this.docModel.columns.sendTableAction(
|
||||
['UpdateRecord', colRefs, {
|
||||
isFormula: false,
|
||||
formula,
|
||||
recalcWhen: RecalcWhen.DEFAULT,
|
||||
recalcWhen: recalcWhen,
|
||||
recalcDeps: null,
|
||||
}]
|
||||
);
|
||||
|
||||
@@ -25,6 +25,10 @@ export function WHICH_FORMULA_ASSISTANT() {
|
||||
return getGristConfig().assistantService;
|
||||
}
|
||||
|
||||
export function GRIST_NEW_COLUMN_MENU(){
|
||||
return Boolean(getGristConfig().gristNewColumnMenu);
|
||||
}
|
||||
|
||||
export function PERMITTED_CUSTOM_WIDGETS(): Observable<string[]> {
|
||||
const G = getBrowserGlobals('document', 'window');
|
||||
if (!G.window.PERMITTED_CUSTOM_WIDGETS) {
|
||||
|
||||
@@ -1,30 +1,181 @@
|
||||
import {allCommands} from 'app/client/components/commands';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import { allCommands } from 'app/client/components/commands';
|
||||
import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec';
|
||||
import { testId, theme } from 'app/client/ui2018/cssVars';
|
||||
import { icon } from 'app/client/ui2018/icons';
|
||||
import { menuDivider, menuItem, menuItemCmd } from 'app/client/ui2018/menus';
|
||||
import { Sort } from 'app/common/SortSpec';
|
||||
import { dom, DomElementArg, styled } from 'grainjs';
|
||||
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||
import {testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {
|
||||
enhanceBySearch,
|
||||
menuDivider,
|
||||
menuItem,
|
||||
menuItemCmd,
|
||||
menuItemSubmenu,
|
||||
menuSubHeader,
|
||||
menuText
|
||||
} from 'app/client/ui2018/menus';
|
||||
import {Sort} from 'app/common/SortSpec';
|
||||
import {dom, DomElementArg, Observable, styled} from 'grainjs';
|
||||
import {RecalcWhen} from "../../common/gristTypes";
|
||||
import {GristDoc} from "../components/GristDoc";
|
||||
import {ColumnRec} from "../models/entities/ColumnRec";
|
||||
import {FieldBuilder} from "../widgets/FieldBuilder";
|
||||
import isEqual = require('lodash/isEqual');
|
||||
|
||||
const t = makeT('GridViewMenus');
|
||||
|
||||
//encapsulation over the view that menu will be generated for
|
||||
interface IView {
|
||||
addNewColumn: () => void;
|
||||
gristDoc: GristDoc;
|
||||
//adding new column to the view, and return a FieldBuilder that can be used to further modify the column
|
||||
addNewColumn: () => Promise<null>;
|
||||
addNewColumnWithoutRenamePopup: () => Promise<FieldBuilder>;
|
||||
showColumn: (colId: number, atIndex: number) => void;
|
||||
//Add new colum to the view as formula column, with given column name and
|
||||
//formula equation.
|
||||
// Return a FieldBuilder that can be used to further modify the column
|
||||
addNewFormulaColumn(formula: string, columnName: string): Promise<FieldBuilder>;
|
||||
}
|
||||
|
||||
interface IViewSection {
|
||||
viewFields: any;
|
||||
hiddenColumns: any;
|
||||
columns: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a menu to add a new column. Should be used only when there are hidden columns to display,
|
||||
* otherwise there is no need for this menu.
|
||||
*/
|
||||
export function ColumnAddMenu(gridView: IView, viewSection: IViewSection) {
|
||||
interface IColumnInfo{
|
||||
colId: string;
|
||||
label: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
|
||||
// Section for "Show hidden column" in a colum menu.
|
||||
// If there are no hidden columns - don't show the section.
|
||||
// If there is more that X - show submenu
|
||||
function MenuHideColumnSection(gridView: IView, viewSection: IViewSection){
|
||||
//function to generate the list with name of hidden columns and unhinging them on click
|
||||
const listOfHiddenColumns = viewSection.hiddenColumns().map((col: any, index: number): IColumnInfo => { return {
|
||||
colId:col.id(), label: col.label(), index: viewSection.columns().findIndex((c: any) => c.id() === col.id()),
|
||||
}; });
|
||||
|
||||
//Generating dom and hadling actions in menu section for hidden columns - allow to unhide it.
|
||||
const hiddenColumnMenu = () => {
|
||||
//if there is more than 5 hidden columns - show submenu
|
||||
if(listOfHiddenColumns.length > 5){
|
||||
return[
|
||||
menuItemSubmenu(
|
||||
(ctl: any)=>{
|
||||
// enhance this submenu by adding search bar on the top. enhanceBySearch is doing basically two things:
|
||||
// adding search bar, and expose searchCriteria observable to be used to generate list of items to be shown
|
||||
return enhanceBySearch((searchCriteria)=> {
|
||||
// put all hidden columns into observable
|
||||
const hiddenColumns: Array<IColumnInfo> = listOfHiddenColumns;
|
||||
const dynamicHiddenColumnsList = Observable.create<any[]>(null, hiddenColumns);
|
||||
// when search criteria changes - filter the list of hidden columns and update the observable
|
||||
searchCriteria.addListener((sc: string) => {
|
||||
return dynamicHiddenColumnsList.set(
|
||||
hiddenColumns.filter((c: IColumnInfo) => c.label.includes(sc)));
|
||||
});
|
||||
// generate a list of menu items from the observable
|
||||
return [
|
||||
// each hidden column is a menu item that will call showColumn on click
|
||||
// and place column at the end of the table
|
||||
dom.forEach(dynamicHiddenColumnsList,
|
||||
(col: any) => menuItem(
|
||||
()=>{ gridView.showColumn(col.colId, viewSection.columns().length); },
|
||||
col.label //column label as menu item text
|
||||
)
|
||||
)
|
||||
];
|
||||
});
|
||||
},
|
||||
{}, //options - we do not need any for this submenu
|
||||
t("Show hidden columns"), //text of the submenu
|
||||
{class: menuItem.className} // style of the submenu
|
||||
)
|
||||
];
|
||||
// in case there are less than five hidden columns - show them all in the main level of the menu
|
||||
} else {
|
||||
// generate a list of menu items from the list of hidden columns
|
||||
return listOfHiddenColumns.map((col: any) =>
|
||||
menuItem(
|
||||
()=> { gridView.showColumn(col.colId, viewSection.columns().length); },
|
||||
col.label, //column label as menu item text
|
||||
testId(`new-columns-menu-hidden-columns-${col.label.replace(' ', '-')}`)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return dom.maybe(() => viewSection.hiddenColumns().length > 0, ()=>[
|
||||
menuDivider(),
|
||||
menuSubHeader(t("Hidden Columns"), testId('new-columns-menu-hidden-columns')),
|
||||
hiddenColumnMenu()]
|
||||
);
|
||||
}
|
||||
|
||||
function MenuShortcuts(gridView: IView){
|
||||
return [
|
||||
menuDivider(),
|
||||
menuSubHeader(t("Shortcuts"), testId('new-columns-menu-shortcuts')),
|
||||
menuItemSubmenu((ctl: any)=>[
|
||||
menuItem(
|
||||
() => addNewColumnWithTimestamp(gridView, false), t("Apply to new records"),
|
||||
testId('new-columns-menu-shortcuts-timestamp-new')
|
||||
),
|
||||
menuItem(
|
||||
() => addNewColumnWithTimestamp(gridView, true), t("Apply on record changes"),
|
||||
testId('new-columns-menu-shortcuts-timestamp-change')
|
||||
),
|
||||
], {}, t("Timestamp"), testId('new-columns-menu-shortcuts-timestamp')),
|
||||
menuItemSubmenu((ctl: any)=>[
|
||||
menuItem(
|
||||
() => addNewColumnWithAuthor(gridView, false), t("Apply to new records"),
|
||||
testId('new-columns-menu-shortcuts-author-new')
|
||||
),
|
||||
menuItem(
|
||||
() => addNewColumnWithAuthor(gridView, true), t("Apply on record changes"),
|
||||
testId('new-columns-menu-shortcuts-author-change')
|
||||
),
|
||||
|
||||
], {}, t("Authorship"), testId('new-columns-menu-shortcuts-author')),
|
||||
]; }
|
||||
|
||||
function MenuLookups(viewSection: IViewSection, gridView: IView){
|
||||
return [
|
||||
menuDivider(),
|
||||
menuSubHeader(t("Lookups"), testId('new-columns-menu-lookups')),
|
||||
buildLookupsOptions(viewSection, gridView)
|
||||
];
|
||||
}
|
||||
|
||||
function buildLookupsOptions(viewSection: IViewSection, gridView: IView){
|
||||
const referenceCollection = viewSection.columns().filter((e: ColumnRec)=> e.pureType()=="Ref");
|
||||
|
||||
if(referenceCollection.length == 0){
|
||||
return menuText(()=>{}, t("no reference column"), testId('new-columns-menu-lookups-none'));
|
||||
}
|
||||
//TODO: Make search work - right now enhanceBySearch searchQuery parameter is not subscribed and menu items are
|
||||
// not updated when search query changes. Filter the columns names based on search query observable (like in
|
||||
// MenuHideColumnSection)
|
||||
return referenceCollection.map((ref: any) => menuItemSubmenu((ctl) => {
|
||||
return enhanceBySearch((searchQuery) => [
|
||||
...ref.refTable().columns().all().map((col: ColumnRec) =>
|
||||
menuItem(
|
||||
async () => {
|
||||
await gridView.addNewFormulaColumn(`$${ref.label()}.${col.label()}`,
|
||||
`${ref.label()}_${col.label()}`);
|
||||
}, col.label()
|
||||
)
|
||||
)
|
||||
]);
|
||||
}, {}, ref.label(), {class: menuItem.className}, testId(`new-columns-menu-lookups-${ref.label()}`)));
|
||||
}
|
||||
|
||||
// Old version of column menu
|
||||
// TODO: This is only valid as long as feature flag GRIST_NEW_COLUMN_MENU is existing in the system.
|
||||
// Once it is removed (so production is working only with the new column menu, this function should be removed as well.
|
||||
export function ColumnAddMenuOld(gridView: IView, viewSection: IViewSection) {
|
||||
return [
|
||||
menuItem(() => gridView.addNewColumn(), t("Add Column")),
|
||||
menuDivider(),
|
||||
@@ -35,6 +186,56 @@ export function ColumnAddMenu(gridView: IView, viewSection: IViewSection) {
|
||||
}, t("Show column {{- label}}", {label: col.label()})))
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a menu to add a new column.
|
||||
*/
|
||||
export function ColumnAddMenu(gridView: IView, viewSection: IViewSection) {
|
||||
return [
|
||||
menuItem(
|
||||
async () => { await gridView.addNewColumn(); },
|
||||
`+ ${t("Add Column")}`,
|
||||
testId('new-columns-menu-add-new')
|
||||
),
|
||||
MenuHideColumnSection(gridView, viewSection),
|
||||
MenuLookups(viewSection, gridView),
|
||||
MenuShortcuts(gridView),
|
||||
];
|
||||
}
|
||||
|
||||
//TODO: figure out how to change columns names;
|
||||
const addNewColumnWithTimestamp = async (gridView: IView, triggerOnUpdate: boolean) => {
|
||||
await gridView.gristDoc.docData.bundleActions('Add new column with timestamp', async () => {
|
||||
const column = await gridView.addNewColumnWithoutRenamePopup();
|
||||
if (!triggerOnUpdate) {
|
||||
await column.gristDoc.convertToTrigger(column.origColumn.id.peek(), 'NOW()', RecalcWhen.DEFAULT);
|
||||
await column.field.displayLabel.setAndSave(t('Created At'));
|
||||
await column.field.column.peek().type.setAndSave('DateTime');
|
||||
} else {
|
||||
await column.gristDoc.convertToTrigger(column.origColumn.id.peek(), 'NOW()', RecalcWhen.MANUAL_UPDATES);
|
||||
await column.field.displayLabel.setAndSave(t('Last Updated At'));
|
||||
await column.field.column.peek().type.setAndSave('DateTime');
|
||||
}
|
||||
}, {nestInActiveBundle: true});
|
||||
};
|
||||
|
||||
const addNewColumnWithAuthor = async (gridView: IView, triggerOnUpdate: boolean) => {
|
||||
await gridView.gristDoc.docData.bundleActions('Add new column with author', async () => {
|
||||
const column = await gridView.addNewColumnWithoutRenamePopup();
|
||||
if (!triggerOnUpdate) {
|
||||
await column.gristDoc.convertToTrigger(column.origColumn.id.peek(), 'user.Name', RecalcWhen.DEFAULT);
|
||||
await column.field.displayLabel.setAndSave(t('Created By'));
|
||||
await column.field.column.peek().type.setAndSave('Text');
|
||||
} else {
|
||||
await column.gristDoc.convertToTrigger(column.origColumn.id.peek(), 'user.Name', RecalcWhen.MANUAL_UPDATES);
|
||||
await column.field.displayLabel.setAndSave(t('Last Updated By'));
|
||||
await column.field.column.peek().type.setAndSave('Text');
|
||||
}
|
||||
}, {nestInActiveBundle: true});
|
||||
};
|
||||
|
||||
|
||||
|
||||
export interface IMultiColumnContextMenu {
|
||||
// For multiple selection, true/false means the value applies to all columns, 'mixed' means it's
|
||||
// true for some columns, but not all.
|
||||
|
||||
@@ -8,8 +8,10 @@ import { testId, theme, 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 { BindableValue, Computed, dom, DomElementArg, DomElementMethod, IDomArgs,
|
||||
MaybeObsArray, MutableObsArray, Observable, styled } from 'grainjs';
|
||||
import {
|
||||
BindableValue, Computed, dom, DomElementArg, DomElementMethod, IDomArgs,
|
||||
MaybeObsArray, MutableObsArray, Observable, styled
|
||||
} from 'grainjs';
|
||||
import * as weasel from 'popweasel';
|
||||
|
||||
const t = makeT('menus');
|
||||
@@ -47,6 +49,27 @@ export function menu(createFunc: weasel.MenuCreateFunc, options?: weasel.IMenuOp
|
||||
return weasel.menu(wrappedCreateFunc, {...defaults, ...options});
|
||||
}
|
||||
|
||||
const cssSearchField = styled('input',
|
||||
'border: none;'+
|
||||
'background-color: transparent;'+
|
||||
'padding: 8px 24px 4px 24px;'+
|
||||
'&:focus {outline: none;}'
|
||||
);
|
||||
export function enhanceBySearch( menuFunc: (searchCriteria: Observable<string>) => DomElementArg[]): DomElementArg[]
|
||||
{
|
||||
const searchCriteria = Observable.create(null, '');
|
||||
const searchInput = [
|
||||
menuItemStatic(
|
||||
cssSearchField(
|
||||
dom.on('input', (_ev, elem) => searchCriteria.set(elem.value)),
|
||||
{placeholder: '🔍\uFE0E\t' + t("Search columns")}
|
||||
)
|
||||
),
|
||||
menuDivider(),
|
||||
];
|
||||
return [...searchInput, ...menuFunc(searchCriteria)];
|
||||
}
|
||||
|
||||
// TODO Weasel doesn't allow other options for submenus, but probably should.
|
||||
export type ISubMenuOptions = weasel.ISubMenuOptions & weasel.IPopupOptions;
|
||||
|
||||
@@ -78,7 +101,7 @@ export const cssMenuElem = styled('div', `
|
||||
}
|
||||
`);
|
||||
|
||||
const menuItemStyle = `
|
||||
export const menuItemStyle = `
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
color: ${theme.menuItemFg};
|
||||
@@ -94,6 +117,8 @@ const menuItemStyle = `
|
||||
}
|
||||
`;
|
||||
|
||||
export const menuItemStatic = styled('div', menuItemStyle);
|
||||
|
||||
export const menuCssClass = cssMenuElem.className;
|
||||
|
||||
// Add grist-floating-menu class to support existing browser tests
|
||||
@@ -376,17 +401,23 @@ export function selectMenu(
|
||||
items: () => DomElementArg[],
|
||||
...args: IDomArgs<HTMLDivElement>
|
||||
) {
|
||||
const _menu = cssSelectMenuElem(testId('select-menu'));
|
||||
return cssSelectBtn(
|
||||
label,
|
||||
icon('Dropdown'),
|
||||
menu(
|
||||
listOfMenuItems(items),
|
||||
...args,
|
||||
);
|
||||
}
|
||||
|
||||
export function listOfMenuItems(items: () => DomElementArg[],) {
|
||||
const _menu = cssSelectMenuElem(testId('select-menu'));
|
||||
return menu(
|
||||
items,
|
||||
{
|
||||
...weasel.defaultMenuOptions,
|
||||
menuCssClass: _menu.className + ' grist-floating-menu',
|
||||
stretchToSelector : `.${cssSelectBtn.className}`,
|
||||
trigger : [(triggerElem, ctl) => {
|
||||
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', {
|
||||
@@ -395,8 +426,6 @@ export function selectMenu(
|
||||
});
|
||||
}]
|
||||
},
|
||||
),
|
||||
...args,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -434,10 +463,12 @@ export const menuText = styled('div', `
|
||||
cursor: default;
|
||||
`);
|
||||
|
||||
|
||||
export const menuItem = styled(weasel.menuItem, menuItemStyle);
|
||||
|
||||
export const menuItemLink = styled(weasel.menuItemLink, menuItemStyle);
|
||||
|
||||
|
||||
/**
|
||||
* A version of menuItem which runs the action on next tick, allowing the menu to close even when
|
||||
* the action causes the disabling of the element being clicked.
|
||||
|
||||
@@ -676,6 +676,9 @@ export interface GristLoadConfig {
|
||||
|
||||
permittedCustomWidgets?: IAttachedCustomWidget[];
|
||||
|
||||
// Feature flag for the new column menu.
|
||||
gristNewColumnMenu?: boolean;
|
||||
|
||||
// Used to determine which disclosure links should be provided to user of
|
||||
// formula assistance.
|
||||
assistantService?: 'OpenAI' | undefined;
|
||||
|
||||
@@ -79,6 +79,7 @@ export function makeGristConfig(options: MakeGristConfigOptons): GristLoadConfig
|
||||
featureFormulaAssistant: Boolean(process.env.OPENAI_API_KEY || process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT),
|
||||
assistantService: process.env.OPENAI_API_KEY ? 'OpenAI' : undefined,
|
||||
permittedCustomWidgets: getPermittedCustomWidgets(),
|
||||
gristNewColumnMenu: isAffirmative(process.env.GRIST_NEW_COLUMN_MENU),
|
||||
supportEmail: SUPPORT_EMAIL,
|
||||
userLocale: (req as RequestWithLogin | undefined)?.user?.options?.locale,
|
||||
telemetry: server?.getTelemetry().getTelemetryConfig(),
|
||||
|
||||
Reference in New Issue
Block a user