diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index cafc71ef..73970b2b 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -55,6 +55,7 @@ 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 { isList } = require('app/common/gristTypes'); const t = makeT('GridView'); @@ -345,6 +346,7 @@ GridView.gridCommands = { this.insertColumn(null, {index: this.cursor.fieldIndex() + 1}); } }, + makeHeadersFromRow: function() { this.makeHeadersFromRow(this.getSelection()); }, renameField: function() { this.renameColumn(this.cursor.fieldIndex()); }, hideFields: function() { this.hideFields(this.getSelection()); }, deleteFields: function() { @@ -902,6 +904,38 @@ GridView.prototype.insertColumn = async function(colId = null, options = {}) { return newColInfo; }; +GridView.prototype.makeHeadersFromRow = async function(selection) { + if (this._getCellContextMenuOptions().disableMakeHeadersFromRow){ + return; + } + const record = this.tableModel.tableData.getRecord(selection.rowIds[0]); + const actions = this.viewSection.viewFields().peek().reduce((acc, field) => { + const col = field.column(); + const colId = col.colId.peek(); + let formatter = field.formatter(); + let newColLabel = record[colId]; + // Manage column that are references + if (col.refTable()) { + const refTableDisplayCol = this.gristDoc.docModel.columns.getRowModel(col.displayCol()); + newColLabel = record[refTableDisplayCol.colId()]; + formatter = field.visibleColFormatter(); + } + // Manage column that are lists + if (isList(newColLabel)) { + newColLabel = newColLabel[1]; + } + if (typeof newColLabel === 'string') { + newColLabel = newColLabel.trim(); + } + // Check value is not empty but accept 0 and false as valid values + if (newColLabel !== null && newColLabel !== undefined && newColLabel !== "") { + return [...acc, ['ModifyColumn', colId, {"label": formatter.formatAny(newColLabel)}]]; + } + return acc + }, []); + this.tableModel.sendTableActions(actions, "Use as table headers"); +}; + GridView.prototype.renameColumn = function(index) { this.currentEditingColumnIndex(index); }; @@ -1974,6 +2008,9 @@ GridView.prototype._getCellContextMenuOptions = function() { this.viewSection.disableAddRemoveRows() || this.getSelection().onlyAddRowSelected() ), + disableMakeHeadersFromRow: Boolean ( + this.gristDoc.isReadonly.get() || this.getSelection().rowIds.length !== 1 || this.getSelection().onlyAddRowSelected() + ), isViewSorted: this.viewSection.activeSortSpec.peek().length > 0, numRows: this.getSelection().rowIds.length, }; diff --git a/app/client/components/commandList.ts b/app/client/components/commandList.ts index d943e9ec..c34fc16e 100644 --- a/app/client/components/commandList.ts +++ b/app/client/components/commandList.ts @@ -84,6 +84,7 @@ export type CommandName = | 'deleteRecords' | 'insertFieldBefore' | 'insertFieldAfter' + | 'makeHeadersFromRow' | 'renameField' | 'hideFields' | 'hideCardFields' @@ -562,6 +563,10 @@ export const groups: CommendGroupDef[] = [{ name: 'insertFieldAfter', keys: ['Alt+='], desc: 'Insert a new column, after the currently selected one' + }, { + name: 'makeHeadersFromRow', + keys: ['Mod+Shift+H'], + desc: 'Use currently selected line as table headers' }, { name: 'renameField', keys: ['Ctrl+m'], diff --git a/app/client/ui/RowContextMenu.ts b/app/client/ui/RowContextMenu.ts index 365387ce..7421880a 100644 --- a/app/client/ui/RowContextMenu.ts +++ b/app/client/ui/RowContextMenu.ts @@ -8,6 +8,7 @@ const t = makeT('RowContextMenu'); export interface IRowContextMenu { disableInsert: boolean; disableDelete: boolean; + disableMakeHeadersFromRow: boolean; disableShowRecordCard: boolean; isViewSorted: boolean; numRows: number; @@ -16,6 +17,7 @@ export interface IRowContextMenu { export function RowContextMenu({ disableInsert, disableDelete, + disableMakeHeadersFromRow, disableShowRecordCard, isViewSorted, numRows @@ -51,6 +53,11 @@ export function RowContextMenu({ menuItemCmd(allCommands.duplicateRows, t('Duplicate rows', { count: numRows }), dom.cls('disabled', disableInsert || numRows === 0)), ); + result.push( + menuDivider(), + menuItemCmd(allCommands.makeHeadersFromRow, t("Use as table headers"), + dom.cls('disabled', disableMakeHeadersFromRow)), + ); result.push( menuDivider(), // TODO: should show `Delete ${num} rows` when multiple are selected diff --git a/static/locales/en.client.json b/static/locales/en.client.json index bec87294..90460109 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -665,7 +665,8 @@ "Insert row": "Insert row", "Insert row above": "Insert row above", "Insert row below": "Insert row below", - "View as card": "View as card" + "View as card": "View as card", + "Use as table headers": "Use as table headers" }, "SelectionSummary": { "Copied to clipboard": "Copied to clipboard" diff --git a/test/nbrowser/RowMenu.ts b/test/nbrowser/RowMenu.ts index f4c4e552..6ee539d8 100644 --- a/test/nbrowser/RowMenu.ts +++ b/test/nbrowser/RowMenu.ts @@ -72,11 +72,22 @@ describe('RowMenu', function() { assert.isFalse(await driver.find('.grist-floating-menu').isPresent()); }); + it('can rename headers from the selected line', async function() { + assert.notEqual(await gu.getColumnHeader({col: 0}).getText(), await gu.getCell(0, 1).getText()); + assert.notEqual(await gu.getColumnHeader({col: 1}).getText(), await gu.getCell(1, 1).getText()); + await (await gu.openRowMenu(1)).findContent('li', /Use as table headers/).click(); + await gu.waitForServer(); + assert.equal(await gu.getColumnHeader({col: 0}).getText(), await gu.getCell(0, 1).getText()); + assert.equal(await gu.getColumnHeader({col: 1}).getText(), await gu.getCell(1, 1).getText()); + }); + it('should work even when no columns are visible', async function() { // Previously, a bug would cause an error to be thrown instead. - await gu.openColumnMenu('A', 'Hide column'); - await gu.openColumnMenu('B', 'Hide column'); + await gu.openColumnMenu({col: 0}, 'Hide column'); + // After hiding the first column, the second one will be the new first column. + await gu.openColumnMenu({col: 0}, 'Hide column'); await assertRowMenuOpensAndCloses(); await assertRowMenuOpensWithRightClick(); }); + });