mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) Polish new Add Column menu
Summary: Fixes and features for the unreleased Add Column menu. Test Plan: Manual. Reviewers: jarek Reviewed By: jarek Subscribers: jarek Differential Revision: https://phab.getgrist.com/D4076
This commit is contained in:
		
							parent
							
								
									7f091cf057
								
							
						
					
					
						commit
						f1cf92aca1
					
				@ -30,18 +30,24 @@ const {reportWarning} = require('app/client/models/errors');
 | 
			
		||||
const {reportUndo} = require('app/client/components/modals');
 | 
			
		||||
 | 
			
		||||
const {onDblClickMatchElem} = require('app/client/lib/dblclick');
 | 
			
		||||
const {FocusLayer} = require('app/client/lib/FocusLayer');
 | 
			
		||||
 | 
			
		||||
// Grist UI Components
 | 
			
		||||
const {dom: grainjsDom, Holder, Computed} = require('grainjs');
 | 
			
		||||
const {closeRegisteredMenu, menu} = require('../ui2018/menus');
 | 
			
		||||
const {calcFieldsCondition, ColumnAddMenuOld} = require('../ui/GridViewMenus');
 | 
			
		||||
const {ColumnAddMenu, ColumnContextMenu, MultiColumnMenu, freezeAction} = require('../ui/GridViewMenus');
 | 
			
		||||
const {RowContextMenu} = require('../ui/RowContextMenu');
 | 
			
		||||
 | 
			
		||||
const {setPopupToCreateDom} = require('popweasel');
 | 
			
		||||
const {CellContextMenu} = require('app/client/ui/CellContextMenu');
 | 
			
		||||
const {testId, isNarrowScreen} = require('app/client/ui2018/cssVars');
 | 
			
		||||
const {contextMenu} = require('app/client/ui/contextMenu');
 | 
			
		||||
const {
 | 
			
		||||
  buildAddColumnMenu,
 | 
			
		||||
  buildColumnContextMenu,
 | 
			
		||||
  buildMultiColumnMenu,
 | 
			
		||||
  buildOldAddColumnMenu,
 | 
			
		||||
  calcFieldsCondition,
 | 
			
		||||
  freezeAction,
 | 
			
		||||
} = require('app/client/ui/GridViewMenus');
 | 
			
		||||
const {mouseDragMatchElem} = require('app/client/ui/mouseDrag');
 | 
			
		||||
const {menuToggle} = require('app/client/ui/MenuToggle');
 | 
			
		||||
const {descriptionInfoTooltip, showTooltip} = require('app/client/ui/tooltips');
 | 
			
		||||
@ -50,7 +56,6 @@ 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');
 | 
			
		||||
@ -209,6 +214,8 @@ function GridView(gristDoc, viewSectionModel, isPreview = false) {
 | 
			
		||||
  // Holds column index that is hovered, works only in full-edit formula mode.
 | 
			
		||||
  this.hoverColumn = ko.observable(-1);
 | 
			
		||||
 | 
			
		||||
  this._insertColumnIndex = ko.observable(null);
 | 
			
		||||
 | 
			
		||||
  // Checks if there is active formula editor for a column in this table.
 | 
			
		||||
  this.editingFormula = ko.pureComputed(() => {
 | 
			
		||||
    const isEditing = this.gristDoc.docModel.editingFormula();
 | 
			
		||||
@ -303,8 +310,22 @@ GridView.gridCommands = {
 | 
			
		||||
  // Re-define editField after fieldEditSave to make it take precedence for the Enter key.
 | 
			
		||||
  editField: function() { closeRegisteredMenu(); this.scrollToCursor(true); this.activateEditorAtCursor(); },
 | 
			
		||||
 | 
			
		||||
  insertFieldBefore: function() { this.insertColumn(this.cursor.fieldIndex()); },
 | 
			
		||||
  insertFieldAfter: function() { this.insertColumn(this.cursor.fieldIndex() + 1); },
 | 
			
		||||
  insertFieldBefore: function() {
 | 
			
		||||
    if (GRIST_NEW_COLUMN_MENU()) {
 | 
			
		||||
      this._openInsertColumnMenu(this.cursor.fieldIndex());
 | 
			
		||||
    } else {
 | 
			
		||||
      // FIXME: remove once New Column menu is enabled by default.
 | 
			
		||||
      this.insertColumn(null, {index: this.cursor.fieldIndex()});
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  insertFieldAfter: function() {
 | 
			
		||||
    if (GRIST_NEW_COLUMN_MENU()) {
 | 
			
		||||
      this._openInsertColumnMenu(this.cursor.fieldIndex() + 1);
 | 
			
		||||
    } else {
 | 
			
		||||
      // FIXME: remove once New Column menu is enabled by default.
 | 
			
		||||
      this.insertColumn(null, {index: this.cursor.fieldIndex() + 1});
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  renameField: function() { this.renameColumn(this.cursor.fieldIndex()); },
 | 
			
		||||
  hideFields: function() { this.hideFields(this.getSelection()); },
 | 
			
		||||
  deleteFields: function() {
 | 
			
		||||
@ -836,60 +857,26 @@ GridView.prototype.deleteRows = async function(rowIds) {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
GridView.prototype.addNewColumn = function() {
 | 
			
		||||
  this.insertColumn(this.viewSection.viewFields().peekLength)
 | 
			
		||||
    .then(() => this.scrollPaneRight());
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
GridView.prototype.insertColumn = async function(index) {
 | 
			
		||||
  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]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
GridView.prototype.insertColumn = async function(colId = null, options = {}) {
 | 
			
		||||
  const {
 | 
			
		||||
    colInfo = {},
 | 
			
		||||
    index = this.viewSection.viewFields().peekLength,
 | 
			
		||||
    skipPopup = false
 | 
			
		||||
  } = options;
 | 
			
		||||
  const newColInfo = await this.viewSection.insertColumn(colId, {colInfo, index});
 | 
			
		||||
  this.selectColumn(index);
 | 
			
		||||
  this.currentEditingColumnIndex(index);
 | 
			
		||||
  if (!skipPopup) { this.currentEditingColumnIndex(index); }
 | 
			
		||||
  return newColInfo;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
GridView.prototype.scrollPaneLeft = function() {
 | 
			
		||||
  this.scrollPane.scrollLeft = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
GridView.prototype.scrollPaneRight = function() {
 | 
			
		||||
  this.scrollPane.scrollLeft = this.scrollPane.scrollWidth;
 | 
			
		||||
};
 | 
			
		||||
@ -899,16 +886,12 @@ GridView.prototype.selectColumn = function(colIndex) {
 | 
			
		||||
  this.cellSelector.currentSelectType(selector.COL);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
GridView.prototype.showColumn = function(colId, index) {
 | 
			
		||||
  let fieldPos = tableUtil.fieldInsertPositions(this.viewSection.viewFields(), index, 1)[0];
 | 
			
		||||
  let colInfo = {
 | 
			
		||||
    parentId: this.viewSection.id(),
 | 
			
		||||
    colRef: colId,
 | 
			
		||||
    parentPos: fieldPos
 | 
			
		||||
  };
 | 
			
		||||
  return this.gristDoc.docModel.viewFields.sendTableAction(['AddRecord', null, colInfo])
 | 
			
		||||
  .then(() => this.selectColumn(index))
 | 
			
		||||
  .then(() => this.scrollPaneRight());
 | 
			
		||||
GridView.prototype.showColumn = async function(
 | 
			
		||||
  colRef,
 | 
			
		||||
  index = this.viewSection.viewFields().peekLength
 | 
			
		||||
) {
 | 
			
		||||
  await this.viewSection.showColumn(colRef, index);
 | 
			
		||||
  this.selectColumn(index);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// TODO: Replace alerts with custom notifications
 | 
			
		||||
@ -1134,28 +1117,6 @@ 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
 | 
			
		||||
@ -1343,13 +1304,15 @@ GridView.prototype.buildDom = function() {
 | 
			
		||||
                  testId('column-menu-trigger'),
 | 
			
		||||
                ),
 | 
			
		||||
                dom('div.selection'),
 | 
			
		||||
                // FIXME: remove once New Column menu is enabled by default.
 | 
			
		||||
                GRIST_NEW_COLUMN_MENU() ? this._buildInsertColumnMenu({field}) : null,
 | 
			
		||||
              );
 | 
			
		||||
            }),
 | 
			
		||||
            this.isPreview ? null : kd.maybe(() => !this.gristDoc.isReadonlyKo(), () => (
 | 
			
		||||
              this._modField = dom('div.column_name.mod-add-column.field',
 | 
			
		||||
                '+',
 | 
			
		||||
                kd.style("width", PLUS_WIDTH + 'px'),
 | 
			
		||||
                addColumnMenu(this, this.viewSection),
 | 
			
		||||
                this._buildInsertColumnMenu(),
 | 
			
		||||
              )
 | 
			
		||||
            ))
 | 
			
		||||
          )
 | 
			
		||||
@ -1504,7 +1467,10 @@ GridView.prototype.buildDom = function() {
 | 
			
		||||
        kd.foreach(v.viewFields(), function(field) {
 | 
			
		||||
          // Whether the cell has a cursor (possibly in an inactive view section).
 | 
			
		||||
          var isCellSelected = ko.computed(() =>
 | 
			
		||||
            isRowActive() && field._index() === self.cursor.fieldIndex());
 | 
			
		||||
            isRowActive() &&
 | 
			
		||||
            field._index() === self.cursor.fieldIndex() &&
 | 
			
		||||
            self._insertColumnIndex() === null
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          // Whether the cell is active: has the cursor in the active section.
 | 
			
		||||
          var isCellActive = ko.computed(() => isCellSelected() && v.hasFocus());
 | 
			
		||||
@ -1529,6 +1495,8 @@ GridView.prototype.buildDom = function() {
 | 
			
		||||
 | 
			
		||||
          return dom(
 | 
			
		||||
            'div.field',
 | 
			
		||||
            kd.toggleClass('field-insert-before', () =>
 | 
			
		||||
              self._insertColumnIndex() === field._index()),
 | 
			
		||||
            kd.style('--frozen-position', () => ko.unwrap(self.frozenPositions.at(field._index()))),
 | 
			
		||||
            kd.toggleClass("frozen", () => ko.unwrap(self.frozenMap.at(field._index()))),
 | 
			
		||||
            kd.toggleClass('scissors', isCopyActive),
 | 
			
		||||
@ -1541,8 +1509,9 @@ GridView.prototype.buildDom = function() {
 | 
			
		||||
            //TODO: Ensure that fields in a row resize when
 | 
			
		||||
            //a cell in that row becomes larger
 | 
			
		||||
            kd.style('borderRightWidth', v.borderWidthPx),
 | 
			
		||||
 | 
			
		||||
            kd.toggleClass('selected', isSelected),
 | 
			
		||||
            // Optional icon. Currently only use to show formula icon.
 | 
			
		||||
            dom('div.field-icon'),
 | 
			
		||||
            fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected),
 | 
			
		||||
            dom('div.selection'),
 | 
			
		||||
          );
 | 
			
		||||
@ -1881,9 +1850,9 @@ GridView.prototype.columnContextMenu = function(ctl, copySelection, field, filte
 | 
			
		||||
  const options = this._getColumnMenuOptions(copySelection);
 | 
			
		||||
 | 
			
		||||
  if (selectedColIds.length > 1 && selectedColIds.includes(field.column().colId())) {
 | 
			
		||||
    return MultiColumnMenu(options);
 | 
			
		||||
    return buildMultiColumnMenu(options);
 | 
			
		||||
  } else {
 | 
			
		||||
    return ColumnContextMenu({
 | 
			
		||||
    return buildColumnContextMenu({
 | 
			
		||||
      filterOpenFunc: () => filterTriggerCtl.open(),
 | 
			
		||||
      sortSpec: this.gristDoc.viewModel.activeSection.peek().activeSortSpec.peek(),
 | 
			
		||||
      colId: field.column.peek().id.peek(),
 | 
			
		||||
@ -2000,20 +1969,113 @@ GridView.prototype._scrollColumnIntoView = function(colIndex) {
 | 
			
		||||
  // If there are some frozen columns.
 | 
			
		||||
  if (this.numFrozen.peek() && colIndex < this.numFrozen.peek()) { return; }
 | 
			
		||||
 | 
			
		||||
  const offset = this.colRightOffsets.peek().getSumTo(colIndex);
 | 
			
		||||
  if (colIndex === 0) {
 | 
			
		||||
    this.scrollPaneLeft();
 | 
			
		||||
  } else if (colIndex === this.viewSection.viewFields().peekLength - 1) {
 | 
			
		||||
    this.scrollPaneRight();
 | 
			
		||||
  } else {
 | 
			
		||||
    const offset = this.colRightOffsets.peek().getSumTo(colIndex);
 | 
			
		||||
 | 
			
		||||
  const rowNumsWidth = this._cornerDom.clientWidth;
 | 
			
		||||
  const viewWidth = this.scrollPane.clientWidth - rowNumsWidth;
 | 
			
		||||
  const fieldWidth = this.colRightOffsets.peek().getValue(colIndex) + 1; // +1px border
 | 
			
		||||
    const rowNumsWidth = this._cornerDom.clientWidth;
 | 
			
		||||
    const viewWidth = this.scrollPane.clientWidth - rowNumsWidth;
 | 
			
		||||
    const fieldWidth = this.colRightOffsets.peek().getValue(colIndex) + 1; // +1px border
 | 
			
		||||
 | 
			
		||||
  // Left and right pixel edge of 'viewport', starting from edge of row nums.
 | 
			
		||||
  const frozenWidth = this.frozenWidth.peek();
 | 
			
		||||
  const leftEdge = this.scrollPane.scrollLeft + frozenWidth;
 | 
			
		||||
  const rightEdge = leftEdge + (viewWidth - frozenWidth);
 | 
			
		||||
    // Left and right pixel edge of 'viewport', starting from edge of row nums.
 | 
			
		||||
    const frozenWidth = this.frozenWidth.peek();
 | 
			
		||||
    const leftEdge = this.scrollPane.scrollLeft + frozenWidth;
 | 
			
		||||
    const rightEdge = leftEdge + (viewWidth - frozenWidth);
 | 
			
		||||
 | 
			
		||||
  // If cell doesn't fit onscreen, scroll to fit.
 | 
			
		||||
  const scrollShift = offset - gutil.clamp(offset, leftEdge, rightEdge - fieldWidth);
 | 
			
		||||
  this.scrollPane.scrollLeft = this.scrollPane.scrollLeft + scrollShift;
 | 
			
		||||
    // If cell doesn't fit onscreen, scroll to fit.
 | 
			
		||||
    const scrollShift = offset - gutil.clamp(offset, leftEdge, rightEdge - fieldWidth);
 | 
			
		||||
    this.scrollPane.scrollLeft = this.scrollPane.scrollLeft + scrollShift;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attaches the Add Column menu.
 | 
			
		||||
 *
 | 
			
		||||
 * The menu can be triggered in two ways, depending on the presence of a `field`
 | 
			
		||||
 * in `options`.
 | 
			
		||||
 *
 | 
			
		||||
 * If a field is present, the menu is triggered only when `_insertColumnIndex` is set
 | 
			
		||||
 * to the index of the field the menu is attached to.
 | 
			
		||||
 *
 | 
			
		||||
 * If a field is not present, the menu is triggered either when `_insertColumnIndex`
 | 
			
		||||
 * is set to `-1` or when the attached element is clicked. In practice, there will
 | 
			
		||||
 * only be one element attached this way: the "+" field, which appears at the end of
 | 
			
		||||
 * the GridView.
 | 
			
		||||
 */
 | 
			
		||||
GridView.prototype._buildInsertColumnMenu = function(options = {}) {
 | 
			
		||||
  if (GRIST_NEW_COLUMN_MENU()) {
 | 
			
		||||
    const {field} = options;
 | 
			
		||||
    const triggers = [];
 | 
			
		||||
    if (!field) { triggers.push('click'); }
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
      field ? kd.toggleClass('field-insert-before', () =>
 | 
			
		||||
        this._insertColumnIndex() === field._index()) : null,
 | 
			
		||||
      menu(
 | 
			
		||||
        ctl => {
 | 
			
		||||
          ctl.onDispose(() => this._insertColumnIndex(null));
 | 
			
		||||
 | 
			
		||||
          let index = this._insertColumnIndex.peek();
 | 
			
		||||
          if (index === null || index === -1) {
 | 
			
		||||
            index = undefined;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return [
 | 
			
		||||
            buildAddColumnMenu(this, index),
 | 
			
		||||
            elem => { FocusLayer.create(ctl, {defaultFocusElem: elem, pauseMousetrap: true}); },
 | 
			
		||||
            testId('new-columns-menu'),
 | 
			
		||||
          ];
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          modifiers: {
 | 
			
		||||
            offset: {
 | 
			
		||||
              offset: '8,8',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          selectOnOpen: true,
 | 
			
		||||
          trigger: [
 | 
			
		||||
            ...triggers,
 | 
			
		||||
            (_, ctl) => {
 | 
			
		||||
              ctl.autoDispose(this._insertColumnIndex.subscribe((index) => {
 | 
			
		||||
                if (field?._index() === index || (!field && index === -1)) {
 | 
			
		||||
                  ctl.open();
 | 
			
		||||
                } else if (!ctl.isDisposed()) {
 | 
			
		||||
                  ctl.close();
 | 
			
		||||
                }
 | 
			
		||||
              }));
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        }
 | 
			
		||||
      ),
 | 
			
		||||
    ];
 | 
			
		||||
  } else {
 | 
			
		||||
    // FIXME: remove once New Column menu is enabled by default.
 | 
			
		||||
    return [
 | 
			
		||||
      dom.on('click', async 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) {
 | 
			
		||||
          // Don't open the menu defined below.
 | 
			
		||||
          ev.stopImmediatePropagation();
 | 
			
		||||
          await this.insertColumn();
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      menu((() => buildOldAddColumnMenu(this, this.viewSection))),
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
GridView.prototype._openInsertColumnMenu = function(columnIndex) {
 | 
			
		||||
  if (columnIndex < this.viewSection.viewFields().peekLength) {
 | 
			
		||||
    this._scrollColumnIntoView(columnIndex);
 | 
			
		||||
    this._insertColumnIndex(columnIndex);
 | 
			
		||||
  } else {
 | 
			
		||||
    this.scrollPaneRight();
 | 
			
		||||
    this._insertColumnIndex(-1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildStyleOption(owner, computedRule, optionName) {
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ import {DocComm} from 'app/client/components/DocComm';
 | 
			
		||||
import * as DocConfigTab from 'app/client/components/DocConfigTab';
 | 
			
		||||
import {Drafts} from "app/client/components/Drafts";
 | 
			
		||||
import {EditorMonitor} from "app/client/components/EditorMonitor";
 | 
			
		||||
import * as GridView from 'app/client/components/GridView';
 | 
			
		||||
import GridView from 'app/client/components/GridView';
 | 
			
		||||
import {importFromFile, selectAndImport} from 'app/client/components/Importer';
 | 
			
		||||
import {RawDataPage, RawDataPopup} from 'app/client/components/RawDataPage';
 | 
			
		||||
import {ActionGroupWithCursorPos, UndoStack} from 'app/client/components/UndoStack';
 | 
			
		||||
@ -785,7 +785,7 @@ export class GristDoc extends DisposableWithEvents {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getTableModel(tableId: string): DataTableModel {
 | 
			
		||||
    return this.docModel.dataTables[tableId];
 | 
			
		||||
    return this.docModel.getTableModel(tableId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get a DataTableModel, possibly wrapped to include diff data if a comparison is
 | 
			
		||||
 | 
			
		||||
@ -61,6 +61,20 @@
 | 
			
		||||
  background-color: var(--field-background-color, unset);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* The vertical line indicating where a column will be inserted when the
 | 
			
		||||
 * Add Column menu is open. */
 | 
			
		||||
.field.field-insert-before::before {
 | 
			
		||||
  content: '';
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 0px;
 | 
			
		||||
  top: 0px;
 | 
			
		||||
  /* Overlap the top/bottom table borders so that the line appears uninterrupted. */
 | 
			
		||||
  bottom: -1px;
 | 
			
		||||
  z-index: var(--grist-insert-column-line-z-index);
 | 
			
		||||
  width: 3px;
 | 
			
		||||
  background-color: var(--grist-theme-widget-active-border, #16B378);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Similar order is for detail view, but there is no row rules */
 | 
			
		||||
.g_record_detail_value {
 | 
			
		||||
  background-color: var(--grist-diff-background-color,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										25
									
								
								app/client/declarations.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								app/client/declarations.d.ts
									
									
									
									
										vendored
									
									
								
							@ -81,6 +81,31 @@ declare module "app/client/components/BaseView" {
 | 
			
		||||
  export = BaseView;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module 'app/client/components/GridView' {
 | 
			
		||||
  import BaseView from 'app/client/components/BaseView';
 | 
			
		||||
  import {GristDoc} from 'app/client/components/GristDoc';
 | 
			
		||||
  import {ColInfo, NewColInfo} from 'app/client/models/entities/ViewSectionRec';
 | 
			
		||||
 | 
			
		||||
  interface InsertColOptions {
 | 
			
		||||
    colInfo?: ColInfo;
 | 
			
		||||
    index?: number;
 | 
			
		||||
    skipPopup?: boolean;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  namespace GridView {}
 | 
			
		||||
 | 
			
		||||
  class GridView extends BaseView {
 | 
			
		||||
    public static create(...args: any[]): any;
 | 
			
		||||
 | 
			
		||||
    public gristDoc: GristDoc;
 | 
			
		||||
 | 
			
		||||
    constructor(gristDoc: GristDoc, viewSectionModel: any, isPreview?: boolean);
 | 
			
		||||
    public insertColumn(colId?: string|null, options?: InsertColOptions): Promise<NewColInfo>;
 | 
			
		||||
    public showColumn(colRef: number, index?: number): Promise<void>;
 | 
			
		||||
  }
 | 
			
		||||
  export = GridView;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module "app/client/components/ViewConfigTab" {
 | 
			
		||||
  import {GristDoc} from 'app/client/components/GristDoc';
 | 
			
		||||
  import {Disposable} from 'app/client/lib/dispose';
 | 
			
		||||
 | 
			
		||||
@ -239,6 +239,10 @@ export class DocModel {
 | 
			
		||||
      && this.allTableIds.all().includes('GristDocTutorial'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getTableModel(tableId: string) {
 | 
			
		||||
    return this.dataTables[tableId];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _metaTableModel<TName extends keyof SchemaTypes, TRow extends IRowModel<TName>>(
 | 
			
		||||
    tableId: TName,
 | 
			
		||||
    rowConstructor: (this: TRow, docModel: DocModel) => void,
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import randomcolor from 'randomcolor';
 | 
			
		||||
// Represents a user-defined table.
 | 
			
		||||
export interface TableRec extends IRowModel<"_grist_Tables"> {
 | 
			
		||||
  columns: ko.Computed<KoArray<ColumnRec>>;
 | 
			
		||||
  visibleColumns: ko.Computed<ColumnRec[]>;
 | 
			
		||||
  validations: ko.Computed<KoArray<ValidationRec>>;
 | 
			
		||||
 | 
			
		||||
  primaryView: ko.Computed<ViewRec>;
 | 
			
		||||
@ -45,6 +46,8 @@ export interface TableRec extends IRowModel<"_grist_Tables"> {
 | 
			
		||||
 | 
			
		||||
export function createTableRec(this: TableRec, docModel: DocModel): void {
 | 
			
		||||
  this.columns = recordSet(this, docModel.columns, 'parentId', {sortBy: 'parentPos'});
 | 
			
		||||
  this.visibleColumns = this.autoDispose(ko.pureComputed(() =>
 | 
			
		||||
    this.columns().all().filter(c => !c.isHiddenCol())));
 | 
			
		||||
  this.validations = recordSet(this, docModel.validations, 'tableRef');
 | 
			
		||||
 | 
			
		||||
  this.primaryView = refRecord(docModel.views, this.primaryViewId);
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import BaseView from 'app/client/components/BaseView';
 | 
			
		||||
import {SequenceNEVER, SequenceNum} from 'app/client/components/Cursor';
 | 
			
		||||
import {EmptyFilterColValues, LinkingState} from 'app/client/components/LinkingState';
 | 
			
		||||
import {KoArray} from 'app/client/lib/koArray';
 | 
			
		||||
import {fieldInsertPositions} from 'app/client/lib/tableUtil';
 | 
			
		||||
import {ColumnToMapImpl} from 'app/client/models/ColumnToMap';
 | 
			
		||||
import {
 | 
			
		||||
  ColumnRec,
 | 
			
		||||
@ -23,14 +24,36 @@ import {getWidgetTypes} from "app/client/ui/widgetTypesMap";
 | 
			
		||||
import {FilterColValues} from "app/common/ActiveDocAPI";
 | 
			
		||||
import {AccessLevel, ICustomWidget} from 'app/common/CustomWidget';
 | 
			
		||||
import {UserAction} from 'app/common/DocActions';
 | 
			
		||||
import {RecalcWhen} from 'app/common/gristTypes';
 | 
			
		||||
import {arrayRepeat} from 'app/common/gutil';
 | 
			
		||||
import {Sort} from 'app/common/SortSpec';
 | 
			
		||||
import {ColumnsToMap, WidgetColumnMap} from 'app/plugin/CustomSectionAPI';
 | 
			
		||||
import {CursorPos, UIRowId} from 'app/plugin/GristAPI';
 | 
			
		||||
import {GristObjCode} from 'app/plugin/GristData';
 | 
			
		||||
import {Computed, Holder, Observable} from 'grainjs';
 | 
			
		||||
import * as ko from 'knockout';
 | 
			
		||||
import defaults = require('lodash/defaults');
 | 
			
		||||
 | 
			
		||||
export interface InsertColOptions {
 | 
			
		||||
  colInfo?: ColInfo;
 | 
			
		||||
  index?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ColInfo {
 | 
			
		||||
  label?: string;
 | 
			
		||||
  type?: string;
 | 
			
		||||
  isFormula?: boolean;
 | 
			
		||||
  formula?: string;
 | 
			
		||||
  recalcWhen?: RecalcWhen;
 | 
			
		||||
  recalcDeps?: [GristObjCode.List, ...number[]]|null;
 | 
			
		||||
  widgetOptions?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NewColInfo {
 | 
			
		||||
  colId: string;
 | 
			
		||||
  colRef: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Represents a section of user views, now also known as a "page widget" (e.g. a view may contain
 | 
			
		||||
// a grid section and a chart section).
 | 
			
		||||
export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleOwner {
 | 
			
		||||
@ -231,6 +254,10 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO
 | 
			
		||||
 | 
			
		||||
  // Saves custom definition (bundles change)
 | 
			
		||||
  saveCustomDef(): Promise<void>;
 | 
			
		||||
 | 
			
		||||
  insertColumn(colId?: string|null, options?: InsertColOptions): Promise<NewColInfo>;
 | 
			
		||||
 | 
			
		||||
  showColumn(colRef: number, index?: number): Promise<void>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type WidgetMappedColumn = number|number[]|null;
 | 
			
		||||
@ -304,7 +331,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
 | 
			
		||||
  this.linkedSections = recordSet(this, docModel.viewSections, 'linkSrcSectionRef');
 | 
			
		||||
 | 
			
		||||
  // All table columns associated with this view section, excluding any hidden helper columns.
 | 
			
		||||
  this.columns = this.autoDispose(ko.pureComputed(() => this.table().columns().all().filter(c => !c.isHiddenCol())));
 | 
			
		||||
  this.columns = this.autoDispose(ko.pureComputed(() => this.table().visibleColumns()));
 | 
			
		||||
  this.editingFormula = ko.pureComputed({
 | 
			
		||||
    read: () => docModel.editingFormula(),
 | 
			
		||||
    write: val => {
 | 
			
		||||
@ -766,4 +793,36 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
 | 
			
		||||
    const list = this.view().activeCollapsedSections();
 | 
			
		||||
    return list.includes(this.id());
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  this.insertColumn = async (colId: string|null = null, options: InsertColOptions = {}) => {
 | 
			
		||||
    const {colInfo = {}, index = this.viewFields().peekLength} = options;
 | 
			
		||||
    const parentPos = fieldInsertPositions(this.viewFields(), index)[0];
 | 
			
		||||
    const action = ['AddColumn', colId, {
 | 
			
		||||
      ...colInfo,
 | 
			
		||||
      '_position': parentPos,
 | 
			
		||||
    }];
 | 
			
		||||
    let newColInfo: NewColInfo;
 | 
			
		||||
    await docModel.docData.bundleActions('Insert column', async () => {
 | 
			
		||||
      newColInfo = await docModel.dataTables[this.tableId.peek()].sendTableAction(action);
 | 
			
		||||
      if (!this.isRaw.peek()) {
 | 
			
		||||
        const fieldInfo = {
 | 
			
		||||
          colRef: newColInfo.colRef,
 | 
			
		||||
          parentId: this.id.peek(),
 | 
			
		||||
          parentPos,
 | 
			
		||||
        };
 | 
			
		||||
        await docModel.viewFields.sendTableAction(['AddRecord', null, fieldInfo]);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return newColInfo!;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  this.showColumn = async (colRef: number, index = this.viewFields().peekLength) => {
 | 
			
		||||
    const parentPos = fieldInsertPositions(this.viewFields(), index, 1)[0];
 | 
			
		||||
    const colInfo = {
 | 
			
		||||
      colRef,
 | 
			
		||||
      parentId: this.id.peek(),
 | 
			
		||||
      parentPos,
 | 
			
		||||
    };
 | 
			
		||||
    await docModel.viewFields.sendTableAction(['AddRecord', null, colInfo]);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,240 +1,316 @@
 | 
			
		||||
import {allCommands} from 'app/client/components/commands';
 | 
			
		||||
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 {testId, theme} from 'app/client/ui2018/cssVars';
 | 
			
		||||
import {icon} from 'app/client/ui2018/icons';
 | 
			
		||||
import {
 | 
			
		||||
  enhanceBySearch,
 | 
			
		||||
  menuDivider,
 | 
			
		||||
  menuIcon,
 | 
			
		||||
  menuItem,
 | 
			
		||||
  menuItemCmd,
 | 
			
		||||
  menuItemSubmenu,
 | 
			
		||||
  menuSubHeader,
 | 
			
		||||
  menuText
 | 
			
		||||
  menuText,
 | 
			
		||||
  searchableMenu,
 | 
			
		||||
} from 'app/client/ui2018/menus';
 | 
			
		||||
import {Sort} from 'app/common/SortSpec';
 | 
			
		||||
import {dom, DomElementArg, Observable, styled} from 'grainjs';
 | 
			
		||||
import {dom, DomElementArg, 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 {
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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){
 | 
			
		||||
// FIXME: remove once New Column menu is enabled by default.
 | 
			
		||||
export function buildOldAddColumnMenu(gridView: GridView, viewSection: ViewSectionRec) {
 | 
			
		||||
  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")),
 | 
			
		||||
    menuItem(async () => { await gridView.insertColumn(); }, t("Add Column")),
 | 
			
		||||
    menuDivider(),
 | 
			
		||||
    ...viewSection.hiddenColumns().map((col: any) => menuItem(
 | 
			
		||||
      () => {
 | 
			
		||||
        gridView.showColumn(col.id(), viewSection.viewFields().peekLength);
 | 
			
		||||
        // .then(() => gridView.scrollPaneRight());
 | 
			
		||||
      async () => {
 | 
			
		||||
        await gridView.showColumn(col.id());
 | 
			
		||||
      }, t("Show column {{- label}}", {label: col.label()})))
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates a menu to add a new column.
 | 
			
		||||
 */
 | 
			
		||||
export function ColumnAddMenu(gridView: IView, viewSection: IViewSection) {
 | 
			
		||||
export function buildAddColumnMenu(gridView: GridView, index?: number) {
 | 
			
		||||
  return [
 | 
			
		||||
    menuItem(
 | 
			
		||||
      async () => { await gridView.addNewColumn(); },
 | 
			
		||||
      `+ ${t("Add Column")}`,
 | 
			
		||||
      testId('new-columns-menu-add-new')
 | 
			
		||||
      async () => { await gridView.insertColumn(null, {index}); },
 | 
			
		||||
      menuIcon('Plus'),
 | 
			
		||||
      t("Add Column"),
 | 
			
		||||
      testId('new-columns-menu-add-new'),
 | 
			
		||||
    ),
 | 
			
		||||
    MenuHideColumnSection(gridView, viewSection),
 | 
			
		||||
    MenuLookups(viewSection, gridView),
 | 
			
		||||
    MenuShortcuts(gridView),
 | 
			
		||||
    buildHiddenColumnsMenuItems(gridView, index),
 | 
			
		||||
    buildLookupsMenuItems(gridView, index),
 | 
			
		||||
    buildShortcutsMenuItems(gridView, index),
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//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});
 | 
			
		||||
};
 | 
			
		||||
function buildHiddenColumnsMenuItems(gridView: GridView, index?: number) {
 | 
			
		||||
  const {viewSection} = gridView;
 | 
			
		||||
  const hiddenColumns = viewSection.hiddenColumns();
 | 
			
		||||
  if (hiddenColumns.length === 0) { return null; }
 | 
			
		||||
 | 
			
		||||
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});
 | 
			
		||||
};
 | 
			
		||||
  return [
 | 
			
		||||
    menuDivider(),
 | 
			
		||||
    menuSubHeader(t('Hidden Columns'), testId('new-columns-menu-hidden-columns')),
 | 
			
		||||
    hiddenColumns.length > 5
 | 
			
		||||
      ? [
 | 
			
		||||
        menuItemSubmenu(
 | 
			
		||||
          () => {
 | 
			
		||||
            return searchableMenu(
 | 
			
		||||
              hiddenColumns.map((col) => ({
 | 
			
		||||
                cleanText: col.label().trim().toLowerCase(),
 | 
			
		||||
                label: col.label(),
 | 
			
		||||
                action: async () => { await gridView.showColumn(col.id(), index); },
 | 
			
		||||
              })),
 | 
			
		||||
              {searchInputPlaceholder: t('Search columns')}
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
          {allowNothingSelected: true},
 | 
			
		||||
          t('Show hidden columns'),
 | 
			
		||||
        ),
 | 
			
		||||
      ]
 | 
			
		||||
      : hiddenColumns.map((col: ColumnRec) =>
 | 
			
		||||
        menuItem(
 | 
			
		||||
          async () => {
 | 
			
		||||
            await gridView.showColumn(col.id(), index);
 | 
			
		||||
          },
 | 
			
		||||
          col.label(),
 | 
			
		||||
        )
 | 
			
		||||
      ),
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildShortcutsMenuItems(gridView: GridView, index?: number) {
 | 
			
		||||
  return [
 | 
			
		||||
    menuDivider(),
 | 
			
		||||
    menuSubHeader(t("Shortcuts"), testId('new-columns-menu-shortcuts')),
 | 
			
		||||
    buildTimestampMenuItems(gridView, index),
 | 
			
		||||
    buildAuthorshipMenuItems(gridView, index),
 | 
			
		||||
    buildDetectDuplicatesMenuItems(gridView, index),
 | 
			
		||||
    buildUUIDMenuItem(gridView, index),
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildTimestampMenuItems(gridView: GridView, index?: number) {
 | 
			
		||||
  return menuItemSubmenu(() => [
 | 
			
		||||
    menuItem(
 | 
			
		||||
      async () => {
 | 
			
		||||
        await gridView.insertColumn(t('Created At'), {
 | 
			
		||||
          colInfo: {
 | 
			
		||||
            label: t('Created At'),
 | 
			
		||||
            type: 'DateTime',
 | 
			
		||||
            isFormula: false,
 | 
			
		||||
            formula: 'NOW()',
 | 
			
		||||
            recalcWhen: RecalcWhen.DEFAULT,
 | 
			
		||||
            recalcDeps: null,
 | 
			
		||||
          },
 | 
			
		||||
          index,
 | 
			
		||||
          skipPopup: true,
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
      t("Apply to new records"),
 | 
			
		||||
      testId('new-columns-menu-shortcuts-timestamp-new'),
 | 
			
		||||
    ),
 | 
			
		||||
    menuItem(
 | 
			
		||||
      async () => {
 | 
			
		||||
        await gridView.insertColumn(t('Last Updated At'), {
 | 
			
		||||
          colInfo: {
 | 
			
		||||
            label: t('Last Updated At'),
 | 
			
		||||
            type: 'DateTime',
 | 
			
		||||
            isFormula: false,
 | 
			
		||||
            formula: 'NOW()',
 | 
			
		||||
            recalcWhen: RecalcWhen.MANUAL_UPDATES,
 | 
			
		||||
            recalcDeps: null,
 | 
			
		||||
          },
 | 
			
		||||
          index,
 | 
			
		||||
          skipPopup: true,
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
      t("Apply on record changes"),
 | 
			
		||||
      testId('new-columns-menu-shortcuts-timestamp-change'),
 | 
			
		||||
    ),
 | 
			
		||||
  ], {}, t("Timestamp"), testId('new-columns-menu-shortcuts-timestamp'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildAuthorshipMenuItems(gridView: GridView, index?: number) {
 | 
			
		||||
  return menuItemSubmenu(() => [
 | 
			
		||||
    menuItem(
 | 
			
		||||
      async () => {
 | 
			
		||||
        await gridView.insertColumn(t('Created By'), {
 | 
			
		||||
          colInfo: {
 | 
			
		||||
            label: t('Created By'),
 | 
			
		||||
            type: 'Text',
 | 
			
		||||
            isFormula: false,
 | 
			
		||||
            formula: 'user.Name',
 | 
			
		||||
            recalcWhen: RecalcWhen.DEFAULT,
 | 
			
		||||
            recalcDeps: null,
 | 
			
		||||
          },
 | 
			
		||||
          index,
 | 
			
		||||
          skipPopup: true,
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
      t("Apply to new records"),
 | 
			
		||||
      testId('new-columns-menu-shortcuts-author-new')
 | 
			
		||||
    ),
 | 
			
		||||
    menuItem(
 | 
			
		||||
      async () => {
 | 
			
		||||
        await gridView.insertColumn(t('Last Updated By'), {
 | 
			
		||||
          colInfo: {
 | 
			
		||||
            label: t('Last Updated By'),
 | 
			
		||||
            type: 'Text',
 | 
			
		||||
            isFormula: false,
 | 
			
		||||
            formula: 'user.Name',
 | 
			
		||||
            recalcWhen: RecalcWhen.MANUAL_UPDATES,
 | 
			
		||||
            recalcDeps: null,
 | 
			
		||||
          },
 | 
			
		||||
          index,
 | 
			
		||||
          skipPopup: true,
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
      t("Apply on record changes"),
 | 
			
		||||
      testId('new-columns-menu-shortcuts-author-change')
 | 
			
		||||
    ),
 | 
			
		||||
  ], {}, t("Authorship"), testId('new-columns-menu-shortcuts-author'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildDetectDuplicatesMenuItems(gridView: GridView, index?: number) {
 | 
			
		||||
  const {viewSection} = gridView;
 | 
			
		||||
  return menuItemSubmenu(
 | 
			
		||||
    () => searchableMenu(
 | 
			
		||||
      viewSection.columns().map((col) => ({
 | 
			
		||||
        cleanText: col.label().trim().toLowerCase(),
 | 
			
		||||
        label: col.label(),
 | 
			
		||||
        action: async () => {
 | 
			
		||||
          await gridView.gristDoc.docData.bundleActions(t('Adding duplicates column'), async () => {
 | 
			
		||||
            const newColInfo = await gridView.insertColumn(
 | 
			
		||||
              t('Duplicate in {{- label}}', {label: col.label()}),
 | 
			
		||||
              {
 | 
			
		||||
                colInfo: {
 | 
			
		||||
                  label: t('Duplicate in {{- label}}', {label: col.label()}),
 | 
			
		||||
                  type: 'Bool',
 | 
			
		||||
                  isFormula: true,
 | 
			
		||||
                  formula: `True if len(${col.table().tableId()}.lookupRecords(` +
 | 
			
		||||
                    `${col.colId()}=$${col.colId()})) > 1 else False`,
 | 
			
		||||
                  recalcWhen: RecalcWhen.DEFAULT,
 | 
			
		||||
                  recalcDeps: null,
 | 
			
		||||
                  widgetOptions: JSON.stringify({
 | 
			
		||||
                    rulesOptions: [{
 | 
			
		||||
                      fillColor: '#ffc23d',
 | 
			
		||||
                      textColor: '#262633',
 | 
			
		||||
                    }],
 | 
			
		||||
                  }),
 | 
			
		||||
                },
 | 
			
		||||
                index,
 | 
			
		||||
                skipPopup: true,
 | 
			
		||||
              }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // TODO: do the steps below as part of the AddColumn action.
 | 
			
		||||
            const newField = viewSection.viewFields().all()
 | 
			
		||||
              .find(field => field.colId() === newColInfo.colId);
 | 
			
		||||
            if (!newField) {
 | 
			
		||||
              throw new Error(`Unable to find field for column ${newColInfo.colId}`);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await newField.addEmptyRule();
 | 
			
		||||
            const newRule = newField.rulesCols()[0];
 | 
			
		||||
            if (!newRule) {
 | 
			
		||||
              throw new Error(`Unable to find conditional rule for field ${newField.label()}`);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await newRule.formula.setAndSave(`$${newColInfo.colId}`);
 | 
			
		||||
          }, {nestInActiveBundle: true});
 | 
			
		||||
        },
 | 
			
		||||
      })),
 | 
			
		||||
      {searchInputPlaceholder: t('Search columns')}
 | 
			
		||||
    ),
 | 
			
		||||
    {allowNothingSelected: true},
 | 
			
		||||
    t('Detect Duplicates in...'),
 | 
			
		||||
    testId('new-columns-menu-shortcuts-duplicates'),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildUUIDMenuItem(gridView: GridView, index?: number) {
 | 
			
		||||
  return menuItem(
 | 
			
		||||
    async () => {
 | 
			
		||||
      await gridView.gristDoc.docData.bundleActions(t('Adding UUID column'), async () => {
 | 
			
		||||
        // First create a formula column so that UUIDs are computed for existing cells.
 | 
			
		||||
        const {colRef} = await gridView.insertColumn(t('UUID'), {
 | 
			
		||||
          colInfo: {
 | 
			
		||||
            label: t('UUID'),
 | 
			
		||||
            type: 'Text',
 | 
			
		||||
            isFormula: true,
 | 
			
		||||
            formula: 'UUID()',
 | 
			
		||||
            recalcWhen: RecalcWhen.DEFAULT,
 | 
			
		||||
            recalcDeps: null,
 | 
			
		||||
          },
 | 
			
		||||
          index,
 | 
			
		||||
          skipPopup: true,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Then convert it to a trigger formula, so that UUIDs aren't re-computed.
 | 
			
		||||
        //
 | 
			
		||||
        // TODO: remove this step and do it as part of the AddColumn action.
 | 
			
		||||
        await gridView.gristDoc.convertToTrigger(colRef, 'UUID()');
 | 
			
		||||
      }, {nestInActiveBundle: true});
 | 
			
		||||
    },
 | 
			
		||||
    t('UUID'),
 | 
			
		||||
    testId('new-columns-menu-shortcuts-uuid'),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildLookupsMenuItems(gridView: GridView, index?: number) {
 | 
			
		||||
  const {viewSection} = gridView;
 | 
			
		||||
  const columns = viewSection.columns();
 | 
			
		||||
  const references = columns.filter((c) => c.pureType() === 'Ref');
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
    menuDivider(),
 | 
			
		||||
    menuSubHeader(
 | 
			
		||||
      t('Lookups'),
 | 
			
		||||
      testId('new-columns-menu-lookups'),
 | 
			
		||||
    ),
 | 
			
		||||
    references.length === 0
 | 
			
		||||
      ? [
 | 
			
		||||
        menuText(
 | 
			
		||||
          t('No reference columns.'),
 | 
			
		||||
          testId('new-columns-menu-lookups-none'),
 | 
			
		||||
        ),
 | 
			
		||||
      ]
 | 
			
		||||
      : references.map((ref) => menuItemSubmenu(
 | 
			
		||||
        () => {
 | 
			
		||||
          return searchableMenu(
 | 
			
		||||
            ref.refTable()?.visibleColumns().map((col) => ({
 | 
			
		||||
              cleanText: col.label().trim().toLowerCase(),
 | 
			
		||||
              label: col.label(),
 | 
			
		||||
              action: async () => {
 | 
			
		||||
                await gridView.insertColumn(t(`${ref.label()}_${col.label()}`), {
 | 
			
		||||
                  colInfo: {
 | 
			
		||||
                    label: `${ref.label()}_${col.label()}`,
 | 
			
		||||
                    isFormula: true,
 | 
			
		||||
                    formula: `$${ref.colId()}.${col.colId()}`,
 | 
			
		||||
                    recalcWhen: RecalcWhen.DEFAULT,
 | 
			
		||||
                    recalcDeps: null,
 | 
			
		||||
                  },
 | 
			
		||||
                  index,
 | 
			
		||||
                  skipPopup: true,
 | 
			
		||||
                });
 | 
			
		||||
              },
 | 
			
		||||
            })) ?? [],
 | 
			
		||||
            {searchInputPlaceholder: t('Search columns')}
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
        {allowNothingSelected: true},
 | 
			
		||||
        ref.label(),
 | 
			
		||||
        testId(`new-columns-menu-lookups-${ref.label()}`),
 | 
			
		||||
      )),
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IMultiColumnContextMenu {
 | 
			
		||||
  // For multiple selection, true/false means the value applies to all columns, 'mixed' means it's
 | 
			
		||||
@ -261,7 +337,7 @@ export function calcFieldsCondition(fields: ViewFieldRec[], condition: (f: ViewF
 | 
			
		||||
  return fields.every(condition) ? true : (fields.some(condition) ? "mixed" : false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ColumnContextMenu(options: IColumnContextMenu) {
 | 
			
		||||
export function buildColumnContextMenu(options: IColumnContextMenu) {
 | 
			
		||||
  const { disableModify, filterOpenFunc, colId, sortSpec, isReadonly } = options;
 | 
			
		||||
 | 
			
		||||
  const disableForReadonlyColumn = dom.cls('disabled', Boolean(disableModify) || isReadonly);
 | 
			
		||||
@ -318,7 +394,7 @@ export function ColumnContextMenu(options: IColumnContextMenu) {
 | 
			
		||||
    menuItemCmd(allCommands.renameField, t("Rename column"), disableForReadonlyColumn),
 | 
			
		||||
    freezeMenuItemCmd(options),
 | 
			
		||||
    menuDivider(),
 | 
			
		||||
    MultiColumnMenu((options.disableFrozenMenu = true, options)),
 | 
			
		||||
    buildMultiColumnMenu((options.disableFrozenMenu = true, options)),
 | 
			
		||||
    testId('column-menu'),
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
@ -331,7 +407,7 @@ export function ColumnContextMenu(options: IColumnContextMenu) {
 | 
			
		||||
 * We offer both options if data columns are selected. If only formulas, only the second option
 | 
			
		||||
 * makes sense.
 | 
			
		||||
 */
 | 
			
		||||
export function MultiColumnMenu(options: IMultiColumnContextMenu) {
 | 
			
		||||
export function buildMultiColumnMenu(options: IMultiColumnContextMenu) {
 | 
			
		||||
  const disableForReadonlyColumn = dom.cls('disabled', Boolean(options.disableModify) || options.isReadonly);
 | 
			
		||||
  const disableForReadonlyView = dom.cls('disabled', options.isReadonly);
 | 
			
		||||
  const num: number = options.numColumns;
 | 
			
		||||
 | 
			
		||||
@ -136,6 +136,7 @@ export const vars = {
 | 
			
		||||
  toastBg: new CustomProp('toast-bg', '#040404'),
 | 
			
		||||
 | 
			
		||||
  /* Z indexes */
 | 
			
		||||
  insertColumnLineZIndex: new CustomProp('insert-column-line-z-index', '20'),
 | 
			
		||||
  menuZIndex: new CustomProp('menu-z-index', '999'),
 | 
			
		||||
  modalZIndex: new CustomProp('modal-z-index', '999'),
 | 
			
		||||
  onboardingBackdropZIndex: new CustomProp('onboarding-backdrop-z-index', '999'),
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ import {
 | 
			
		||||
  BindableValue, Computed, dom, DomElementArg, DomElementMethod, IDomArgs,
 | 
			
		||||
  MaybeObsArray, MutableObsArray, Observable, styled
 | 
			
		||||
} from 'grainjs';
 | 
			
		||||
import debounce from 'lodash/debounce';
 | 
			
		||||
import * as weasel from 'popweasel';
 | 
			
		||||
 | 
			
		||||
const t = makeT('menus');
 | 
			
		||||
@ -49,36 +50,70 @@ 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")}
 | 
			
		||||
        )
 | 
			
		||||
export interface SearchableMenuOptions {
 | 
			
		||||
  searchInputPlaceholder?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SearchableMenuItem {
 | 
			
		||||
  cleanText: string;
 | 
			
		||||
  label: string;
 | 
			
		||||
  action: (item: HTMLElement) => void;
 | 
			
		||||
  args?: DomElementArg[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function searchableMenu(
 | 
			
		||||
  menuItems: MaybeObsArray<SearchableMenuItem>,
 | 
			
		||||
  options: SearchableMenuOptions = {}
 | 
			
		||||
): DomElementArg[] {
 | 
			
		||||
  const {searchInputPlaceholder} = options;
 | 
			
		||||
 | 
			
		||||
  const searchValue = Observable.create(null, '');
 | 
			
		||||
  const setSearchValue = debounce((value) => { searchValue.set(value); }, 100);
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
    menuItemStatic(
 | 
			
		||||
      cssMenuSearch(
 | 
			
		||||
        cssMenuSearchIcon('Search'),
 | 
			
		||||
        cssMenuSearchInput(
 | 
			
		||||
          dom.autoDispose(searchValue),
 | 
			
		||||
          dom.on('input', (_ev, elem) => { setSearchValue(elem.value); }),
 | 
			
		||||
          {placeholder: searchInputPlaceholder},
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      menuDivider(),
 | 
			
		||||
    ];
 | 
			
		||||
    return [...searchInput, ...menuFunc(searchCriteria)];
 | 
			
		||||
    ),
 | 
			
		||||
    menuDivider(),
 | 
			
		||||
    dom.domComputed(searchValue, (value) => {
 | 
			
		||||
      const cleanSearchValue = value.trim().toLowerCase();
 | 
			
		||||
      return dom.forEach(menuItems, (item) => {
 | 
			
		||||
        if (!item.cleanText.includes(cleanSearchValue)) { return null; }
 | 
			
		||||
 | 
			
		||||
        return menuItem(item.action, item.label, ...(item.args ?? []));
 | 
			
		||||
      });
 | 
			
		||||
    }),
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO Weasel doesn't allow other options for submenus, but probably should.
 | 
			
		||||
export type ISubMenuOptions = weasel.ISubMenuOptions & weasel.IPopupOptions;
 | 
			
		||||
export type ISubMenuOptions =
 | 
			
		||||
  weasel.ISubMenuOptions &
 | 
			
		||||
  weasel.IPopupOptions &
 | 
			
		||||
  {allowNothingSelected?: boolean};
 | 
			
		||||
 | 
			
		||||
export function menuItemSubmenu(
 | 
			
		||||
  submenu: weasel.MenuCreateFunc,
 | 
			
		||||
  options: ISubMenuOptions,
 | 
			
		||||
  ...args: DomElementArg[]
 | 
			
		||||
): Element {
 | 
			
		||||
  return weasel.menuItemSubmenu(submenu, {...defaults, ...options}, ...args);
 | 
			
		||||
  return weasel.menuItemSubmenu(
 | 
			
		||||
    submenu,
 | 
			
		||||
    {
 | 
			
		||||
      ...defaults,
 | 
			
		||||
      expandIcon: () => icon('Expand'),
 | 
			
		||||
      ...options,
 | 
			
		||||
    },
 | 
			
		||||
    dom.cls(cssMenuItemSubmenu.className),
 | 
			
		||||
    ...args
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const cssMenuElem = styled('div', `
 | 
			
		||||
@ -449,7 +484,7 @@ export const menuSubHeader = styled('div', `
 | 
			
		||||
  font-size: ${vars.xsmallFontSize};
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  font-weight: ${vars.bigControlTextWeight};
 | 
			
		||||
  padding: 8px 24px 16px 24px;
 | 
			
		||||
  padding: 8px 24px 8px 24px;
 | 
			
		||||
  cursor: default;
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
@ -669,3 +704,43 @@ const cssCheckboxText = styled(cssLabelText, `
 | 
			
		||||
const cssUpgradeTextButton = styled(textButton, `
 | 
			
		||||
  font-size: ${vars.smallFontSize};
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
const cssMenuItemSubmenu = styled('div', `
 | 
			
		||||
  color: ${theme.menuItemFg};
 | 
			
		||||
  --icon-color: ${theme.menuItemFg};
 | 
			
		||||
  .${weasel.cssMenuItem.className}-sel {
 | 
			
		||||
    color: ${theme.menuItemSelectedFg};
 | 
			
		||||
    --icon-color: ${theme.menuItemSelectedFg};
 | 
			
		||||
  }
 | 
			
		||||
  &.disabled {
 | 
			
		||||
    cursor: default;
 | 
			
		||||
    color: ${theme.menuItemDisabledFg};
 | 
			
		||||
    --icon-color: ${theme.menuItemDisabledFg};
 | 
			
		||||
  }
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
const cssMenuSearch = styled('div', `
 | 
			
		||||
  display: flex;
 | 
			
		||||
  column-gap: 8px;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  padding: 8px 16px;
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
const cssMenuSearchIcon = styled(icon, `
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
  --icon-color: ${theme.menuItemIconFg};
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
const cssMenuSearchInput = styled('input', `
 | 
			
		||||
  color: ${theme.inputFg};
 | 
			
		||||
  background-color: ${theme.inputBg};
 | 
			
		||||
  flex-grow: 1;
 | 
			
		||||
  font-size: ${vars.mediumFontSize};
 | 
			
		||||
  padding: 0px;
 | 
			
		||||
  border: none;
 | 
			
		||||
  outline: none;
 | 
			
		||||
 | 
			
		||||
  &::placeholder {
 | 
			
		||||
    color: ${theme.inputPlaceholderFg};
 | 
			
		||||
  }
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
@ -189,7 +189,7 @@ const cssAttachmentWidget = styled('div', `
 | 
			
		||||
const cssAttachmentIcon = styled('div.glyphicon.glyphicon-paperclip', `
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 2px;
 | 
			
		||||
  left: 2px;
 | 
			
		||||
  left: 5px;
 | 
			
		||||
  padding: 2px;
 | 
			
		||||
  background-color: ${theme.attachmentsCellIconBg};
 | 
			
		||||
  color: ${theme.attachmentsCellIconFg};
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,9 @@
 | 
			
		||||
    color: #D0D0D0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .formula_field::before, .formula_field_edit::before, .formula_field_sidepane::before {
 | 
			
		||||
  .formula_field .field-icon,
 | 
			
		||||
  .formula_field_edit::before,
 | 
			
		||||
  .formula_field_sidepane::before {
 | 
			
		||||
    /* based on standard icon styles */
 | 
			
		||||
    content: "";
 | 
			
		||||
    position: absolute;
 | 
			
		||||
@ -28,13 +30,13 @@
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .formula_field::before, .formula_field_edit::before {
 | 
			
		||||
  .formula_field .field-icon, .formula_field_edit::before {
 | 
			
		||||
    background-color: #D0D0D0;
 | 
			
		||||
  }
 | 
			
		||||
  .formula_field_edit:not(.readonly)::before {
 | 
			
		||||
    background-color: var(--grist-color-cursor);
 | 
			
		||||
  }
 | 
			
		||||
  .formula_field.invalid::before {
 | 
			
		||||
  .formula_field.invalid .field-icon {
 | 
			
		||||
    background-color: white;
 | 
			
		||||
    color: #ffb6c1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -172,7 +172,7 @@
 | 
			
		||||
    "piscina": "3.2.0",
 | 
			
		||||
    "plotly.js-basic-dist": "2.13.2",
 | 
			
		||||
    "popper-max-size-modifier": "0.2.0",
 | 
			
		||||
    "popweasel": "0.1.18",
 | 
			
		||||
    "popweasel": "0.1.20",
 | 
			
		||||
    "qrcode": "1.5.0",
 | 
			
		||||
    "randomcolor": "0.5.3",
 | 
			
		||||
    "redis": "3.1.1",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								yarn.lock
									
									
									
									
									
								
							@ -4221,7 +4221,7 @@ grain-rpc@0.1.7:
 | 
			
		||||
    events "^1.1.1"
 | 
			
		||||
    ts-interface-checker "^1.0.0"
 | 
			
		||||
 | 
			
		||||
grainjs@1.0.2, grainjs@^1.0.1:
 | 
			
		||||
grainjs@1.0.2:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.npmjs.org/grainjs/-/grainjs-1.0.2.tgz"
 | 
			
		||||
  integrity sha512-wrj8TqpgxTGOKHpTlMBxMeX2uS3lTvXj4ROLKC+EZNM7J6RHQLGjMzMqWtiryBnMhGIBlbCicMNFppCrK1zv9w==
 | 
			
		||||
@ -6455,12 +6455,11 @@ popper.js@1.15.0:
 | 
			
		||||
  resolved "https://registry.npmjs.org/popper.js/-/popper.js-1.15.0.tgz"
 | 
			
		||||
  integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
 | 
			
		||||
 | 
			
		||||
popweasel@0.1.18:
 | 
			
		||||
  version "0.1.18"
 | 
			
		||||
  resolved "https://registry.npmjs.org/popweasel/-/popweasel-0.1.18.tgz"
 | 
			
		||||
  integrity sha512-F7+QRcnkj963ahDGURcZpucONfxhFWtlXLABawzaW7J/iOfcKMFRyjqluCebOLnBROPPBrih4g2qbq7KdQ0WMw==
 | 
			
		||||
popweasel@0.1.20:
 | 
			
		||||
  version "0.1.20"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/popweasel/-/popweasel-0.1.20.tgz#b69af57b08288dce398c2105cb1e8a9f4e0e324c"
 | 
			
		||||
  integrity sha512-iG51KFrHL49YuWTeI2yGby8BdNewdtxiKRv6y+Pyh1CkRKenLFu5CPMaKDRLbfiQJeZ/t67WW0e9ggWTt09ClA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    grainjs "^1.0.1"
 | 
			
		||||
    lodash "^4.17.15"
 | 
			
		||||
    popper.js "1.15.0"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user