mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +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 {reportUndo} = require('app/client/components/modals');
|
||||||
|
|
||||||
const {onDblClickMatchElem} = require('app/client/lib/dblclick');
|
const {onDblClickMatchElem} = require('app/client/lib/dblclick');
|
||||||
|
const {FocusLayer} = require('app/client/lib/FocusLayer');
|
||||||
|
|
||||||
// Grist UI Components
|
// Grist UI Components
|
||||||
const {dom: grainjsDom, Holder, Computed} = require('grainjs');
|
const {dom: grainjsDom, Holder, Computed} = require('grainjs');
|
||||||
const {closeRegisteredMenu, menu} = require('../ui2018/menus');
|
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 {RowContextMenu} = require('../ui/RowContextMenu');
|
||||||
|
|
||||||
const {setPopupToCreateDom} = require('popweasel');
|
const {setPopupToCreateDom} = require('popweasel');
|
||||||
const {CellContextMenu} = require('app/client/ui/CellContextMenu');
|
const {CellContextMenu} = require('app/client/ui/CellContextMenu');
|
||||||
const {testId, isNarrowScreen} = require('app/client/ui2018/cssVars');
|
const {testId, isNarrowScreen} = require('app/client/ui2018/cssVars');
|
||||||
const {contextMenu} = require('app/client/ui/contextMenu');
|
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 {mouseDragMatchElem} = require('app/client/ui/mouseDrag');
|
||||||
const {menuToggle} = require('app/client/ui/MenuToggle');
|
const {menuToggle} = require('app/client/ui/MenuToggle');
|
||||||
const {descriptionInfoTooltip, showTooltip} = require('app/client/ui/tooltips');
|
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 {CombinedStyle} = require("app/client/models/Styles");
|
||||||
const {buildRenameColumn} = require('app/client/ui/ColumnTitle');
|
const {buildRenameColumn} = require('app/client/ui/ColumnTitle');
|
||||||
const {makeT} = require('app/client/lib/localization');
|
const {makeT} = require('app/client/lib/localization');
|
||||||
const {FieldBuilder} = require("../widgets/FieldBuilder");
|
|
||||||
const {GRIST_NEW_COLUMN_MENU} = require("../models/features");
|
const {GRIST_NEW_COLUMN_MENU} = require("../models/features");
|
||||||
|
|
||||||
const t = makeT('GridView');
|
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.
|
// Holds column index that is hovered, works only in full-edit formula mode.
|
||||||
this.hoverColumn = ko.observable(-1);
|
this.hoverColumn = ko.observable(-1);
|
||||||
|
|
||||||
|
this._insertColumnIndex = ko.observable(null);
|
||||||
|
|
||||||
// Checks if there is active formula editor for a column in this table.
|
// Checks if there is active formula editor for a column in this table.
|
||||||
this.editingFormula = ko.pureComputed(() => {
|
this.editingFormula = ko.pureComputed(() => {
|
||||||
const isEditing = this.gristDoc.docModel.editingFormula();
|
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.
|
// Re-define editField after fieldEditSave to make it take precedence for the Enter key.
|
||||||
editField: function() { closeRegisteredMenu(); this.scrollToCursor(true); this.activateEditorAtCursor(); },
|
editField: function() { closeRegisteredMenu(); this.scrollToCursor(true); this.activateEditorAtCursor(); },
|
||||||
|
|
||||||
insertFieldBefore: function() { this.insertColumn(this.cursor.fieldIndex()); },
|
insertFieldBefore: function() {
|
||||||
insertFieldAfter: function() { this.insertColumn(this.cursor.fieldIndex() + 1); },
|
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()); },
|
renameField: function() { this.renameColumn(this.cursor.fieldIndex()); },
|
||||||
hideFields: function() { this.hideFields(this.getSelection()); },
|
hideFields: function() { this.hideFields(this.getSelection()); },
|
||||||
deleteFields: function() {
|
deleteFields: function() {
|
||||||
@ -836,60 +857,26 @@ GridView.prototype.deleteRows = async function(rowIds) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
GridView.prototype.addNewColumn = function() {
|
GridView.prototype.insertColumn = async function(colId = null, options = {}) {
|
||||||
this.insertColumn(this.viewSection.viewFields().peekLength)
|
const {
|
||||||
.then(() => this.scrollPaneRight());
|
colInfo = {},
|
||||||
};
|
index = this.viewSection.viewFields().peekLength,
|
||||||
|
skipPopup = false
|
||||||
GridView.prototype.insertColumn = async function(index) {
|
} = options;
|
||||||
const pos = tableUtil.fieldInsertPositions(this.viewSection.viewFields(), index)[0];
|
const newColInfo = await this.viewSection.insertColumn(colId, {colInfo, index});
|
||||||
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]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.selectColumn(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) {
|
GridView.prototype.renameColumn = function(index) {
|
||||||
this.currentEditingColumnIndex(index);
|
this.currentEditingColumnIndex(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
GridView.prototype.scrollPaneLeft = function() {
|
||||||
|
this.scrollPane.scrollLeft = 0;
|
||||||
|
};
|
||||||
|
|
||||||
GridView.prototype.scrollPaneRight = function() {
|
GridView.prototype.scrollPaneRight = function() {
|
||||||
this.scrollPane.scrollLeft = this.scrollPane.scrollWidth;
|
this.scrollPane.scrollLeft = this.scrollPane.scrollWidth;
|
||||||
};
|
};
|
||||||
@ -899,16 +886,12 @@ GridView.prototype.selectColumn = function(colIndex) {
|
|||||||
this.cellSelector.currentSelectType(selector.COL);
|
this.cellSelector.currentSelectType(selector.COL);
|
||||||
};
|
};
|
||||||
|
|
||||||
GridView.prototype.showColumn = function(colId, index) {
|
GridView.prototype.showColumn = async function(
|
||||||
let fieldPos = tableUtil.fieldInsertPositions(this.viewSection.viewFields(), index, 1)[0];
|
colRef,
|
||||||
let colInfo = {
|
index = this.viewSection.viewFields().peekLength
|
||||||
parentId: this.viewSection.id(),
|
) {
|
||||||
colRef: colId,
|
await this.viewSection.showColumn(colRef, index);
|
||||||
parentPos: fieldPos
|
this.selectColumn(index);
|
||||||
};
|
|
||||||
return this.gristDoc.docModel.viewFields.sendTableAction(['AddRecord', null, colInfo])
|
|
||||||
.then(() => this.selectColumn(index))
|
|
||||||
.then(() => this.scrollPaneRight());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Replace alerts with custom notifications
|
// 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(
|
return dom(
|
||||||
'div.gridview_data_pane.flexvbox',
|
'div.gridview_data_pane.flexvbox',
|
||||||
// offset for frozen columns - how much move them to the left
|
// offset for frozen columns - how much move them to the left
|
||||||
@ -1343,13 +1304,15 @@ GridView.prototype.buildDom = function() {
|
|||||||
testId('column-menu-trigger'),
|
testId('column-menu-trigger'),
|
||||||
),
|
),
|
||||||
dom('div.selection'),
|
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.isPreview ? null : kd.maybe(() => !this.gristDoc.isReadonlyKo(), () => (
|
||||||
this._modField = dom('div.column_name.mod-add-column.field',
|
this._modField = dom('div.column_name.mod-add-column.field',
|
||||||
'+',
|
'+',
|
||||||
kd.style("width", PLUS_WIDTH + 'px'),
|
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) {
|
kd.foreach(v.viewFields(), function(field) {
|
||||||
// Whether the cell has a cursor (possibly in an inactive view section).
|
// Whether the cell has a cursor (possibly in an inactive view section).
|
||||||
var isCellSelected = ko.computed(() =>
|
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.
|
// Whether the cell is active: has the cursor in the active section.
|
||||||
var isCellActive = ko.computed(() => isCellSelected() && v.hasFocus());
|
var isCellActive = ko.computed(() => isCellSelected() && v.hasFocus());
|
||||||
@ -1529,6 +1495,8 @@ GridView.prototype.buildDom = function() {
|
|||||||
|
|
||||||
return dom(
|
return dom(
|
||||||
'div.field',
|
'div.field',
|
||||||
|
kd.toggleClass('field-insert-before', () =>
|
||||||
|
self._insertColumnIndex() === field._index()),
|
||||||
kd.style('--frozen-position', () => ko.unwrap(self.frozenPositions.at(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("frozen", () => ko.unwrap(self.frozenMap.at(field._index()))),
|
||||||
kd.toggleClass('scissors', isCopyActive),
|
kd.toggleClass('scissors', isCopyActive),
|
||||||
@ -1541,8 +1509,9 @@ GridView.prototype.buildDom = function() {
|
|||||||
//TODO: Ensure that fields in a row resize when
|
//TODO: Ensure that fields in a row resize when
|
||||||
//a cell in that row becomes larger
|
//a cell in that row becomes larger
|
||||||
kd.style('borderRightWidth', v.borderWidthPx),
|
kd.style('borderRightWidth', v.borderWidthPx),
|
||||||
|
|
||||||
kd.toggleClass('selected', isSelected),
|
kd.toggleClass('selected', isSelected),
|
||||||
|
// Optional icon. Currently only use to show formula icon.
|
||||||
|
dom('div.field-icon'),
|
||||||
fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected),
|
fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected),
|
||||||
dom('div.selection'),
|
dom('div.selection'),
|
||||||
);
|
);
|
||||||
@ -1881,9 +1850,9 @@ GridView.prototype.columnContextMenu = function(ctl, copySelection, field, filte
|
|||||||
const options = this._getColumnMenuOptions(copySelection);
|
const options = this._getColumnMenuOptions(copySelection);
|
||||||
|
|
||||||
if (selectedColIds.length > 1 && selectedColIds.includes(field.column().colId())) {
|
if (selectedColIds.length > 1 && selectedColIds.includes(field.column().colId())) {
|
||||||
return MultiColumnMenu(options);
|
return buildMultiColumnMenu(options);
|
||||||
} else {
|
} else {
|
||||||
return ColumnContextMenu({
|
return buildColumnContextMenu({
|
||||||
filterOpenFunc: () => filterTriggerCtl.open(),
|
filterOpenFunc: () => filterTriggerCtl.open(),
|
||||||
sortSpec: this.gristDoc.viewModel.activeSection.peek().activeSortSpec.peek(),
|
sortSpec: this.gristDoc.viewModel.activeSection.peek().activeSortSpec.peek(),
|
||||||
colId: field.column.peek().id.peek(),
|
colId: field.column.peek().id.peek(),
|
||||||
@ -2000,20 +1969,113 @@ GridView.prototype._scrollColumnIntoView = function(colIndex) {
|
|||||||
// If there are some frozen columns.
|
// If there are some frozen columns.
|
||||||
if (this.numFrozen.peek() && colIndex < this.numFrozen.peek()) { return; }
|
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 rowNumsWidth = this._cornerDom.clientWidth;
|
||||||
const viewWidth = this.scrollPane.clientWidth - rowNumsWidth;
|
const viewWidth = this.scrollPane.clientWidth - rowNumsWidth;
|
||||||
const fieldWidth = this.colRightOffsets.peek().getValue(colIndex) + 1; // +1px border
|
const fieldWidth = this.colRightOffsets.peek().getValue(colIndex) + 1; // +1px border
|
||||||
|
|
||||||
// Left and right pixel edge of 'viewport', starting from edge of row nums.
|
// Left and right pixel edge of 'viewport', starting from edge of row nums.
|
||||||
const frozenWidth = this.frozenWidth.peek();
|
const frozenWidth = this.frozenWidth.peek();
|
||||||
const leftEdge = this.scrollPane.scrollLeft + frozenWidth;
|
const leftEdge = this.scrollPane.scrollLeft + frozenWidth;
|
||||||
const rightEdge = leftEdge + (viewWidth - frozenWidth);
|
const rightEdge = leftEdge + (viewWidth - frozenWidth);
|
||||||
|
|
||||||
// If cell doesn't fit onscreen, scroll to fit.
|
// If cell doesn't fit onscreen, scroll to fit.
|
||||||
const scrollShift = offset - gutil.clamp(offset, leftEdge, rightEdge - fieldWidth);
|
const scrollShift = offset - gutil.clamp(offset, leftEdge, rightEdge - fieldWidth);
|
||||||
this.scrollPane.scrollLeft = this.scrollPane.scrollLeft + scrollShift;
|
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) {
|
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 * as DocConfigTab from 'app/client/components/DocConfigTab';
|
||||||
import {Drafts} from "app/client/components/Drafts";
|
import {Drafts} from "app/client/components/Drafts";
|
||||||
import {EditorMonitor} from "app/client/components/EditorMonitor";
|
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 {importFromFile, selectAndImport} from 'app/client/components/Importer';
|
||||||
import {RawDataPage, RawDataPopup} from 'app/client/components/RawDataPage';
|
import {RawDataPage, RawDataPopup} from 'app/client/components/RawDataPage';
|
||||||
import {ActionGroupWithCursorPos, UndoStack} from 'app/client/components/UndoStack';
|
import {ActionGroupWithCursorPos, UndoStack} from 'app/client/components/UndoStack';
|
||||||
@ -785,7 +785,7 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getTableModel(tableId: string): DataTableModel {
|
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
|
// Get a DataTableModel, possibly wrapped to include diff data if a comparison is
|
||||||
|
@ -61,6 +61,20 @@
|
|||||||
background-color: var(--field-background-color, unset);
|
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 */
|
/** Similar order is for detail view, but there is no row rules */
|
||||||
.g_record_detail_value {
|
.g_record_detail_value {
|
||||||
background-color: var(--grist-diff-background-color,
|
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;
|
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" {
|
declare module "app/client/components/ViewConfigTab" {
|
||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
import {Disposable} from 'app/client/lib/dispose';
|
import {Disposable} from 'app/client/lib/dispose';
|
||||||
|
@ -239,6 +239,10 @@ export class DocModel {
|
|||||||
&& this.allTableIds.all().includes('GristDocTutorial'));
|
&& this.allTableIds.all().includes('GristDocTutorial'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getTableModel(tableId: string) {
|
||||||
|
return this.dataTables[tableId];
|
||||||
|
}
|
||||||
|
|
||||||
private _metaTableModel<TName extends keyof SchemaTypes, TRow extends IRowModel<TName>>(
|
private _metaTableModel<TName extends keyof SchemaTypes, TRow extends IRowModel<TName>>(
|
||||||
tableId: TName,
|
tableId: TName,
|
||||||
rowConstructor: (this: TRow, docModel: DocModel) => void,
|
rowConstructor: (this: TRow, docModel: DocModel) => void,
|
||||||
|
@ -10,6 +10,7 @@ import randomcolor from 'randomcolor';
|
|||||||
// Represents a user-defined table.
|
// Represents a user-defined table.
|
||||||
export interface TableRec extends IRowModel<"_grist_Tables"> {
|
export interface TableRec extends IRowModel<"_grist_Tables"> {
|
||||||
columns: ko.Computed<KoArray<ColumnRec>>;
|
columns: ko.Computed<KoArray<ColumnRec>>;
|
||||||
|
visibleColumns: ko.Computed<ColumnRec[]>;
|
||||||
validations: ko.Computed<KoArray<ValidationRec>>;
|
validations: ko.Computed<KoArray<ValidationRec>>;
|
||||||
|
|
||||||
primaryView: ko.Computed<ViewRec>;
|
primaryView: ko.Computed<ViewRec>;
|
||||||
@ -45,6 +46,8 @@ export interface TableRec extends IRowModel<"_grist_Tables"> {
|
|||||||
|
|
||||||
export function createTableRec(this: TableRec, docModel: DocModel): void {
|
export function createTableRec(this: TableRec, docModel: DocModel): void {
|
||||||
this.columns = recordSet(this, docModel.columns, 'parentId', {sortBy: 'parentPos'});
|
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.validations = recordSet(this, docModel.validations, 'tableRef');
|
||||||
|
|
||||||
this.primaryView = refRecord(docModel.views, this.primaryViewId);
|
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 {SequenceNEVER, SequenceNum} from 'app/client/components/Cursor';
|
||||||
import {EmptyFilterColValues, LinkingState} from 'app/client/components/LinkingState';
|
import {EmptyFilterColValues, LinkingState} from 'app/client/components/LinkingState';
|
||||||
import {KoArray} from 'app/client/lib/koArray';
|
import {KoArray} from 'app/client/lib/koArray';
|
||||||
|
import {fieldInsertPositions} from 'app/client/lib/tableUtil';
|
||||||
import {ColumnToMapImpl} from 'app/client/models/ColumnToMap';
|
import {ColumnToMapImpl} from 'app/client/models/ColumnToMap';
|
||||||
import {
|
import {
|
||||||
ColumnRec,
|
ColumnRec,
|
||||||
@ -23,14 +24,36 @@ import {getWidgetTypes} from "app/client/ui/widgetTypesMap";
|
|||||||
import {FilterColValues} from "app/common/ActiveDocAPI";
|
import {FilterColValues} from "app/common/ActiveDocAPI";
|
||||||
import {AccessLevel, ICustomWidget} from 'app/common/CustomWidget';
|
import {AccessLevel, ICustomWidget} from 'app/common/CustomWidget';
|
||||||
import {UserAction} from 'app/common/DocActions';
|
import {UserAction} from 'app/common/DocActions';
|
||||||
|
import {RecalcWhen} from 'app/common/gristTypes';
|
||||||
import {arrayRepeat} from 'app/common/gutil';
|
import {arrayRepeat} from 'app/common/gutil';
|
||||||
import {Sort} from 'app/common/SortSpec';
|
import {Sort} from 'app/common/SortSpec';
|
||||||
import {ColumnsToMap, WidgetColumnMap} from 'app/plugin/CustomSectionAPI';
|
import {ColumnsToMap, WidgetColumnMap} from 'app/plugin/CustomSectionAPI';
|
||||||
import {CursorPos, UIRowId} from 'app/plugin/GristAPI';
|
import {CursorPos, UIRowId} from 'app/plugin/GristAPI';
|
||||||
|
import {GristObjCode} from 'app/plugin/GristData';
|
||||||
import {Computed, Holder, Observable} from 'grainjs';
|
import {Computed, Holder, Observable} from 'grainjs';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
import defaults = require('lodash/defaults');
|
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
|
// 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).
|
// a grid section and a chart section).
|
||||||
export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleOwner {
|
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)
|
// Saves custom definition (bundles change)
|
||||||
saveCustomDef(): Promise<void>;
|
saveCustomDef(): Promise<void>;
|
||||||
|
|
||||||
|
insertColumn(colId?: string|null, options?: InsertColOptions): Promise<NewColInfo>;
|
||||||
|
|
||||||
|
showColumn(colRef: number, index?: number): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WidgetMappedColumn = number|number[]|null;
|
export type WidgetMappedColumn = number|number[]|null;
|
||||||
@ -304,7 +331,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
|||||||
this.linkedSections = recordSet(this, docModel.viewSections, 'linkSrcSectionRef');
|
this.linkedSections = recordSet(this, docModel.viewSections, 'linkSrcSectionRef');
|
||||||
|
|
||||||
// All table columns associated with this view section, excluding any hidden helper columns.
|
// 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({
|
this.editingFormula = ko.pureComputed({
|
||||||
read: () => docModel.editingFormula(),
|
read: () => docModel.editingFormula(),
|
||||||
write: val => {
|
write: val => {
|
||||||
@ -766,4 +793,36 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
|||||||
const list = this.view().activeCollapsedSections();
|
const list = this.view().activeCollapsedSections();
|
||||||
return list.includes(this.id());
|
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 {allCommands} from 'app/client/components/commands';
|
||||||
|
import GridView from 'app/client/components/GridView';
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||||
import {testId, theme} from 'app/client/ui2018/cssVars';
|
import {testId, theme} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {
|
import {
|
||||||
enhanceBySearch,
|
|
||||||
menuDivider,
|
menuDivider,
|
||||||
|
menuIcon,
|
||||||
menuItem,
|
menuItem,
|
||||||
menuItemCmd,
|
menuItemCmd,
|
||||||
menuItemSubmenu,
|
menuItemSubmenu,
|
||||||
menuSubHeader,
|
menuSubHeader,
|
||||||
menuText
|
menuText,
|
||||||
|
searchableMenu,
|
||||||
} from 'app/client/ui2018/menus';
|
} from 'app/client/ui2018/menus';
|
||||||
import {Sort} from 'app/common/SortSpec';
|
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 {RecalcWhen} from "../../common/gristTypes";
|
||||||
import {GristDoc} from "../components/GristDoc";
|
|
||||||
import {ColumnRec} from "../models/entities/ColumnRec";
|
import {ColumnRec} from "../models/entities/ColumnRec";
|
||||||
import {FieldBuilder} from "../widgets/FieldBuilder";
|
|
||||||
import isEqual = require('lodash/isEqual');
|
import isEqual = require('lodash/isEqual');
|
||||||
|
|
||||||
const t = makeT('GridViewMenus');
|
const t = makeT('GridViewMenus');
|
||||||
|
|
||||||
//encapsulation over the view that menu will be generated for
|
// FIXME: remove once New Column menu is enabled by default.
|
||||||
interface IView {
|
export function buildOldAddColumnMenu(gridView: GridView, viewSection: ViewSectionRec) {
|
||||||
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){
|
|
||||||
return [
|
return [
|
||||||
menuDivider(),
|
menuItem(async () => { await gridView.insertColumn(); }, t("Add Column")),
|
||||||
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(),
|
menuDivider(),
|
||||||
...viewSection.hiddenColumns().map((col: any) => menuItem(
|
...viewSection.hiddenColumns().map((col: any) => menuItem(
|
||||||
() => {
|
async () => {
|
||||||
gridView.showColumn(col.id(), viewSection.viewFields().peekLength);
|
await gridView.showColumn(col.id());
|
||||||
// .then(() => gridView.scrollPaneRight());
|
|
||||||
}, t("Show column {{- label}}", {label: col.label()})))
|
}, t("Show column {{- label}}", {label: col.label()})))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function buildAddColumnMenu(gridView: GridView, index?: number) {
|
||||||
* Creates a menu to add a new column.
|
|
||||||
*/
|
|
||||||
export function ColumnAddMenu(gridView: IView, viewSection: IViewSection) {
|
|
||||||
return [
|
return [
|
||||||
menuItem(
|
menuItem(
|
||||||
async () => { await gridView.addNewColumn(); },
|
async () => { await gridView.insertColumn(null, {index}); },
|
||||||
`+ ${t("Add Column")}`,
|
menuIcon('Plus'),
|
||||||
testId('new-columns-menu-add-new')
|
t("Add Column"),
|
||||||
|
testId('new-columns-menu-add-new'),
|
||||||
),
|
),
|
||||||
MenuHideColumnSection(gridView, viewSection),
|
buildHiddenColumnsMenuItems(gridView, index),
|
||||||
MenuLookups(viewSection, gridView),
|
buildLookupsMenuItems(gridView, index),
|
||||||
MenuShortcuts(gridView),
|
buildShortcutsMenuItems(gridView, index),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: figure out how to change columns names;
|
function buildHiddenColumnsMenuItems(gridView: GridView, index?: number) {
|
||||||
const addNewColumnWithTimestamp = async (gridView: IView, triggerOnUpdate: boolean) => {
|
const {viewSection} = gridView;
|
||||||
await gridView.gristDoc.docData.bundleActions('Add new column with timestamp', async () => {
|
const hiddenColumns = viewSection.hiddenColumns();
|
||||||
const column = await gridView.addNewColumnWithoutRenamePopup();
|
if (hiddenColumns.length === 0) { return null; }
|
||||||
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) => {
|
return [
|
||||||
await gridView.gristDoc.docData.bundleActions('Add new column with author', async () => {
|
menuDivider(),
|
||||||
const column = await gridView.addNewColumnWithoutRenamePopup();
|
menuSubHeader(t('Hidden Columns'), testId('new-columns-menu-hidden-columns')),
|
||||||
if (!triggerOnUpdate) {
|
hiddenColumns.length > 5
|
||||||
await column.gristDoc.convertToTrigger(column.origColumn.id.peek(), 'user.Name', RecalcWhen.DEFAULT);
|
? [
|
||||||
await column.field.displayLabel.setAndSave(t('Created By'));
|
menuItemSubmenu(
|
||||||
await column.field.column.peek().type.setAndSave('Text');
|
() => {
|
||||||
} else {
|
return searchableMenu(
|
||||||
await column.gristDoc.convertToTrigger(column.origColumn.id.peek(), 'user.Name', RecalcWhen.MANUAL_UPDATES);
|
hiddenColumns.map((col) => ({
|
||||||
await column.field.displayLabel.setAndSave(t('Last Updated By'));
|
cleanText: col.label().trim().toLowerCase(),
|
||||||
await column.field.column.peek().type.setAndSave('Text');
|
label: col.label(),
|
||||||
}
|
action: async () => { await gridView.showColumn(col.id(), index); },
|
||||||
}, {nestInActiveBundle: true});
|
})),
|
||||||
};
|
{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 {
|
export interface IMultiColumnContextMenu {
|
||||||
// For multiple selection, true/false means the value applies to all columns, 'mixed' means it's
|
// 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);
|
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 { disableModify, filterOpenFunc, colId, sortSpec, isReadonly } = options;
|
||||||
|
|
||||||
const disableForReadonlyColumn = dom.cls('disabled', Boolean(disableModify) || isReadonly);
|
const disableForReadonlyColumn = dom.cls('disabled', Boolean(disableModify) || isReadonly);
|
||||||
@ -318,7 +394,7 @@ export function ColumnContextMenu(options: IColumnContextMenu) {
|
|||||||
menuItemCmd(allCommands.renameField, t("Rename column"), disableForReadonlyColumn),
|
menuItemCmd(allCommands.renameField, t("Rename column"), disableForReadonlyColumn),
|
||||||
freezeMenuItemCmd(options),
|
freezeMenuItemCmd(options),
|
||||||
menuDivider(),
|
menuDivider(),
|
||||||
MultiColumnMenu((options.disableFrozenMenu = true, options)),
|
buildMultiColumnMenu((options.disableFrozenMenu = true, options)),
|
||||||
testId('column-menu'),
|
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
|
* We offer both options if data columns are selected. If only formulas, only the second option
|
||||||
* makes sense.
|
* makes sense.
|
||||||
*/
|
*/
|
||||||
export function MultiColumnMenu(options: IMultiColumnContextMenu) {
|
export function buildMultiColumnMenu(options: IMultiColumnContextMenu) {
|
||||||
const disableForReadonlyColumn = dom.cls('disabled', Boolean(options.disableModify) || options.isReadonly);
|
const disableForReadonlyColumn = dom.cls('disabled', Boolean(options.disableModify) || options.isReadonly);
|
||||||
const disableForReadonlyView = dom.cls('disabled', options.isReadonly);
|
const disableForReadonlyView = dom.cls('disabled', options.isReadonly);
|
||||||
const num: number = options.numColumns;
|
const num: number = options.numColumns;
|
||||||
|
@ -136,6 +136,7 @@ export const vars = {
|
|||||||
toastBg: new CustomProp('toast-bg', '#040404'),
|
toastBg: new CustomProp('toast-bg', '#040404'),
|
||||||
|
|
||||||
/* Z indexes */
|
/* Z indexes */
|
||||||
|
insertColumnLineZIndex: new CustomProp('insert-column-line-z-index', '20'),
|
||||||
menuZIndex: new CustomProp('menu-z-index', '999'),
|
menuZIndex: new CustomProp('menu-z-index', '999'),
|
||||||
modalZIndex: new CustomProp('modal-z-index', '999'),
|
modalZIndex: new CustomProp('modal-z-index', '999'),
|
||||||
onboardingBackdropZIndex: new CustomProp('onboarding-backdrop-z-index', '999'),
|
onboardingBackdropZIndex: new CustomProp('onboarding-backdrop-z-index', '999'),
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
BindableValue, Computed, dom, DomElementArg, DomElementMethod, IDomArgs,
|
BindableValue, Computed, dom, DomElementArg, DomElementMethod, IDomArgs,
|
||||||
MaybeObsArray, MutableObsArray, Observable, styled
|
MaybeObsArray, MutableObsArray, Observable, styled
|
||||||
} from 'grainjs';
|
} from 'grainjs';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
import * as weasel from 'popweasel';
|
import * as weasel from 'popweasel';
|
||||||
|
|
||||||
const t = makeT('menus');
|
const t = makeT('menus');
|
||||||
@ -49,36 +50,70 @@ export function menu(createFunc: weasel.MenuCreateFunc, options?: weasel.IMenuOp
|
|||||||
return weasel.menu(wrappedCreateFunc, {...defaults, ...options});
|
return weasel.menu(wrappedCreateFunc, {...defaults, ...options});
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssSearchField = styled('input',
|
export interface SearchableMenuOptions {
|
||||||
'border: none;'+
|
searchInputPlaceholder?: string;
|
||||||
'background-color: transparent;'+
|
}
|
||||||
'padding: 8px 24px 4px 24px;'+
|
|
||||||
'&:focus {outline: none;}'
|
export interface SearchableMenuItem {
|
||||||
);
|
cleanText: string;
|
||||||
export function enhanceBySearch( menuFunc: (searchCriteria: Observable<string>) => DomElementArg[]): DomElementArg[]
|
label: string;
|
||||||
{
|
action: (item: HTMLElement) => void;
|
||||||
const searchCriteria = Observable.create(null, '');
|
args?: DomElementArg[];
|
||||||
const searchInput = [
|
}
|
||||||
menuItemStatic(
|
|
||||||
cssSearchField(
|
export function searchableMenu(
|
||||||
dom.on('input', (_ev, elem) => searchCriteria.set(elem.value)),
|
menuItems: MaybeObsArray<SearchableMenuItem>,
|
||||||
{placeholder: '🔍\uFE0E\t' + t("Search columns")}
|
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(),
|
),
|
||||||
];
|
menuDivider(),
|
||||||
return [...searchInput, ...menuFunc(searchCriteria)];
|
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.
|
// 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(
|
export function menuItemSubmenu(
|
||||||
submenu: weasel.MenuCreateFunc,
|
submenu: weasel.MenuCreateFunc,
|
||||||
options: ISubMenuOptions,
|
options: ISubMenuOptions,
|
||||||
...args: DomElementArg[]
|
...args: DomElementArg[]
|
||||||
): Element {
|
): 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', `
|
export const cssMenuElem = styled('div', `
|
||||||
@ -449,7 +484,7 @@ export const menuSubHeader = styled('div', `
|
|||||||
font-size: ${vars.xsmallFontSize};
|
font-size: ${vars.xsmallFontSize};
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: ${vars.bigControlTextWeight};
|
font-weight: ${vars.bigControlTextWeight};
|
||||||
padding: 8px 24px 16px 24px;
|
padding: 8px 24px 8px 24px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -669,3 +704,43 @@ const cssCheckboxText = styled(cssLabelText, `
|
|||||||
const cssUpgradeTextButton = styled(textButton, `
|
const cssUpgradeTextButton = styled(textButton, `
|
||||||
font-size: ${vars.smallFontSize};
|
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', `
|
const cssAttachmentIcon = styled('div.glyphicon.glyphicon-paperclip', `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
left: 2px;
|
left: 5px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
background-color: ${theme.attachmentsCellIconBg};
|
background-color: ${theme.attachmentsCellIconBg};
|
||||||
color: ${theme.attachmentsCellIconFg};
|
color: ${theme.attachmentsCellIconFg};
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
color: #D0D0D0;
|
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 */
|
/* based on standard icon styles */
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -28,13 +30,13 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formula_field::before, .formula_field_edit::before {
|
.formula_field .field-icon, .formula_field_edit::before {
|
||||||
background-color: #D0D0D0;
|
background-color: #D0D0D0;
|
||||||
}
|
}
|
||||||
.formula_field_edit:not(.readonly)::before {
|
.formula_field_edit:not(.readonly)::before {
|
||||||
background-color: var(--grist-color-cursor);
|
background-color: var(--grist-color-cursor);
|
||||||
}
|
}
|
||||||
.formula_field.invalid::before {
|
.formula_field.invalid .field-icon {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: #ffb6c1;
|
color: #ffb6c1;
|
||||||
}
|
}
|
||||||
|
@ -172,7 +172,7 @@
|
|||||||
"piscina": "3.2.0",
|
"piscina": "3.2.0",
|
||||||
"plotly.js-basic-dist": "2.13.2",
|
"plotly.js-basic-dist": "2.13.2",
|
||||||
"popper-max-size-modifier": "0.2.0",
|
"popper-max-size-modifier": "0.2.0",
|
||||||
"popweasel": "0.1.18",
|
"popweasel": "0.1.20",
|
||||||
"qrcode": "1.5.0",
|
"qrcode": "1.5.0",
|
||||||
"randomcolor": "0.5.3",
|
"randomcolor": "0.5.3",
|
||||||
"redis": "3.1.1",
|
"redis": "3.1.1",
|
||||||
|
11
yarn.lock
11
yarn.lock
@ -4221,7 +4221,7 @@ grain-rpc@0.1.7:
|
|||||||
events "^1.1.1"
|
events "^1.1.1"
|
||||||
ts-interface-checker "^1.0.0"
|
ts-interface-checker "^1.0.0"
|
||||||
|
|
||||||
grainjs@1.0.2, grainjs@^1.0.1:
|
grainjs@1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/grainjs/-/grainjs-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/grainjs/-/grainjs-1.0.2.tgz"
|
||||||
integrity sha512-wrj8TqpgxTGOKHpTlMBxMeX2uS3lTvXj4ROLKC+EZNM7J6RHQLGjMzMqWtiryBnMhGIBlbCicMNFppCrK1zv9w==
|
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"
|
resolved "https://registry.npmjs.org/popper.js/-/popper.js-1.15.0.tgz"
|
||||||
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
|
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
|
||||||
|
|
||||||
popweasel@0.1.18:
|
popweasel@0.1.20:
|
||||||
version "0.1.18"
|
version "0.1.20"
|
||||||
resolved "https://registry.npmjs.org/popweasel/-/popweasel-0.1.18.tgz"
|
resolved "https://registry.yarnpkg.com/popweasel/-/popweasel-0.1.20.tgz#b69af57b08288dce398c2105cb1e8a9f4e0e324c"
|
||||||
integrity sha512-F7+QRcnkj963ahDGURcZpucONfxhFWtlXLABawzaW7J/iOfcKMFRyjqluCebOLnBROPPBrih4g2qbq7KdQ0WMw==
|
integrity sha512-iG51KFrHL49YuWTeI2yGby8BdNewdtxiKRv6y+Pyh1CkRKenLFu5CPMaKDRLbfiQJeZ/t67WW0e9ggWTt09ClA==
|
||||||
dependencies:
|
dependencies:
|
||||||
grainjs "^1.0.1"
|
|
||||||
lodash "^4.17.15"
|
lodash "^4.17.15"
|
||||||
popper.js "1.15.0"
|
popper.js "1.15.0"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user