mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) updates from grist-core
This commit is contained in:
commit
f1a0b61e15
@ -316,6 +316,20 @@ GridView.gridCommands = {
|
||||
this._shiftSelect(-1, this.cellSelector.col.end, selector.ROW,
|
||||
this.viewSection.viewFields().peekLength - 1);
|
||||
},
|
||||
ctrlShiftDown: function () {
|
||||
this._shiftSelectUntilContent(selector.COL, 1, this.cellSelector.row.end, this.getLastDataRowIndex());
|
||||
},
|
||||
ctrlShiftUp: function () {
|
||||
this._shiftSelectUntilContent(selector.COL, -1, this.cellSelector.row.end, this.getLastDataRowIndex());
|
||||
},
|
||||
ctrlShiftRight: function () {
|
||||
this._shiftSelectUntilContent(selector.ROW, 1, this.cellSelector.col.end,
|
||||
this.viewSection.viewFields().peekLength - 1);
|
||||
},
|
||||
ctrlShiftLeft: function () {
|
||||
this._shiftSelectUntilContent(selector.ROW, -1, this.cellSelector.col.end,
|
||||
this.viewSection.viewFields().peekLength - 1);
|
||||
},
|
||||
fillSelectionDown: function() { this.fillSelectionDown(); },
|
||||
selectAll: function() { this.selectAll(); },
|
||||
|
||||
@ -403,6 +417,120 @@ GridView.prototype._shiftSelect = function(step, selectObs, exemptType, maxVal)
|
||||
selectObs(newVal);
|
||||
};
|
||||
|
||||
GridView.prototype._shiftSelectUntilContent = function(type, direction, selectObs, maxVal) {
|
||||
const selection = {
|
||||
colStart: this.cellSelector.col.start(),
|
||||
colEnd: this.cellSelector.col.end(),
|
||||
rowStart: this.cellSelector.row.start(),
|
||||
rowEnd: this.cellSelector.row.end(),
|
||||
};
|
||||
|
||||
const steps = this._stepsToContent(type, direction, selection, maxVal);
|
||||
if (steps > 0) { this._shiftSelect(direction * steps, selectObs, type, maxVal); }
|
||||
}
|
||||
|
||||
GridView.prototype._stepsToContent = function (type, direction, selection, maxVal) {
|
||||
const {colEnd: colEnd, rowEnd: rowEnd} = selection;
|
||||
let selectionData;
|
||||
|
||||
const cursorCol = this.cursor.fieldIndex();
|
||||
const cursorRow = this.cursor.rowIndex();
|
||||
|
||||
if (type === selector.ROW && direction > 0) {
|
||||
if (colEnd + 1 > maxVal) { return 0; }
|
||||
|
||||
selectionData = this._selectionData({colStart: colEnd, colEnd: maxVal, rowStart: cursorRow, rowEnd: cursorRow});
|
||||
} else if (type === selector.ROW && direction < 0) {
|
||||
if (colEnd - 1 < 0) { return 0; }
|
||||
|
||||
selectionData = this._selectionData({colStart: 0, colEnd, rowStart: cursorRow, rowEnd: cursorRow});
|
||||
} else if (type === selector.COL && direction > 0) {
|
||||
if (rowEnd + 1 > maxVal) { return 0; }
|
||||
|
||||
selectionData = this._selectionData({colStart: cursorCol, colEnd: cursorCol, rowStart: rowEnd, rowEnd: maxVal});
|
||||
} else if (type === selector.COL && direction < 0) {
|
||||
if (rowEnd - 1 > maxVal) { return 0; }
|
||||
|
||||
selectionData = this._selectionData({colStart: cursorCol, colEnd: cursorCol, rowStart: 0, rowEnd});
|
||||
}
|
||||
|
||||
const {fields, rowIndices} = selectionData;
|
||||
if (type === selector.ROW && direction < 0) {
|
||||
// When moving selection left, we step through fields in reverse order.
|
||||
fields.reverse();
|
||||
}
|
||||
if (type === selector.COL && direction < 0) {
|
||||
// When moving selection up, we step through rows in reverse order.
|
||||
rowIndices.reverse();
|
||||
}
|
||||
|
||||
const colValuesByIndex = {};
|
||||
for (const field of fields) {
|
||||
const displayColId = field.displayColModel.peek().colId.peek();
|
||||
colValuesByIndex[field._index()] = this.tableModel.tableData.getColValues(displayColId);
|
||||
}
|
||||
|
||||
let steps = 0;
|
||||
|
||||
if (type === selector.ROW) {
|
||||
const rowIndex = rowIndices[0];
|
||||
const isLastColEmpty = this._isCellValueEmpty(colValuesByIndex[colEnd][rowIndex]);
|
||||
const isNextColEmpty = this._isCellValueEmpty(colValuesByIndex[colEnd + direction][rowIndex]);
|
||||
const shouldStopOnEmptyValue = !isLastColEmpty && !isNextColEmpty;
|
||||
for (let i = 1; i < fields.length; i++) {
|
||||
const hasEmptyValues = this._isCellValueEmpty(colValuesByIndex[fields[i]._index()][rowIndex]);
|
||||
if (hasEmptyValues && shouldStopOnEmptyValue) {
|
||||
return steps;
|
||||
} else if (!hasEmptyValues && !shouldStopOnEmptyValue) {
|
||||
return steps + 1;
|
||||
}
|
||||
|
||||
steps += 1;
|
||||
}
|
||||
} else {
|
||||
const colValues = colValuesByIndex[fields[0]._index()];
|
||||
const isLastRowEmpty = this._isCellValueEmpty(colValues[rowIndices[0]]);
|
||||
const isNextRowEmpty = this._isCellValueEmpty(colValues[rowIndices[1]]);
|
||||
const shouldStopOnEmptyValue = !isLastRowEmpty && !isNextRowEmpty;
|
||||
for (let i = 1; i < rowIndices.length; i++) {
|
||||
const hasEmptyValues = this._isCellValueEmpty(colValues[rowIndices[i]]);
|
||||
if (hasEmptyValues && shouldStopOnEmptyValue) {
|
||||
return steps;
|
||||
} else if (!hasEmptyValues && !shouldStopOnEmptyValue) {
|
||||
return steps + 1;
|
||||
}
|
||||
|
||||
steps += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
GridView.prototype._selectionData = function({colStart, colEnd, rowStart, rowEnd}) {
|
||||
const fields = [];
|
||||
for (let i = colStart; i <= colEnd; i++) {
|
||||
const field = this.viewSection.viewFields().at(i);
|
||||
if (!field) { continue; }
|
||||
|
||||
fields.push(field);
|
||||
}
|
||||
|
||||
const rowIndices = [];
|
||||
for (let i = rowStart; i <= rowEnd; i++) {
|
||||
const rowId = this.viewData.getRowId(i);
|
||||
if (!rowId) { continue; }
|
||||
|
||||
rowIndices.push(this.tableModel.tableData.getRowIdIndex(rowId));
|
||||
}
|
||||
|
||||
return {fields, rowIndices};
|
||||
}
|
||||
|
||||
GridView.prototype._isCellValueEmpty = function(value) {
|
||||
return value === null || value === undefined || value === '' || value === 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes the provided data at the current cursor.
|
||||
*
|
||||
@ -532,15 +660,15 @@ GridView.prototype.fillSelectionDown = function() {
|
||||
|
||||
|
||||
/**
|
||||
* Returns a GridSelection of the selected rows and cols
|
||||
* Returns a CopySelection of the selected rows and cols
|
||||
* @returns {Object} CopySelection
|
||||
*/
|
||||
GridView.prototype.getSelection = function() {
|
||||
var rowIds = [], fields = [], rowStyle = {}, colStyle = {};
|
||||
var colStart = this.cellSelector.colLower();
|
||||
var colEnd = this.cellSelector.colUpper();
|
||||
var rowStart = this.cellSelector.rowLower();
|
||||
var rowEnd = this.cellSelector.rowUpper();
|
||||
let rowIds = [], fields = [], rowStyle = {}, colStyle = {};
|
||||
let colStart = this.cellSelector.colLower();
|
||||
let colEnd = this.cellSelector.colUpper();
|
||||
let rowStart = this.cellSelector.rowLower();
|
||||
let rowEnd = this.cellSelector.rowUpper();
|
||||
|
||||
// If there is no selection, just copy/paste the cursor cell
|
||||
if (this.cellSelector.isCurrentSelectType(selector.NONE)) {
|
||||
|
@ -50,6 +50,10 @@ export type CommandName =
|
||||
| 'shiftUp'
|
||||
| 'shiftRight'
|
||||
| 'shiftLeft'
|
||||
| 'ctrlShiftDown'
|
||||
| 'ctrlShiftUp'
|
||||
| 'ctrlShiftRight'
|
||||
| 'ctrlShiftLeft'
|
||||
| 'selectAll'
|
||||
| 'copyLink'
|
||||
| 'editField'
|
||||
@ -373,6 +377,22 @@ export const groups: CommendGroupDef[] = [{
|
||||
name: 'shiftLeft',
|
||||
keys: ['Shift+Left'],
|
||||
desc: 'Adds the element to the left of the cursor to the selected range'
|
||||
}, {
|
||||
name: 'ctrlShiftDown',
|
||||
keys: ['Mod+Shift+Down'],
|
||||
desc: 'Adds all elements below the cursor to the selected range'
|
||||
}, {
|
||||
name: 'ctrlShiftUp',
|
||||
keys: ['Mod+Shift+Up'],
|
||||
desc: 'Adds all elements above the cursor to the selected range'
|
||||
}, {
|
||||
name: 'ctrlShiftRight',
|
||||
keys: ['Mod+Shift+Right'],
|
||||
desc: 'Adds all elements to the right of the cursor to the selected range'
|
||||
}, {
|
||||
name: 'ctrlShiftLeft',
|
||||
keys: ['Mod+Shift+Left'],
|
||||
desc: 'Adds all elements to the left of the cursor to the selected range'
|
||||
}, {
|
||||
name: 'selectAll',
|
||||
keys: ['Mod+A'],
|
||||
|
@ -68,6 +68,10 @@ export const ColumnsPatch = t.iface([], {
|
||||
"columns": t.tuple("RecordWithStringId", t.rest(t.array("RecordWithStringId"))),
|
||||
});
|
||||
|
||||
export const ColumnsPut = t.iface([], {
|
||||
"columns": t.tuple("RecordWithStringId", t.rest(t.array("RecordWithStringId"))),
|
||||
});
|
||||
|
||||
export const TablePost = t.iface(["ColumnsPost"], {
|
||||
"id": t.opt("string"),
|
||||
});
|
||||
@ -93,6 +97,7 @@ const exportedTypeSuite: t.ITypeSuite = {
|
||||
MinimalRecord,
|
||||
ColumnsPost,
|
||||
ColumnsPatch,
|
||||
ColumnsPut,
|
||||
TablePost,
|
||||
TablesPost,
|
||||
TablesPatch,
|
||||
|
@ -88,6 +88,10 @@ export interface ColumnsPatch {
|
||||
columns: [RecordWithStringId, ...RecordWithStringId[]]; // at least one column is required
|
||||
}
|
||||
|
||||
export interface ColumnsPut {
|
||||
columns: [RecordWithStringId, ...RecordWithStringId[]]; // at least one column is required
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating tables requires a list of columns.
|
||||
* `fields` is not accepted because it's not generally sensible to set the metadata fields on new tables.
|
||||
|
@ -2,7 +2,14 @@ import {concatenateSummaries, summarizeAction} from "app/common/ActionSummarizer
|
||||
import {createEmptyActionSummary} from "app/common/ActionSummary";
|
||||
import {ApiError, LimitType} from 'app/common/ApiError';
|
||||
import {BrowserSettings} from "app/common/BrowserSettings";
|
||||
import {BulkColValues, ColValues, fromTableDataAction, TableColValues, TableRecordValue} from 'app/common/DocActions';
|
||||
import {
|
||||
BulkColValues,
|
||||
ColValues,
|
||||
fromTableDataAction,
|
||||
TableColValues,
|
||||
TableRecordValue,
|
||||
UserAction
|
||||
} from 'app/common/DocActions';
|
||||
import {isRaisedException} from "app/common/gristTypes";
|
||||
import {buildUrlId, parseUrlId} from "app/common/gristUrls";
|
||||
import {isAffirmative} from "app/common/gutil";
|
||||
@ -94,7 +101,7 @@ type WithDocHandler = (activeDoc: ActiveDoc, req: RequestWithLogin, resp: Respon
|
||||
// Schema validators for api endpoints that creates or updates records.
|
||||
const {
|
||||
RecordsPatch, RecordsPost, RecordsPut,
|
||||
ColumnsPost, ColumnsPatch,
|
||||
ColumnsPost, ColumnsPatch, ColumnsPut,
|
||||
TablesPost, TablesPatch,
|
||||
} = t.createCheckers(DocApiTypesTI, GristDataTI);
|
||||
|
||||
@ -194,16 +201,23 @@ export class DocWorkerApi {
|
||||
}
|
||||
|
||||
async function getTableRecords(
|
||||
activeDoc: ActiveDoc, req: RequestWithLogin, optTableId?: string
|
||||
activeDoc: ActiveDoc, req: RequestWithLogin, opts?: { optTableId?: string; includeHidden?: boolean }
|
||||
): Promise<TableRecordValue[]> {
|
||||
const columnData = await getTableData(activeDoc, req, optTableId);
|
||||
const fieldNames = Object.keys(columnData)
|
||||
.filter(k => !(
|
||||
["id", "manualSort"].includes(k)
|
||||
|| k.startsWith("gristHelper_")
|
||||
));
|
||||
const columnData = await getTableData(activeDoc, req, opts?.optTableId);
|
||||
const fieldNames = Object.keys(columnData).filter((k) => {
|
||||
if (k === "id") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!opts?.includeHidden &&
|
||||
(k === "manualSort" || k.startsWith("gristHelper_"))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return columnData.id.map((id, index) => {
|
||||
const result: TableRecordValue = {id, fields: {}};
|
||||
const result: TableRecordValue = { id, fields: {} };
|
||||
for (const key of fieldNames) {
|
||||
let value = columnData[key][index];
|
||||
if (isRaisedException(value)) {
|
||||
@ -226,7 +240,9 @@ export class DocWorkerApi {
|
||||
// Get the specified table in record-oriented format
|
||||
this._app.get('/api/docs/:docId/tables/:tableId/records', canView,
|
||||
withDoc(async (activeDoc, req, res) => {
|
||||
const records = await getTableRecords(activeDoc, req);
|
||||
const records = await getTableRecords(activeDoc, req,
|
||||
{ includeHidden: isAffirmative(req.query.hidden) }
|
||||
);
|
||||
res.json({records});
|
||||
})
|
||||
);
|
||||
@ -357,8 +373,9 @@ export class DocWorkerApi {
|
||||
this._app.get('/api/docs/:docId/tables/:tableId/columns', canView,
|
||||
withDoc(async (activeDoc, req, res) => {
|
||||
const tableId = req.params.tableId;
|
||||
const includeHidden = isAffirmative(req.query.hidden);
|
||||
const columns = await handleSandboxError('', [],
|
||||
activeDoc.getTableCols(docSessionFromRequest(req), tableId));
|
||||
activeDoc.getTableCols(docSessionFromRequest(req), tableId, includeHidden));
|
||||
res.json({columns});
|
||||
})
|
||||
);
|
||||
@ -366,7 +383,7 @@ export class DocWorkerApi {
|
||||
// Get the tables of the specified document in recordish format
|
||||
this._app.get('/api/docs/:docId/tables', canView,
|
||||
withDoc(async (activeDoc, req, res) => {
|
||||
const records = await getTableRecords(activeDoc, req, "_grist_Tables");
|
||||
const records = await getTableRecords(activeDoc, req, { optTableId: "_grist_Tables" });
|
||||
const tables = records.map((record) => ({
|
||||
id: record.fields.tableId,
|
||||
fields: {
|
||||
@ -395,7 +412,7 @@ export class DocWorkerApi {
|
||||
|
||||
// Returns cleaned metadata for all attachments in /records format.
|
||||
this._app.get('/api/docs/:docId/attachments', canView, withDoc(async (activeDoc, req, res) => {
|
||||
const rawRecords = await getTableRecords(activeDoc, req, "_grist_Attachments");
|
||||
const rawRecords = await getTableRecords(activeDoc, req, { optTableId: "_grist_Attachments" });
|
||||
const records = rawRecords.map(r => ({
|
||||
id: r.id,
|
||||
fields: cleanAttachmentRecord(r.fields as MetaRowRecord<"_grist_Attachments">),
|
||||
@ -681,6 +698,54 @@ export class DocWorkerApi {
|
||||
})
|
||||
);
|
||||
|
||||
// Add or update records given in records format
|
||||
this._app.put('/api/docs/:docId/tables/:tableId/columns', canEdit, validate(ColumnsPut),
|
||||
withDoc(async (activeDoc, req, res) => {
|
||||
const tablesTable = activeDoc.docData!.getMetaTable("_grist_Tables");
|
||||
const columnsTable = activeDoc.docData!.getMetaTable("_grist_Tables_column");
|
||||
const {tableId} = req.params;
|
||||
const tableRef = tablesTable.findMatchingRowId({tableId});
|
||||
if (!tableRef) {
|
||||
throw new ApiError(`Table not found "${tableId}"`, 404);
|
||||
}
|
||||
const body = req.body as Types.ColumnsPut;
|
||||
|
||||
const addActions: UserAction[] = [];
|
||||
const updateActions: UserAction[] = [];
|
||||
const updatedColumnsIds = new Set();
|
||||
|
||||
for (const col of body.columns) {
|
||||
const id = columnsTable.findMatchingRowId({parentId: tableRef, colId: col.id});
|
||||
if (id) {
|
||||
updateActions.push( ['UpdateRecord', '_grist_Tables_column', id, col.fields] );
|
||||
updatedColumnsIds.add( id );
|
||||
} else {
|
||||
addActions.push( ['AddVisibleColumn', tableId, col.id, col.fields] );
|
||||
}
|
||||
}
|
||||
|
||||
const getRemoveAction = async () => {
|
||||
const columns = await handleSandboxError('', [],
|
||||
activeDoc.getTableCols(docSessionFromRequest(req), tableId));
|
||||
const columnsToRemove = columns
|
||||
.map(col => col.fields.colRef as number)
|
||||
.filter(colRef => !updatedColumnsIds.has(colRef));
|
||||
|
||||
return [ 'BulkRemoveRecord', '_grist_Tables_column', columnsToRemove ];
|
||||
};
|
||||
|
||||
const actions = [
|
||||
...(!isAffirmative(req.query.noupdate) ? updateActions : []),
|
||||
...(!isAffirmative(req.query.noadd) ? addActions : []),
|
||||
...(isAffirmative(req.query.replaceall) ? [ await getRemoveAction() ] : [] )
|
||||
];
|
||||
await handleSandboxError(tableId, [],
|
||||
activeDoc.applyUserActions(docSessionFromRequest(req), actions)
|
||||
);
|
||||
res.json(null);
|
||||
})
|
||||
);
|
||||
|
||||
// Add a new webhook and trigger
|
||||
this._app.post('/api/docs/:docId/webhooks', isOwner, validate(WebhookSubscribeCollection),
|
||||
withDoc(async (activeDoc, req, res) => {
|
||||
|
@ -483,7 +483,18 @@
|
||||
"{{count}} unmatched field in import_other": "{{count}} unangepasste Felder im Import",
|
||||
"{{count}} unmatched field_one": "{{count}} unangepasstes Feld",
|
||||
"{{count}} unmatched field_other": "{{count}} unangepasste Felder",
|
||||
"{{count}} unmatched field in import_one": "{{count}} unangepasst Feld im Import"
|
||||
"{{count}} unmatched field in import_one": "{{count}} unangepasst Feld im Import",
|
||||
"Column mapping": "Spaltenzuordnung",
|
||||
"Column Mapping": "Spaltenzuordnung",
|
||||
"Destination table": "Zieltabelle",
|
||||
"Grist column": "Grist Spalte",
|
||||
"Import from file": "Aus Datei importieren",
|
||||
"New Table": "Neue Tabelle",
|
||||
"Revert": "Zurücksetzen",
|
||||
"Skip": "überspringen",
|
||||
"Skip Import": "Import überspringen",
|
||||
"Skip Table on Import": "Tabelle beim Import überspringen",
|
||||
"Source column": "Quellenspalte"
|
||||
},
|
||||
"LeftPanelCommon": {
|
||||
"Help Center": "Hilfe-Center"
|
||||
@ -904,7 +915,10 @@
|
||||
"Open row styles": "Zeilenstile öffnen",
|
||||
"Cell Style": "Zellenstil",
|
||||
"Default cell style": "Standard-Zellenstil",
|
||||
"Mixed style": "Gemischter Stil"
|
||||
"Mixed style": "Gemischter Stil",
|
||||
"Default header style": "Standard Kopfzeilenstil",
|
||||
"Header Style": "Kopfzeilenstil",
|
||||
"HEADER STYLE": "KOPFSTIL"
|
||||
},
|
||||
"DiscussionEditor": {
|
||||
"Resolve": "Beschließen",
|
||||
@ -1127,7 +1141,8 @@
|
||||
"Sign Up for Free": "Melden Sie sich kostenlos an",
|
||||
"Sign up for a free Grist account to start using the Formula AI Assistant.": "Melden Sie sich für ein kostenloses Grist-Konto an, um den KI Formel Assistenten zu verwenden.",
|
||||
"There are some things you should know when working with me:": "Es gibt einige Dinge, die Sie wissen sollten, wenn Sie mit mir arbeiten:",
|
||||
"What do you need help with?": "Womit brauchen Sie Hilfe?"
|
||||
"What do you need help with?": "Womit brauchen Sie Hilfe?",
|
||||
"Formula AI Assistant is only available for logged in users.": "Der Formel-KI-Assistent ist nur für eingeloggte Benutzer verfügbar."
|
||||
},
|
||||
"GridView": {
|
||||
"Click to insert": "Zum Einfügen klicken"
|
||||
|
@ -853,7 +853,8 @@
|
||||
"Mixed style": "Mixed style",
|
||||
"Open row styles": "Open row styles",
|
||||
"Default header style": "Default header style",
|
||||
"Header Style": "Header Style"
|
||||
"Header Style": "Header Style",
|
||||
"HEADER STYLE": "HEADER STYLE"
|
||||
},
|
||||
"ChoiceTextBox": {
|
||||
"CHOICES": "CHOICES"
|
||||
@ -1076,7 +1077,8 @@
|
||||
"Sign Up for Free": "Sign Up for Free",
|
||||
"Sign up for a free Grist account to start using the Formula AI Assistant.": "Sign up for a free Grist account to start using the Formula AI Assistant.",
|
||||
"There are some things you should know when working with me:": "There are some things you should know when working with me:",
|
||||
"What do you need help with?": "What do you need help with?"
|
||||
"What do you need help with?": "What do you need help with?",
|
||||
"Formula AI Assistant is only available for logged in users.": "Formula AI Assistant is only available for logged in users."
|
||||
},
|
||||
"GridView": {
|
||||
"Click to insert": "Click to insert"
|
||||
|
@ -776,7 +776,7 @@
|
||||
"Unmark On-Demand": "Desmarcar bajo demanda"
|
||||
},
|
||||
"FilterBar": {
|
||||
"SearchColumns": "Buscar columnas",
|
||||
"SearchColumns": "Buscar en columnas",
|
||||
"Search Columns": "Buscar columnas"
|
||||
},
|
||||
"Importer": {
|
||||
@ -786,7 +786,18 @@
|
||||
"{{count}} unmatched field in import_one": "{{count}} campo no coincide con la importación",
|
||||
"{{count}} unmatched field_one": "{{count}} campo sin equivalente",
|
||||
"{{count}} unmatched field_other": "{{count}} campos sin equivalentes",
|
||||
"{{count}} unmatched field in import_other": "{{count}} campos sin equivalente en la importación"
|
||||
"{{count}} unmatched field in import_other": "{{count}} campos sin equivalente en la importación",
|
||||
"Column Mapping": "Asignación de columnas",
|
||||
"Column mapping": "Asignación de las columnas",
|
||||
"Import from file": "Importar desde un archivo",
|
||||
"New Table": "Tabla nueva",
|
||||
"Revert": "Revertir",
|
||||
"Skip": "Omitir",
|
||||
"Skip Import": "Omitir la importación",
|
||||
"Source column": "Columna de origen",
|
||||
"Destination table": "Tabla de destino",
|
||||
"Grist column": "Columna Grist",
|
||||
"Skip Table on Import": "Omitir la tabla en la importación"
|
||||
},
|
||||
"PermissionsWidget": {
|
||||
"Allow All": "Permitir todo",
|
||||
@ -894,7 +905,10 @@
|
||||
"Default cell style": "Estilo de celda predeterminado",
|
||||
"CELL STYLE": "ESTILO DE CELDA",
|
||||
"Open row styles": "Abrir estilos de fila",
|
||||
"Mixed style": "Estilo mixto"
|
||||
"Mixed style": "Estilo mixto",
|
||||
"Header Style": "Estilo del encabezado",
|
||||
"Default header style": "Estilo del encabezado por defecto",
|
||||
"HEADER STYLE": "ESTILO DEL ENCABEZADO"
|
||||
},
|
||||
"ColumnInfo": {
|
||||
"Cancel": "Cancelar",
|
||||
@ -1117,7 +1131,8 @@
|
||||
"Hi, I'm the Grist Formula AI Assistant.": "Hola, soy el Asistente de IA de Fórmula Grist.",
|
||||
"I can only help with formulas. I cannot build tables, columns, and views, or write access rules.": "Sólo puedo ayudar con fórmulas. No puedo construir tablas, columnas y vistas, ni escribir reglas de acceso.",
|
||||
"Sign Up for Free": "Regístrate gratis",
|
||||
"There are some things you should know when working with me:": "Hay algunas cosas que debes saber cuando trabajes conmigo:"
|
||||
"There are some things you should know when working with me:": "Hay algunas cosas que debes saber cuando trabajes conmigo:",
|
||||
"Formula AI Assistant is only available for logged in users.": "Formula AI Assistant sólo está disponible para usuarios registrados."
|
||||
},
|
||||
"GridView": {
|
||||
"Click to insert": "Haga clic para insertar"
|
||||
|
@ -483,7 +483,18 @@
|
||||
"{{count}} unmatched field_other": "{{count}} campos sem equivalente",
|
||||
"{{count}} unmatched field in import_other": "{{count}} campos sem equivalente na importação",
|
||||
"{{count}} unmatched field in import_one": "{{count}} campo sem equivalente na importação",
|
||||
"{{count}} unmatched field_one": "{{count}} campo sem equivalente"
|
||||
"{{count}} unmatched field_one": "{{count}} campo sem equivalente",
|
||||
"Column Mapping": "Mapeamento de coluna",
|
||||
"Column mapping": "Mapeamento de coluna",
|
||||
"Destination table": "Tabela de destino",
|
||||
"Grist column": "Coluna de Grist",
|
||||
"New Table": "Nova tabela",
|
||||
"Revert": "Reverter",
|
||||
"Skip": "Pular",
|
||||
"Skip Import": "Pular importação",
|
||||
"Source column": "Coluna de origem",
|
||||
"Import from file": "Importar de arquivo",
|
||||
"Skip Table on Import": "Pular tabela na importação"
|
||||
},
|
||||
"LeftPanelCommon": {
|
||||
"Help Center": "Centro de Ajuda"
|
||||
@ -996,7 +1007,10 @@
|
||||
"Cell Style": "Estilo de célula",
|
||||
"Default cell style": "Estilo de célula padrão",
|
||||
"Mixed style": "Estilo misto",
|
||||
"Open row styles": "Estilos de linha aberta"
|
||||
"Open row styles": "Estilos de linha aberta",
|
||||
"Default header style": "Estilo de cabeçalho padrão",
|
||||
"Header Style": "Estilo de cabeçalho",
|
||||
"HEADER STYLE": "ESTILO DE CABEÇALHO"
|
||||
},
|
||||
"ColumnEditor": {
|
||||
"COLUMN DESCRIPTION": "DESCRIÇÃO DA COLUNA",
|
||||
@ -1127,7 +1141,8 @@
|
||||
"Sign up for a free Grist account to start using the Formula AI Assistant.": "Cadastre-se para uma conta gratuita do Grist para começar a usar o Assistente de Fórmula AI.",
|
||||
"What do you need help with?": "Em que você precisa de ajuda?",
|
||||
"There are some things you should know when working with me:": "Há algumas coisas que você deve saber ao trabalhar comigo:",
|
||||
"Learn more": "Saiba mais"
|
||||
"Learn more": "Saiba mais",
|
||||
"Formula AI Assistant is only available for logged in users.": "O Assistente de Fórmula de IA só está disponível para usuários registrados."
|
||||
},
|
||||
"GridView": {
|
||||
"Click to insert": "Clique para inserir"
|
||||
|
@ -549,7 +549,18 @@
|
||||
"{{count}} unmatched field in import_other": "{{count}} несовпадающие поля при импорте",
|
||||
"{{count}} unmatched field in import_one": "{{count}} несовпадающее поле при импорте",
|
||||
"{{count}} unmatched field_one": "{{count}} несовпадающее поле",
|
||||
"{{count}} unmatched field_other": "{{count}} несовпадающие поля"
|
||||
"{{count}} unmatched field_other": "{{count}} несовпадающие поля",
|
||||
"Column Mapping": "Сопоставление столбцов",
|
||||
"Column mapping": "Сопоставление столбцов",
|
||||
"Destination table": "Таблица назначения",
|
||||
"Grist column": "Grist столбец",
|
||||
"Import from file": "Импорт из файла",
|
||||
"New Table": "Новая таблица",
|
||||
"Revert": "Восстановить",
|
||||
"Skip": "Пропустить",
|
||||
"Skip Table on Import": "Пропустить Таблицу при импорте",
|
||||
"Source column": "Столбец Источника",
|
||||
"Skip Import": "Пропустить импорт"
|
||||
},
|
||||
"LeftPanelCommon": {
|
||||
"Help Center": "Справочный центр"
|
||||
@ -757,7 +768,10 @@
|
||||
"Default cell style": "Стиль ячейки по умолчанию",
|
||||
"Open row styles": "Открыть форматы строк",
|
||||
"Cell Style": "Стиль ячейки",
|
||||
"Mixed style": "Смешанный стиль"
|
||||
"Mixed style": "Смешанный стиль",
|
||||
"Default header style": "Стиль заголовка по умолчанию",
|
||||
"HEADER STYLE": "СТИЛЬ ЗАГОЛОВКА",
|
||||
"Header Style": "Стиль заголовка"
|
||||
},
|
||||
"TypeTransform": {
|
||||
"Cancel": "Отмена",
|
||||
@ -1063,7 +1077,8 @@
|
||||
"Sign up for a free Grist account to start using the Formula AI Assistant.": "Зарегистрируйте бесплатную учетную запись Grist, чтобы начать использовать AI Помощника по формулам.",
|
||||
"There are some things you should know when working with me:": "Есть некоторые вещи, которые вы должны знать, работая со мной:",
|
||||
"What do you need help with?": "Как я могу вам помочь?",
|
||||
"Hi, I'm the Grist Formula AI Assistant.": "Привет, я AI Помощник по формулам Grist."
|
||||
"Hi, I'm the Grist Formula AI Assistant.": "Привет, я AI Помощник по формулам Grist.",
|
||||
"Formula AI Assistant is only available for logged in users.": "AI Ассистент для формул доступен только для зарегистрированных пользователей."
|
||||
},
|
||||
"GridView": {
|
||||
"Click to insert": "Нажмите для вставки"
|
||||
|
BIN
test/fixtures/docs/ShiftSelection.grist
vendored
Normal file
BIN
test/fixtures/docs/ShiftSelection.grist
vendored
Normal file
Binary file not shown.
213
test/nbrowser/ShiftSelection.ts
Normal file
213
test/nbrowser/ShiftSelection.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import {assert, driver, Key, WebElement} from 'mocha-webdriver';
|
||||
import * as gu from 'test/nbrowser/gristUtils';
|
||||
import {setupTestSuite} from 'test/nbrowser/testUtils';
|
||||
|
||||
interface CellSelection {
|
||||
/** 0-based column index. */
|
||||
colStart: number;
|
||||
|
||||
/** 0-based column index. */
|
||||
colEnd: number;
|
||||
|
||||
/** 0-based row index. */
|
||||
rowStart: number;
|
||||
|
||||
/** 0-based row index. */
|
||||
rowEnd: number;
|
||||
}
|
||||
|
||||
interface SelectionRange {
|
||||
/** 0-based index. */
|
||||
start: number;
|
||||
|
||||
/** 0-based index. */
|
||||
end: number;
|
||||
}
|
||||
|
||||
describe('ShiftSelection', function () {
|
||||
this.timeout(20000);
|
||||
const cleanup = setupTestSuite();
|
||||
gu.bigScreen();
|
||||
|
||||
before(async function() {
|
||||
const session = await gu.session().personalSite.login();
|
||||
await session.tempDoc(cleanup, 'ShiftSelection.grist');
|
||||
});
|
||||
|
||||
async function getSelectionRange(parent: WebElement, selector: string): Promise<SelectionRange|undefined> {
|
||||
let start, end;
|
||||
|
||||
let selectionActive = false;
|
||||
const elements = await parent.findAll(selector);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i];
|
||||
const isSelected = await element.matches('.selected');
|
||||
|
||||
if (isSelected && !selectionActive) {
|
||||
start = i;
|
||||
selectionActive = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isSelected && selectionActive) {
|
||||
end = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (start === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (end === undefined) {
|
||||
end = elements.length - 1;
|
||||
}
|
||||
|
||||
return {
|
||||
start: start,
|
||||
end: end,
|
||||
};
|
||||
}
|
||||
|
||||
async function getSelectedCells(): Promise<CellSelection|undefined> {
|
||||
const activeSection = await driver.find('.active_section');
|
||||
|
||||
const colSelection = await getSelectionRange(activeSection, '.column_names .column_name');
|
||||
if (!colSelection) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rowSelection = await getSelectionRange(activeSection, '.gridview_row .gridview_data_row_num');
|
||||
if (!rowSelection) {
|
||||
// Edge case if only a cell in the "new" row is selected
|
||||
// Not relevant for our tests
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
colStart: colSelection.start,
|
||||
colEnd: colSelection.end,
|
||||
rowStart: rowSelection.start,
|
||||
rowEnd: rowSelection.end,
|
||||
};
|
||||
}
|
||||
|
||||
async function assertCellSelection(expected: CellSelection|undefined) {
|
||||
const currentSelection = await getSelectedCells();
|
||||
assert.deepEqual(currentSelection, expected);
|
||||
}
|
||||
|
||||
|
||||
it('Shift+Up extends the selection up', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 0, rowEnd: 1});
|
||||
});
|
||||
|
||||
it('Shift+Down extends the selection down', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.DOWN));
|
||||
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 1, rowEnd: 2});
|
||||
});
|
||||
|
||||
it('Shift+Left extends the selection left', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.LEFT));
|
||||
await assertCellSelection({colStart: 0, colEnd: 1, rowStart: 1, rowEnd: 1});
|
||||
});
|
||||
|
||||
it('Shift+Right extends the selection right', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.RIGHT));
|
||||
await assertCellSelection({colStart: 1, colEnd: 2, rowStart: 1, rowEnd: 1});
|
||||
});
|
||||
|
||||
it('Shift+Right + Shift+Left leads to the initial selection', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.RIGHT));
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.LEFT));
|
||||
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 1, rowEnd: 1});
|
||||
});
|
||||
|
||||
it('Shift+Up + Shift+Down leads to the initial selection', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.UP));
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.DOWN));
|
||||
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 1, rowEnd: 1});
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+Up extends the selection blockwise up', async function () {
|
||||
await gu.getCell(5, 7).click();
|
||||
|
||||
const ctrlKey = await gu.modKey();
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 6});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 2, rowEnd: 6});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 0, rowEnd: 6});
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+Down extends the selection blockwise down', async function () {
|
||||
await gu.getCell(5, 5).click();
|
||||
|
||||
const ctrlKey = await gu.modKey();
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.DOWN));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 6});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.DOWN));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 8});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.DOWN));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 10});
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+Left extends the selection blockwise left', async function () {
|
||||
await gu.getCell(6, 5).click();
|
||||
|
||||
const ctrlKey = await gu.modKey();
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.LEFT));
|
||||
await assertCellSelection({colStart: 4, colEnd: 6, rowStart: 4, rowEnd: 4});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.LEFT));
|
||||
await assertCellSelection({colStart: 2, colEnd: 6, rowStart: 4, rowEnd: 4});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.LEFT));
|
||||
await assertCellSelection({colStart: 0, colEnd: 6, rowStart: 4, rowEnd: 4});
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+Right extends the selection blockwise right', async function () {
|
||||
await gu.getCell(4, 5).click();
|
||||
|
||||
const ctrlKey = await gu.modKey();
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.RIGHT));
|
||||
await assertCellSelection({colStart: 4, colEnd: 6, rowStart: 4, rowEnd: 4});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.RIGHT));
|
||||
await assertCellSelection({colStart: 4, colEnd: 8, rowStart: 4, rowEnd: 4});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.RIGHT));
|
||||
await assertCellSelection({colStart: 4, colEnd: 10, rowStart: 4, rowEnd: 4});
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+* extends the selection until all the next cells are empty', async function () {
|
||||
await gu.getCell(3, 7).click();
|
||||
|
||||
const ctrlKey = await gu.modKey();
|
||||
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.RIGHT));
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 3, colEnd: 4, rowStart: 2, rowEnd: 6});
|
||||
|
||||
await gu.getCell(4, 7).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.LEFT));
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 3, colEnd: 4, rowStart: 4, rowEnd: 6});
|
||||
});
|
||||
});
|
@ -5,7 +5,7 @@ import {arrayRepeat} from 'app/common/gutil';
|
||||
import {WebhookSummary} from 'app/common/Triggers';
|
||||
import {DocAPI, DocState, UserAPIImpl} from 'app/common/UserAPI';
|
||||
import {testDailyApiLimitFeatures} from 'app/gen-server/entity/Product';
|
||||
import {AddOrUpdateRecord, Record as ApiRecord} from 'app/plugin/DocApiTypes';
|
||||
import {AddOrUpdateRecord, Record as ApiRecord, ColumnsPut, RecordWithStringId} from 'app/plugin/DocApiTypes';
|
||||
import {CellValue, GristObjCode} from 'app/plugin/GristData';
|
||||
import {
|
||||
applyQueryParameters,
|
||||
@ -443,6 +443,26 @@ function testDocApi() {
|
||||
});
|
||||
});
|
||||
|
||||
it('GET /docs/{did}/tables/{tid}/records honors the "hidden" param', async function () {
|
||||
const params = { hidden: true };
|
||||
const resp = await axios.get(
|
||||
`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/records`,
|
||||
{...chimpy, params }
|
||||
);
|
||||
assert.equal(resp.status, 200);
|
||||
assert.deepEqual(resp.data.records[0], {
|
||||
id: 1,
|
||||
fields: {
|
||||
manualSort: 1,
|
||||
A: 'hello',
|
||||
B: '',
|
||||
C: '',
|
||||
D: null,
|
||||
E: 'HELLO',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("GET /docs/{did}/tables/{tid}/records handles errors and hidden columns", async function () {
|
||||
let resp = await axios.get(`${serverUrl}/api/docs/${docIds.ApiDataRecordsTest}/tables/Table1/records`, chimpy);
|
||||
assert.equal(resp.status, 200);
|
||||
@ -610,6 +630,21 @@ function testDocApi() {
|
||||
);
|
||||
});
|
||||
|
||||
it('GET /docs/{did}/tables/{tid}/columns retrieves hidden columns when "hidden" is set', async function () {
|
||||
const params = { hidden: true };
|
||||
const resp = await axios.get(
|
||||
`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/columns`,
|
||||
{ ...chimpy, params }
|
||||
);
|
||||
assert.equal(resp.status, 200);
|
||||
const columnsMap = new Map(resp.data.columns.map(({id, fields}: {id: string, fields: object}) => [id, fields]));
|
||||
assert.include([...columnsMap.keys()], "manualSort");
|
||||
assert.deepInclude(columnsMap.get("manualSort"), {
|
||||
colRef: 1,
|
||||
type: 'ManualSortPos',
|
||||
});
|
||||
});
|
||||
|
||||
it("GET/POST/PATCH /docs/{did}/tables and /columns", async function () {
|
||||
// POST /tables: Create new tables
|
||||
let resp = await axios.post(`${serverUrl}/api/docs/${docIds.Timesheets}/tables`, {
|
||||
@ -842,6 +877,126 @@ function testDocApi() {
|
||||
assert.equal(resp.status, 200);
|
||||
});
|
||||
|
||||
describe("PUT /docs/{did}/columns", function () {
|
||||
|
||||
async function generateDocAndUrl() {
|
||||
const wid = (await userApi.getOrgWorkspaces('current')).find((w) => w.name === 'Private')!.id;
|
||||
const docId = await userApi.newDoc({name: 'ColumnsPut'}, wid);
|
||||
const url = `${serverUrl}/api/docs/${docId}/tables/Table1/columns`;
|
||||
return { url, docId };
|
||||
}
|
||||
|
||||
async function getColumnFieldsMapById(url: string, params: any) {
|
||||
const result = await axios.get(url, {...chimpy, params});
|
||||
assert.equal(result.status, 200);
|
||||
return new Map<string, object>(
|
||||
result.data.columns.map(
|
||||
({id, fields}: {id: string, fields: object}) => [id, fields]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function checkPut(
|
||||
columns: [RecordWithStringId, ...RecordWithStringId[]],
|
||||
params: Record<string, any>,
|
||||
expectedFieldsByColId: Record<string, object>,
|
||||
opts?: { getParams?: any }
|
||||
) {
|
||||
const {url} = await generateDocAndUrl();
|
||||
const body: ColumnsPut = { columns };
|
||||
const resp = await axios.put(url, body, {...chimpy, params});
|
||||
assert.equal(resp.status, 200);
|
||||
const fieldsByColId = await getColumnFieldsMapById(url, opts?.getParams);
|
||||
|
||||
assert.deepEqual(
|
||||
[...fieldsByColId.keys()],
|
||||
Object.keys(expectedFieldsByColId),
|
||||
"The updated table should have the expected columns"
|
||||
);
|
||||
|
||||
for (const [colId, expectedFields] of Object.entries(expectedFieldsByColId)) {
|
||||
assert.deepInclude(fieldsByColId.get(colId), expectedFields);
|
||||
}
|
||||
}
|
||||
|
||||
const COLUMN_TO_ADD = {
|
||||
id: "Foo",
|
||||
fields: {
|
||||
type: "Text",
|
||||
label: "FooLabel",
|
||||
}
|
||||
};
|
||||
|
||||
const COLUMN_TO_UPDATE = {
|
||||
id: "A",
|
||||
fields: {
|
||||
type: "Numeric",
|
||||
colId: "NewA"
|
||||
}
|
||||
};
|
||||
|
||||
it('should create new columns', async function () {
|
||||
await checkPut([COLUMN_TO_ADD], {}, {
|
||||
A: {}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
|
||||
});
|
||||
});
|
||||
|
||||
it('should update existing columns and create new ones', async function () {
|
||||
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {}, {
|
||||
NewA: {type: "Numeric", label: "A"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
|
||||
});
|
||||
});
|
||||
|
||||
it('should only update existing columns when noadd is set', async function () {
|
||||
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noadd: "1"}, {
|
||||
NewA: {type: "Numeric"}, B: {}, C: {}
|
||||
});
|
||||
});
|
||||
|
||||
it('should only add columns when noupdate is set', async function () {
|
||||
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noupdate: "1"}, {
|
||||
A: {type: "Any"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove existing columns if replaceall is set', async function () {
|
||||
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
|
||||
NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT remove hidden columns even when replaceall is set', async function () {
|
||||
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
|
||||
manualSort: {type: "ManualSortPos"}, NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
|
||||
}, { getParams: { hidden: true } });
|
||||
});
|
||||
|
||||
it('should forbid update by viewers', async function () {
|
||||
// given
|
||||
const { url, docId } = await generateDocAndUrl();
|
||||
await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}});
|
||||
|
||||
// when
|
||||
const resp = await axios.put(url, { columns: [ COLUMN_TO_ADD ] }, kiwi);
|
||||
|
||||
// then
|
||||
assert.equal(resp.status, 403);
|
||||
});
|
||||
|
||||
it("should return 404 when table is not found", async function() {
|
||||
// given
|
||||
const { url } = await generateDocAndUrl();
|
||||
const notFoundUrl = url.replace("Table1", "NonExistingTable");
|
||||
|
||||
// when
|
||||
const resp = await axios.put(notFoundUrl, { columns: [ COLUMN_TO_ADD ] }, chimpy);
|
||||
|
||||
// then
|
||||
assert.equal(resp.status, 404);
|
||||
assert.equal(resp.data.error, 'Table not found "NonExistingTable"');
|
||||
});
|
||||
});
|
||||
|
||||
it("GET /docs/{did}/tables/{tid}/data returns 404 for non-existent doc", async function () {
|
||||
const resp = await axios.get(`${serverUrl}/api/docs/typotypotypo/tables/Table1/data`, chimpy);
|
||||
assert.equal(resp.status, 404);
|
||||
|
Loading…
Reference in New Issue
Block a user