mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) updates from grist-core
This commit is contained in:
		
						commit
						0c05f4cdc4
					
				| @ -60,7 +60,7 @@ RUN \ | ||||
| # Fetch gvisor-based sandbox. Note, to enable it to run within default | ||||
| # unprivileged docker, layers of protection that require privilege have | ||||
| # been stripped away, see https://github.com/google/gvisor/issues/4371 | ||||
| FROM gristlabs/gvisor-unprivileged:buster as sandbox | ||||
| FROM docker.io/gristlabs/gvisor-unprivileged:buster as sandbox | ||||
| 
 | ||||
| ################################################################################ | ||||
| ## Run-time stage | ||||
|  | ||||
| @ -267,11 +267,11 @@ GRIST_DOMAIN        | in hosted Grist, Grist is served from subdomains of this d | ||||
| GRIST_EXPERIMENTAL_PLUGINS | enables experimental plugins | ||||
| GRIST_ENABLE_REQUEST_FUNCTION | enables the REQUEST function. This function performs HTTP requests in a similar way to `requests.request`. This function presents a significant security risk, since it can let users call internal endpoints when Grist is available publicly. This function can also cause performance issues. Unset by default. | ||||
| GRIST_HIDE_UI_ELEMENTS | comma-separated list of UI features to disable. Allowed names of parts: `helpCenter,billing,templates,createSite,multiSite,multiAccounts,sendToDrive,tutorials`. If a part also exists in GRIST_UI_FEATURES, it will still be disabled. | ||||
| GRIST_HOME_INCLUDE_STATIC | if set, home server also serves static resources | ||||
| GRIST_HOST          | hostname to use when listening on a port. | ||||
| GRIST_HTTPS_PROXY   | if set, use this proxy for webhook payload delivery. | ||||
| GRIST_ID_PREFIX | for subdomains of form o-*, expect or produce o-${GRIST_ID_PREFIX}*. | ||||
| GRIST_IGNORE_SESSION | if set, Grist will not use a session for authentication. | ||||
| GRIST_INCLUDE_CUSTOM_SCRIPT_URL | if set, will load the referenced URL in a `<script>` tag on all app pages. | ||||
| GRIST_INST_DIR      | path to Grist instance configuration files, for Grist server. | ||||
| GRIST_LIST_PUBLIC_SITES | if set to true, sites shared with the public will be listed for anonymous users. Defaults to false. | ||||
| GRIST_MANAGED_WORKERS | if set, Grist can assume that if a url targeted at a doc worker returns a 404, that worker is gone | ||||
|  | ||||
| @ -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, | ||||
|   }; | ||||
|  | ||||
| @ -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'], | ||||
|  | ||||
| @ -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
 | ||||
|  | ||||
| @ -6,6 +6,8 @@ | ||||
| var log = require('app/server/lib/log'); | ||||
| var Promise = require('bluebird'); | ||||
| 
 | ||||
| var os = require('os'); | ||||
| 
 | ||||
| var cleanupHandlers = []; | ||||
| 
 | ||||
| var signalsHandled = {}; | ||||
| @ -65,8 +67,8 @@ function runCleanupHandlers() { | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Internal helper to exit on a signal. It runs the cleanup handlers, and then re-sends the same | ||||
|  * signal, which will no longer get caught. | ||||
|  * Internal helper to exit on a signal. It runs the cleanup handlers, and then | ||||
|  * exits propagating the same signal code than the one caught. | ||||
|  */ | ||||
| function signalExit(signal) { | ||||
|   var prog = 'grist[' + process.pid + ']'; | ||||
| @ -81,7 +83,12 @@ function signalExit(signal) { | ||||
|     log.info("Server %s exiting on %s", prog, signal); | ||||
|     process.removeListener(signal, dup); | ||||
|     delete signalsHandled[signal]; | ||||
|     process.kill(process.pid, signal); | ||||
|     // Exit with the expected exit code for being killed by this signal.
 | ||||
|     // Unlike re-sending the same signal, the explicit exit works even
 | ||||
|     // in a situation when Grist is the init (pid 1) process in a container
 | ||||
|     // See https://github.com/gristlabs/grist-core/pull/830 (and #892)
 | ||||
|     const signalNumber = os.constants.signals[signal]; | ||||
|     process.exit(process.pid, 128 + signalNumber); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -586,7 +586,8 @@ | ||||
|         "Insert row below": "Insertar fila debajo", | ||||
|         "Duplicate rows_one": "Duplicar fila", | ||||
|         "Duplicate rows_other": "Duplicar filas", | ||||
|         "View as card": "Ver como tarjeta" | ||||
|         "View as card": "Ver como tarjeta", | ||||
|         "Use as table headers": "Usar como encabezados de la tabla" | ||||
|     }, | ||||
|     "ShareMenu": { | ||||
|         "Access Details": "Detalles de Acceso", | ||||
|  | ||||
| @ -1379,5 +1379,15 @@ | ||||
|     "FormSuccessPage": { | ||||
|         "Form Submitted": "Formulaire envoyé", | ||||
|         "Thank you! Your response has been recorded.": "Nous vous remercions. Votre réponse a été enregistrée." | ||||
|     }, | ||||
|     "DateRangeOptions": { | ||||
|         "Last 30 days": "30 derniers jours", | ||||
|         "Next 7 days": "7 prochains jours", | ||||
|         "Last 7 days": "7 derniers jours", | ||||
|         "Last Week": "Semaine passée", | ||||
|         "This month": "Ce mois-ci", | ||||
|         "This week": "Cette semaine", | ||||
|         "This year": "Cette année", | ||||
|         "Today": "Aujourd'hui" | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -268,7 +268,9 @@ | ||||
|         "You do not have write access to this site": "У вас нет права записи для этого сайта", | ||||
|         "Remove all data but keep the structure to use as a template": "Удалить все данные, но сохранить структуру для использования в качестве шаблона.", | ||||
|         "Download full document and history": "Скачать полный документ и историю", | ||||
|         "Remove document history (can significantly reduce file size)": "Удалить историю документа (может значительно уменьшить размер файла)" | ||||
|         "Remove document history (can significantly reduce file size)": "Удалить историю документа (может значительно уменьшить размер файла)", | ||||
|         "Download": "Скачать", | ||||
|         "Download document": "Скачать документ" | ||||
|     }, | ||||
|     "ShareMenu": { | ||||
|         "Back to Current": "Вернуться к текущему", | ||||
| @ -494,7 +496,7 @@ | ||||
|         "Welcome to {{orgName}}": "Добро пожаловать в {{orgName}}", | ||||
|         "personal site": "личный сайт", | ||||
|         "You have read-only access to this site. Currently there are no documents.": "Вы имеете доступ к этому сайту только для просмотра. В настоящее время документов нет.", | ||||
|         "{{signUp}} to save your work. ": "{{signUp}} сохранить свою работу. ", | ||||
|         "{{signUp}} to save your work. ": "{{signUp}} для сохранения своих данных. ", | ||||
|         "Welcome to Grist, {{- name}}!": "Добро пожаловать в Grist, {{- name}}!", | ||||
|         "Welcome to {{- orgName}}": "Добро пожаловать в {{- orgName}}", | ||||
|         "Visit our {{link}} to learn more about Grist.": "Посетите наш {{link}} чтобы узнать больше о Grist.", | ||||
| @ -1375,5 +1377,15 @@ | ||||
|     "FormSuccessPage": { | ||||
|         "Form Submitted": "Форма отправлена", | ||||
|         "Thank you! Your response has been recorded.": "Спасибо! Ваш ответ учтен." | ||||
|     }, | ||||
|     "DateRangeOptions": { | ||||
|         "Today": "Сегодня", | ||||
|         "Last 30 days": "Последние 30 дней", | ||||
|         "Last 7 days": "Последние 7 дней", | ||||
|         "Last Week": "Последняя неделя", | ||||
|         "Next 7 days": "Следующие 7 дней", | ||||
|         "This month": "Этот месяц", | ||||
|         "This week": "Эта неделя", | ||||
|         "This year": "Текущий год" | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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(); | ||||
|   }); | ||||
| 
 | ||||
| }); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user