(core) updates from grist-core

pull/695/head
Paul Fitzpatrick 7 months ago
commit f32563e8fb

@ -85,7 +85,7 @@ import {AccessTokenOptions, AccessTokenResult, GristDocAPI, UIRowId} from 'app/p
import {compileAclFormula} from 'app/server/lib/ACLFormula';
import {AssistanceSchemaPromptV1Context} from 'app/server/lib/Assistance';
import {AssistanceContext} from 'app/common/AssistancePrompts';
import {Authorizer} from 'app/server/lib/Authorizer';
import {Authorizer, RequestWithLogin} from 'app/server/lib/Authorizer';
import {checksumFile} from 'app/server/lib/checksumFile';
import {Client} from 'app/server/lib/Client';
import {DEFAULT_CACHE_TTL, DocManager} from 'app/server/lib/DocManager';
@ -141,6 +141,7 @@ import remove = require('lodash/remove');
import sum = require('lodash/sum');
import without = require('lodash/without');
import zipObject = require('lodash/zipObject');
import { getMetaTables } from './DocApi';
bluebird.promisifyAll(tmp);
@ -2791,7 +2792,6 @@ export function tableIdToRef(metaTables: { [p: string]: TableDataAction }, table
// Helper that converts a Grist column colId to a ref given the corresponding table.
export function colIdToRef(metaTables: {[p: string]: TableDataAction}, tableId: string, colId: string) {
const tableRef = tableIdToRef(metaTables, tableId);
const [, , colRefs, columnData] = metaTables._grist_Tables_column;
@ -2804,6 +2804,31 @@ export function colIdToRef(metaTables: {[p: string]: TableDataAction}, tableId:
return colRefs[colRowIndex];
}
// Helper that check if tableRef is used instead of tableId and return real tableId
// If metaTables is not define, activeDoc and req allow it to be created
interface MetaTables {
metaTables: { [p: string]: TableDataAction }
}
interface ActiveDocAndReq {
activeDoc: ActiveDoc, req: RequestWithLogin
}
export async function getRealTableId(
tableId: string,
options: MetaTables | ActiveDocAndReq
): Promise<string> {
if (parseInt(tableId)) {
const metaTables = "metaTables" in options
? options.metaTables
: await getMetaTables(options.activeDoc, options.req);
const [, , tableRefs, tableData] = metaTables._grist_Tables;
if (tableRefs.indexOf(parseInt(tableId)) >= 0) {
const tableRowIndex = tableRefs.indexOf(parseInt(tableId));
return tableData.tableId[tableRowIndex]!.toString();
}
}
return tableId;
}
export function sanitizeApplyUAOptions(options?: ApplyUAOptions): ApplyUAOptions {
return pick(options||{}, ['desc', 'otherId', 'linkId', 'parseStrings']);
}

@ -32,7 +32,7 @@ import {
TableOperationsImpl,
TableOperationsPlatform
} from 'app/plugin/TableOperationsImpl';
import {ActiveDoc, colIdToRef as colIdToReference, tableIdToRef} from "app/server/lib/ActiveDoc";
import {ActiveDoc, colIdToRef as colIdToReference, getRealTableId, tableIdToRef} from "app/server/lib/ActiveDoc";
import {appSettings} from "app/server/lib/AppSettings";
import {sendForCompletion} from 'app/server/lib/Assistance';
import {
@ -201,7 +201,7 @@ export class DocWorkerApi {
if (!Object.keys(filters).every(col => Array.isArray(filters[col]))) {
throw new ApiError("Invalid query: filter values must be arrays", 400);
}
const tableId = optTableId || req.params.tableId;
const tableId = await getRealTableId(optTableId || req.params.tableId, {activeDoc, req});
const session = docSessionFromRequest(req);
const {tableData} = await handleSandboxError(tableId, [], activeDoc.fetchQuery(
session, {tableId, filters}, !immediate));
@ -262,11 +262,6 @@ export class DocWorkerApi {
})
);
async function getMetaTables(activeDoc: ActiveDoc, req: RequestWithLogin) {
return await handleSandboxError("", [],
activeDoc.fetchMetaTables(docSessionFromRequest(req)));
}
const registerWebhook = async (activeDoc: ActiveDoc, req: RequestWithLogin, webhook: WebhookFields) => {
const {fields, url} = await getWebhookSettings(activeDoc, req, null, webhook);
if (!fields.eventTypes?.length) {
@ -337,7 +332,8 @@ export class DocWorkerApi {
const trigger = webhookId ? activeDoc.triggers.getWebhookTriggerRecord(webhookId) : undefined;
let currentTableId = trigger ? tablesTable.getValue(trigger.tableRef, 'tableId')! : undefined;
const {url, eventTypes, isReadyColumn, name} = webhook;
const tableId = req.params.tableId || webhook.tableId;
const tableId = await getRealTableId(req.params.tableId || webhook.tableId, {metaTables});
const fields: Partial<SchemaTypes['_grist_Triggers']> = {};
if (url && !isUrlAllowed(url)) {
@ -387,7 +383,7 @@ export class DocWorkerApi {
// Get the columns of the specified table in recordish format
this._app.get('/api/docs/:docId/tables/:tableId/columns', canView,
withDoc(async (activeDoc, req, res) => {
const tableId = req.params.tableId;
const tableId = await getRealTableId(req.params.tableId, {activeDoc, req});
const includeHidden = isAffirmative(req.query.hidden);
const columns = await handleSandboxError('', [],
activeDoc.getTableCols(docSessionFromRequest(req), tableId, includeHidden));
@ -498,7 +494,7 @@ export class DocWorkerApi {
withDoc(async (activeDoc, req, res) => {
const colValues = req.body as BulkColValues;
const count = colValues[Object.keys(colValues)[0]].length;
const op = getTableOperations(req, activeDoc);
const op = await getTableOperations(req, activeDoc);
const ids = await op.addRecords(count, colValues);
res.json(ids);
})
@ -527,7 +523,7 @@ export class DocWorkerApi {
}
}
validateCore(RecordsPost, req, body);
const ops = getTableOperations(req, activeDoc);
const ops = await getTableOperations(req, activeDoc);
const records = await ops.create(body.records);
res.json({records});
})
@ -558,7 +554,7 @@ export class DocWorkerApi {
this._app.post('/api/docs/:docId/tables/:tableId/columns', canEdit, validate(ColumnsPost),
withDoc(async (activeDoc, req, res) => {
const body = req.body as Types.ColumnsPost;
const {tableId} = req.params;
const tableId = await getRealTableId(req.params.tableId, {activeDoc, req});
const actions = body.columns.map(({fields, id: colId}) =>
// AddVisibleColumn adds the column to all widgets of the table.
// This isn't necessarily what the user wants, but it seems like a good default.
@ -590,7 +586,7 @@ export class DocWorkerApi {
this._app.post('/api/docs/:docId/tables/:tableId/data/delete', canEdit, withDoc(async (activeDoc, req, res) => {
const rowIds = req.body;
const op = getTableOperations(req, activeDoc);
const op = await getTableOperations(req, activeDoc);
await op.destroy(rowIds);
res.json(null);
}));
@ -659,7 +655,7 @@ export class DocWorkerApi {
const rowIds = columnValues.id;
// sandbox expects no id column
delete columnValues.id;
const ops = getTableOperations(req, activeDoc);
const ops = await getTableOperations(req, activeDoc);
await ops.updateRecords(columnValues, rowIds);
res.json(null);
})
@ -669,7 +665,7 @@ export class DocWorkerApi {
this._app.patch('/api/docs/:docId/tables/:tableId/records', canEdit, validate(RecordsPatch),
withDoc(async (activeDoc, req, res) => {
const body = req.body as Types.RecordsPatch;
const ops = getTableOperations(req, activeDoc);
const ops = await getTableOperations(req, activeDoc);
await ops.update(body.records);
res.json(null);
})
@ -680,7 +676,7 @@ export class DocWorkerApi {
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 tableId = await getRealTableId(req.params.tableId, {activeDoc, req});
const tableRef = tablesTable.findMatchingRowId({tableId});
if (!tableRef) {
throw new ApiError(`Table not found "${tableId}"`, 404);
@ -693,7 +689,7 @@ export class DocWorkerApi {
}
return {...col, id};
});
const ops = getTableOperations(req, activeDoc, "_grist_Tables_column");
const ops = await getTableOperations(req, activeDoc, "_grist_Tables_column");
await ops.update(columns);
res.json(null);
})
@ -711,7 +707,7 @@ export class DocWorkerApi {
}
return {...table, id};
});
const ops = getTableOperations(req, activeDoc, "_grist_Tables");
const ops = await getTableOperations(req, activeDoc, "_grist_Tables");
await ops.update(tables);
res.json(null);
})
@ -720,7 +716,7 @@ export class DocWorkerApi {
// Add or update records given in records format
this._app.put('/api/docs/:docId/tables/:tableId/records', canEdit, validate(RecordsPut),
withDoc(async (activeDoc, req, res) => {
const ops = getTableOperations(req, activeDoc);
const ops = await getTableOperations(req, activeDoc);
const body = req.body as Types.RecordsPut;
const options = {
add: !isAffirmative(req.query.noadd),
@ -740,7 +736,7 @@ export class DocWorkerApi {
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 tableId = await getRealTableId(req.params.tableId, {activeDoc, req});
const tableRef = tablesTable.findMatchingRowId({tableId});
if (!tableRef) {
throw new ApiError(`Table not found "${tableId}"`, 404);
@ -785,7 +781,8 @@ export class DocWorkerApi {
this._app.delete('/api/docs/:docId/tables/:tableId/columns/:colId', canEdit,
withDoc(async (activeDoc, req, res) => {
const {tableId, colId} = req.params;
const {colId} = req.params;
const tableId = await getRealTableId(req.params.tableId, {activeDoc, req});
const actions = [ [ 'RemoveColumn', tableId, colId ] ];
await handleSandboxError(tableId, [colId],
activeDoc.applyUserActions(docSessionFromRequest(req), actions)
@ -1205,12 +1202,13 @@ export class DocWorkerApi {
this._app.get('/api/docs/:docId/download/xlsx', canView, withDoc(async (activeDoc, req, res) => {
// Query DB for doc metadata to get the doc title (to use as the filename).
const {name: docTitle} = await this._dbManager.getDoc(req);
const options = !_.isEmpty(req.query) ? this._getDownloadOptions(req, docTitle) : {
const options: DownloadOptions = !_.isEmpty(req.query) ? this._getDownloadOptions(req, docTitle) : {
filename: docTitle,
tableId: '',
viewSectionId: undefined,
filters: [],
sortOrder: [],
header: 'label'
};
await downloadXLSX(activeDoc, req, res, options);
}));
@ -1941,12 +1939,21 @@ function getErrorPlatform(tableId: string): TableOperationsPlatform {
};
}
function getTableOperations(req: RequestWithLogin, activeDoc: ActiveDoc, tableId?: string): TableOperationsImpl {
export async function getMetaTables(activeDoc: ActiveDoc, req: RequestWithLogin) {
return await handleSandboxError("", [],
activeDoc.fetchMetaTables(docSessionFromRequest(req)));
}
async function getTableOperations(
req: RequestWithLogin,
activeDoc: ActiveDoc,
tableId?: string): Promise<TableOperationsImpl> {
const options: OpOptions = {
parseStrings: !isAffirmative(req.query.noparse)
};
const realTableId = await getRealTableId(tableId ?? req.params.tableId, {activeDoc, req});
const platform: TableOperationsPlatform = {
...getErrorPlatform(tableId ?? req.params.tableId),
...getErrorPlatform(realTableId),
applyUserActions(actions, opts) {
if (!activeDoc) { throw new Error('no document'); }
return activeDoc.applyUserActions(

@ -17,7 +17,7 @@ import {BaseFormatter, createFullFormatterFromDocData} from 'app/common/ValueFor
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {RequestWithLogin} from 'app/server/lib/Authorizer';
import {docSessionFromRequest} from 'app/server/lib/DocSession';
import {optIntegerParam, optJsonParam, stringParam} from 'app/server/lib/requestUtils';
import {optIntegerParam, optJsonParam, optStringParam, stringParam} from 'app/server/lib/requestUtils';
import {ServerColumnGetters} from 'app/server/lib/ServerColumnGetters';
import * as express from 'express';
import * as _ from 'underscore';
@ -90,6 +90,8 @@ export interface ExportData {
docSettings: DocumentSettings;
}
export type ExportHeader = 'colId' | 'label';
/**
* Export parameters that identifies a section, filters, sort order.
*/
@ -99,6 +101,7 @@ export interface ExportParameters {
sortOrder?: number[];
filters?: Filter[];
linkingFilter?: FilterColValues;
header?: ExportHeader;
}
/**
@ -117,6 +120,7 @@ export function parseExportParameters(req: express.Request): ExportParameters {
const sortOrder = optJsonParam(req.query.activeSortSpec, []) as number[];
const filters: Filter[] = optJsonParam(req.query.filters, []);
const linkingFilter: FilterColValues = optJsonParam(req.query.linkingFilter, null);
const header = optStringParam(req.query.header, 'header', {allowed: ['label', 'colId']}) as ExportHeader | undefined;
return {
tableId,
@ -124,6 +128,7 @@ export function parseExportParameters(req: express.Request): ExportParameters {
sortOrder,
filters,
linkingFilter,
header,
};
}

@ -1,7 +1,7 @@
import {ApiError} from 'app/common/ApiError';
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {FilterColValues} from "app/common/ActiveDocAPI";
import {DownloadOptions, ExportData, exportSection, exportTable, Filter} from 'app/server/lib/Export';
import {DownloadOptions, ExportData, ExportHeader, exportSection, exportTable, Filter} from 'app/server/lib/Export';
import log from 'app/server/lib/log';
import * as bluebird from 'bluebird';
import contentDisposition from 'content-disposition';
@ -17,11 +17,13 @@ bluebird.promisifyAll(csv);
export async function downloadCSV(activeDoc: ActiveDoc, req: express.Request,
res: express.Response, options: DownloadOptions) {
log.info('Generating .csv file...');
const {filename, tableId, viewSectionId, filters, sortOrder, linkingFilter} = options;
const {filename, tableId, viewSectionId, filters, sortOrder, linkingFilter, header} = options;
const data = viewSectionId ?
await makeCSVFromViewSection(
activeDoc, viewSectionId, sortOrder || null, filters || null, linkingFilter || null, req) :
await makeCSVFromTable(activeDoc, tableId, req);
await makeCSVFromViewSection({
activeDoc, viewSectionId, sortOrder: sortOrder || null, filters: filters || null,
linkingFilter: linkingFilter || null, header, req
}) :
await makeCSVFromTable({activeDoc, tableId, header, req});
res.set('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', contentDisposition(filename + '.csv'));
res.send(data);
@ -32,36 +34,51 @@ export async function downloadCSV(activeDoc: ActiveDoc, req: express.Request,
*
* See https://github.com/wdavidw/node-csv for API details.
*
* @param {Object} activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} viewSectionId - id of the viewsection to export.
* @param {Integer[]} activeSortOrder (optional) - overriding sort order.
* @param {Filter[]} filters (optional) - filters defined from ui.
* @param {Object} options - options for the export.
* @param {Object} options.activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} options.viewSectionId - id of the viewsection to export.
* @param {Integer[]} options.activeSortOrder (optional) - overriding sort order.
* @param {Filter[]} options.filters (optional) - filters defined from ui.
* @param {FilterColValues} options.linkingFilter (optional) - linking filter defined from ui.
* @param {string} options.header (optional) - which field of the column to use as header
* @param {express.Request} options.req - the request object.
*
* @return {Promise<string>} Promise for the resulting CSV.
*/
export async function makeCSVFromViewSection(
export async function makeCSVFromViewSection({
activeDoc, viewSectionId, sortOrder = null, filters = null, linkingFilter = null, header, req
}: {
activeDoc: ActiveDoc,
viewSectionId: number,
sortOrder: number[] | null,
filters: Filter[] | null,
linkingFilter: FilterColValues | null,
req: express.Request) {
header?: ExportHeader,
req: express.Request
}) {
const data = await exportSection(activeDoc, viewSectionId, sortOrder, filters, linkingFilter, req);
const file = convertToCsv(data);
const file = convertToCsv(data, { header });
return file;
}
/**
* Returns a csv stream of a table that can be transformed or parsed.
*
* @param {Object} activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} tableId - id of the table to export.
* @param {Object} options - options for the export.
* @param {Object} options.activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} options.tableId - id of the table to export.
* @param {string} options.header (optional) - which field of the column to use as header
* @param {express.Request} options.req - the request object.
*
* @return {Promise<string>} Promise for the resulting CSV.
*/
export async function makeCSVFromTable(
export async function makeCSVFromTable({ activeDoc, tableId, header, req }: {
activeDoc: ActiveDoc,
tableId: string,
req: express.Request) {
header?: ExportHeader,
req: express.Request
}) {
if (!activeDoc.docData) {
throw new Error('No docData in active document');
@ -76,7 +93,7 @@ export async function makeCSVFromTable(
}
const data = await exportTable(activeDoc, tableRef, req);
const file = convertToCsv(data);
const file = convertToCsv(data, { header });
return file;
}
@ -84,13 +101,13 @@ function convertToCsv({
rowIds,
access,
columns: viewColumns,
docSettings
}: ExportData) {
}: ExportData, options: { header?: ExportHeader }) {
// create formatters for columns
const formatters = viewColumns.map(col => col.formatter);
// Arrange the data into a row-indexed matrix, starting with column headers.
const csvMatrix = [viewColumns.map(col => col.label)];
const colPropertyAsHeader = options.header ?? 'label';
const csvMatrix = [viewColumns.map(col => col[colPropertyAsHeader])];
// populate all the rows with values as strings
rowIds.forEach(row => {
csvMatrix.push(access.map((getter, c) => formatters[c].formatAny(getter(row))));

@ -1,7 +1,7 @@
import {PassThrough} from 'stream';
import {FilterColValues} from "app/common/ActiveDocAPI";
import {ActiveDocSource, doExportDoc, doExportSection, doExportTable,
ExportData, ExportParameters, Filter} from 'app/server/lib/Export';
ExportData, ExportHeader, ExportParameters, Filter} from 'app/server/lib/Export';
import {createExcelFormatter} from 'app/server/lib/ExcelFormatter';
import * as log from 'app/server/lib/log';
import {Alignment, Border, Buffer as ExcelBuffer, stream as ExcelWriteStream,
@ -79,26 +79,34 @@ export async function doMakeXLSXFromOptions(
stream: Stream,
options: ExportParameters
) {
const {tableId, viewSectionId, filters, sortOrder, linkingFilter} = options;
const {tableId, viewSectionId, filters, sortOrder, linkingFilter, header} = options;
if (viewSectionId) {
return doMakeXLSXFromViewSection(activeDocSource, testDates, stream, viewSectionId,
sortOrder || null, filters || null, linkingFilter || null);
return doMakeXLSXFromViewSection({activeDocSource, testDates, stream, viewSectionId, header,
sortOrder: sortOrder || null, filters: filters || null, linkingFilter: linkingFilter || null});
} else if (tableId) {
return doMakeXLSXFromTable(activeDocSource, testDates, stream, tableId);
return doMakeXLSXFromTable({activeDocSource, testDates, stream, tableId, header});
} else {
return doMakeXLSX(activeDocSource, testDates, stream);
return doMakeXLSX({activeDocSource, testDates, stream, header});
}
}
/**
* @async
* Returns a XLSX stream of a view section that can be transformed or parsed.
*
* @param {Object} activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} viewSectionId - id of the viewsection to export.
* @param {Integer[]} activeSortOrder (optional) - overriding sort order.
* @param {Filter[]} filters (optional) - filters defined from ui.
* @param {Object} options - options for the export.
* @param {Object} options.activeDocSource - the activeDoc that the table being converted belongs to.
* @param {Integer} options.viewSectionId - id of the viewsection to export.
* @param {Integer[]} options.activeSortOrder (optional) - overriding sort order.
* @param {Filter[]} options.filters (optional) - filters defined from ui.
* @param {FilterColValues} options.linkingFilter (optional)
* @param {Stream} options.stream - the stream to write to.
* @param {boolean} options.testDates - whether to use static dates for testing.
* @param {string} options.header (optional) - which field of the column to use as header
*/
async function doMakeXLSXFromViewSection(
async function doMakeXLSXFromViewSection({
activeDocSource, testDates, stream, viewSectionId, sortOrder, filters, linkingFilter, header
}: {
activeDocSource: ActiveDocSource,
testDates: boolean,
stream: Stream,
@ -106,27 +114,35 @@ async function doMakeXLSXFromViewSection(
sortOrder: number[] | null,
filters: Filter[] | null,
linkingFilter: FilterColValues | null,
) {
header?: ExportHeader,
}) {
const data = await doExportSection(activeDocSource, viewSectionId, sortOrder, filters, linkingFilter);
const {exportTable, end} = convertToExcel(stream, testDates);
const {exportTable, end} = convertToExcel(stream, testDates, {header});
exportTable(data);
return end();
}
/**
* @async
* Returns a XLSX stream of a table that can be transformed or parsed.
*
* @param {Object} activeDoc - the activeDoc that the table being converted belongs to.
* @param {Integer} tableId - id of the table to export.
* @param {Object} options - options for the export.
* @param {Object} options.activeDocSource - the activeDoc that the table being converted belongs to.
* @param {Integer} options.tableId - id of the table to export.
* @param {Stream} options.stream - the stream to write to.
* @param {boolean} options.testDates - whether to use static dates for testing.
* @param {string} options.header (optional) - which field of the column to use as header
*
*/
async function doMakeXLSXFromTable(
async function doMakeXLSXFromTable({activeDocSource, testDates, stream, tableId, header}: {
activeDocSource: ActiveDocSource,
testDates: boolean,
stream: Stream,
tableId: string,
) {
header?: ExportHeader,
}) {
const data = await doExportTable(activeDocSource, {tableId});
const {exportTable, end} = convertToExcel(stream, testDates);
const {exportTable, end} = convertToExcel(stream, testDates, {header});
exportTable(data);
return end();
}
@ -134,12 +150,13 @@ async function doMakeXLSXFromTable(
/**
* Creates excel document with all tables from an active Grist document.
*/
async function doMakeXLSX(
async function doMakeXLSX({activeDocSource, testDates, stream, header}: {
activeDocSource: ActiveDocSource,
testDates: boolean,
stream: Stream,
): Promise<void|ExcelBuffer> {
const {exportTable, end} = convertToExcel(stream, testDates);
header?: ExportHeader,
}): Promise<void|ExcelBuffer> {
const {exportTable, end} = convertToExcel(stream, testDates, {header});
await doExportDoc(activeDocSource, async (table: ExportData) => exportTable(table));
return end();
}
@ -152,7 +169,7 @@ async function doMakeXLSX(
* (The second option is for grist-static; at the time of writing
* WorkbookWriter doesn't appear to be available in a browser context).
*/
function convertToExcel(stream: Stream|undefined, testDates: boolean): {
function convertToExcel(stream: Stream|undefined, testDates: boolean, options: { header?: ExportHeader }): {
exportTable: (table: ExportData) => void,
end: () => Promise<void|ExcelBuffer>,
} {
@ -206,7 +223,8 @@ function convertToExcel(stream: Stream|undefined, testDates: boolean): {
const formatters = columns.map(col => createExcelFormatter(col.formatter.type, col.formatter.widgetOpts));
// Generate headers for all columns with correct styles for whole column.
// Actual header style for a first row will be overwritten later.
ws.columns = columns.map((col, c) => ({ header: col.label, style: formatters[c].style() }));
const colHeader = options.header ?? 'label';
ws.columns = columns.map((col, c) => ({ header: col[colHeader], style: formatters[c].style() }));
// style up the header row
for (let i = 1; i <= columns.length; i++) {
// apply to all rows (including header)

@ -207,7 +207,9 @@
"{{wrongTypeCount}} non-{{columnType}} column is not shown": "{{wrongTypeCount}} nicht{{columnType}} Spalte wird nicht angezeigt",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown": "{{wrongTypeCount}} nicht{{columnType}} Spalte wird nicht angezeigt",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "Spalte {{wrongTypeCount}} Nicht-{{columnType}} wird nicht angezeigt",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "Spalten {{wrongTypeCount}} Nicht-{{columnType}} werden nicht angezeigt"
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "Spalten {{wrongTypeCount}} Nicht-{{columnType}} werden nicht angezeigt",
"No {{columnType}} columns in table.": "Keine {{columnType}} Spalten in der Tabelle.",
"Clear selection": "Auswahl löschen"
},
"DataTables": {
"Click to copy": "Zum Kopieren anklicken",

@ -199,7 +199,9 @@
"Widget needs to {{read}} the current table.": "Widget needs to {{read}} the current table.",
"Widget needs {{fullAccess}} to this document.": "Widget needs {{fullAccess}} to this document.",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} non-{{columnType}} column is not shown",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} non-{{columnType}} columns are not shown"
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} non-{{columnType}} columns are not shown",
"Clear selection": "Clear selection",
"No {{columnType}} columns in table.": "No {{columnType}} columns in table."
},
"DataTables": {
"Click to copy": "Click to copy",

@ -175,7 +175,9 @@
"{{wrongTypeCount}} non-{{columnType}} column is not shown": "{{wrongTypeCount}} columna no {{columnType}} no se muestra",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown": "{{wrongTypeCount}} columnas no {{columnType}} no se muestran",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} no se muestra la columna {{columnType}}",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} no se muestran las columnas {{columnType}}"
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} no se muestran las columnas {{columnType}}",
"No {{columnType}} columns in table.": "No hay columnas {{columnType}} en la tabla.",
"Clear selection": "Borrar la selección"
},
"DocHistory": {
"Activity": "Actividad",

@ -74,8 +74,8 @@
"Accounts": "Comptes",
"Add Account": "Ajouter un compte",
"Sign Out": "Se déconnecter",
"Upgrade Plan": "Version Premium",
"Support Grist": "Centre d'aide Grist",
"Upgrade Plan": "Changer d'offre",
"Support Grist": "Soutenir Grist",
"Billing Account": "Facturation",
"Activation": "Activer",
"Sign In": "Se connecter",
@ -86,7 +86,8 @@
"Action Log failed to load": "Impossible de charger le journal des actions",
"Table {{tableId}} was subsequently removed in action #{{actionNum}}": "La table {{tableId}} a été ensuite supprimée dans l'action #{{actionNum}}",
"This row was subsequently removed in action {{action.actionNum}}": "Cette ligne a été ensuite supprimée dans l'action {{action.actionNum}}",
"Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "La colonne {{colId}} a ensuite été supprimée dans l'action #{{action.actionNum}}"
"Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "La colonne {{colId}} a ensuite été supprimée dans l'action #{{action.actionNum}}",
"All tables": "Toutes les tables"
},
"AddNewButton": {
"Add New": "Nouveau"
@ -194,7 +195,9 @@
"Widget needs to {{read}} the current table.": "Le widget a besoin de {{read}} la table actuelle.",
"Widget does not require any permissions.": "La vue ne nécessite aucune autorisation.",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} colonnes non-{{columnType}} masquées",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} colonnes de type non-{{columnType}} masquées"
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} colonnes de type non-{{columnType}} masquées",
"No {{columnType}} columns in table.": "Pas de colonne de type {{columnType}} dans la table.",
"Clear selection": "Tout désélectionner"
},
"DataTables": {
"Raw Data Tables": "Données sources",
@ -416,7 +419,7 @@
"Get started by exploring templates, or creating your first Grist document.": "Commencez par explorer des modèles ou créez votre premier document Grist.",
"Welcome to Grist!": "Bienvenue sur Grist !",
"Help Center": "Centre d'aide",
"Invite Team Members": "Inviter un nouveau membre",
"Invite Team Members": "Inviter un nouveau membre à l'espace d'équipe",
"Browse Templates": "Parcourir les modèles",
"Create Empty Document": "Créer un document vide",
"Import Document": "Importer un Fichier",
@ -521,7 +524,7 @@
"Select Widget": "Choisir la vue",
"Select Data": "Choisir les données source",
"Group by": "Grouper par",
"Add to Page": "Ajouter à la page"
"Add to Page": "Ajouter à la Page"
},
"Pages": {
"The following tables will no longer be visible_one": "La donnée source ne sera plus visible",
@ -628,11 +631,11 @@
"Use choice position": "Utiliser l'ordre des choix",
"Natural sort": "Trier",
"Empty values last": "Valeurs vides en dernier",
"Search Columns": "Rechercher"
"Search Columns": "Rechercher dans les colonnes"
},
"SortFilterConfig": {
"Save": "Enregistrer",
"Revert": "Restaurer",
"Revert": "Retour",
"Sort": "TRI",
"Filter": "FILTRE",
"Update Sort & Filter settings": "Mettre à jour le tri et le filtre"
@ -649,7 +652,7 @@
"Code View": "Vue du code",
"How-to Tutorial": "Tutoriel pratique",
"Tour of this Document": "Découvrir le document",
"Delete document tour?": "Delete document tour?",
"Delete document tour?": "Supprimer la visite guidée du document ?",
"Delete": "Supprimer",
"Return to viewing as yourself": "Revenir à une vue en propre",
"Raw Data": "Données source",
@ -690,10 +693,10 @@
"Update formula (Shift+Enter)": "Mettre à jour la formule (Maj+Entrée)"
},
"ViewConfigTab": {
"Unmark On-Demand": "Unmark On-Demand",
"Unmark On-Demand": "Ne plus marquer comme \"à la demande\"",
"Make On-Demand": "Rendre dynamique",
"Advanced settings": "Paramètres avancés",
"Big tables may be marked as \"on-demand\" to avoid loading them into the data engine.": "Big tables may be marked as \"on-demand\" to avoid loading them into the data engine.",
"Big tables may be marked as \"on-demand\" to avoid loading them into the data engine.": "Les grosses tables peuvent être marquées comme \"à la demande\" pour éviter de les charger dans le moteur de calcul.",
"Form": "Formulaire",
"Compact": "Compact",
"Blocks": "Blocs",
@ -720,7 +723,7 @@
"ViewSectionMenu": {
"Update Sort&Filter settings": "Mettre à jour le tri et le filtre",
"Save": "Enregistrer",
"Revert": "Restaurer",
"Revert": "Retour",
"(customized)": "(personnalisé)",
"(modified)": "(modifié)",
"(empty)": "(vide)",
@ -791,7 +794,10 @@
"Contact support": "Contacter le support",
"Something went wrong": "Une erreur sest produite",
"There was an error: {{message}}": "Une erreur sest produite : {{message}}",
"There was an unknown error.": "Une erreur inconnue sest produite."
"There was an unknown error.": "Une erreur inconnue sest produite.",
"Account deleted{{suffix}}": "Compte supprimé {{suffix}}",
"Your account has been deleted.": "Votre compte a été supprimé.",
"Sign up": "S'inscrire"
},
"menus": {
"Select fields": "Sélectionner les champs",
@ -847,7 +853,7 @@
"Open row styles": "Ouvrir les styles de ligne",
"Cell Style": "Style de cellule",
"CELL STYLE": "STYLE de CELLULE",
"Default cell style": "Style par défaut",
"Default cell style": "Style par défaut de la cellule",
"Mixed style": "Style composite",
"Header Style": "Style de l'entête",
"Default header style": "Style par défaut",
@ -916,7 +922,7 @@
"Update formula (Shift+Enter)": "Modifier la formule (MAJ+Entrée)"
},
"ColumnEditor": {
"COLUMN DESCRIPTION": "DESCRIPTION",
"COLUMN DESCRIPTION": "DESCRIPTION DE LA COLONNE",
"COLUMN LABEL": "LIBELLÉ"
},
"ACLUsers": {
@ -938,7 +944,7 @@
"CHOICES": "CHOIX"
},
"ColumnInfo": {
"COLUMN DESCRIPTION": "DESCRIPTION",
"COLUMN DESCRIPTION": "DESCRIPTION DE LA COLONNE",
"COLUMN ID: ": "Identifiant de la colonne : ",
"COLUMN LABEL": "LIBELLÉ",
"Cancel": "Annuler",
@ -1013,7 +1019,7 @@
"relational": "relationnelles",
"Access Rules": "Règles d'accès",
"Learn more.": "En savoir plus.",
"They allow for one record to point (or refer) to another.": "Ils permettent à un enregistrement de pointer (ou de faire référence) vers un autre.",
"They allow for one record to point (or refer) to another.": "Ils permettent à une ligne de pointer (ou de faire référence) vers une autre.",
"Add New": "Nouveau",
"Access rules give you the power to create nuanced rules to determine who can see or edit which parts of your document.": "Les règles d'accès vous donnent le pouvoir de créer des règles nuancées pour déterminer qui peut voir ou modifier quelles parties de votre document.",
"Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.": "Utilisez l'icône 𝚺 pour créer des tables récapitulatives (ou tables croisées dynamiques), pour les totaux ou les sous-totaux.",
@ -1021,7 +1027,12 @@
"Anchor Links": "Ancres",
"Custom Widgets": "Vues personnalisées",
"To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Pour créer un lien d'ancrage qui amène l'utilisateur à une cellule spécifique, cliquez sur une ligne et appuyez sur {{shortcut}}.",
"You can choose one of our pre-made widgets or embed your own by providing its full URL.": "Vous pouvez choisir l'une de nos vues prédéfinis ou intégrer la vôtre en indiquant son URL complète."
"You can choose one of our pre-made widgets or embed your own by providing its full URL.": "Vous pouvez choisir l'une de nos vues prédéfinies ou intégrer la vôtre en indiquant son URL complète.",
"To configure your calendar, select columns for start": {
"end dates and event titles. Note each column's type.": "Pour configurer votre calendrier, sélectionnez les colonnes pour les dates de début/fin et le nom de l'évènement. Notez le type de chaque colonne."
},
"Calendar": "Calendrier",
"Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Impossible de trouver les bonnes colonnes ? Cliquez sur \"Changer de vue\" pour sélectionner la table contenant les évènements."
},
"ColumnTitle": {
"Add description": "Ajouter une description",
@ -1030,7 +1041,7 @@
"COLUMN ID: ": "Identifiant de la column : ",
"Column description": "Description de la colonne",
"Column label": "Libellé de la colonne",
"Provide a column label": "Renommer la colonne",
"Provide a column label": "Donner un nom à la colonne",
"Save": "Sauvegarder",
"Close": "Fermer"
},
@ -1074,8 +1085,8 @@
"AI Assistant": "Assistant IA",
"Apply": "Appliquer",
"Cancel": "Annuler",
"Hi, I'm the Grist Formula AI Assistant.": "Bonjour, je suis l'assistant IA de Grist pour les formules",
"I can only help with formulas. I cannot build tables, columns, and views, or write access rules.": "Je peux aider seulement pour les formules, je ne peut pas créer de tables, de colonnes, de vue ou gérer les droits d'accès.",
"Hi, I'm the Grist Formula AI Assistant.": "Bonjour, je suis l'assistant IA de Grist pour les formules.",
"I can only help with formulas. I cannot build tables, columns, and views, or write access rules.": "Je ne peux aider que pour les formules, je ne peut pas créer de tables, de colonnes, de vue ou gérer les droits d'accès.",
"Learn more": "En apprendre plus",
"Clear Conversation": "Effacer la conversation",
"Code View": "Vue du code",
@ -1100,7 +1111,9 @@
"Close": "Fermer",
"Contribute": "Contribuer",
"Support Grist": "Support Grist",
"Opt in to Telemetry": "S'inscrire à Telemetry"
"Opt in to Telemetry": "S'inscrire à l'envoi de données de télémétrie",
"Opted In": "Accepté",
"Support Grist page": "Soutenir Grist"
},
"GridView": {
"Click to insert": "Cliquer pour insérer"
@ -1112,12 +1125,16 @@
"Sponsor Grist Labs on GitHub": "Sponsoriser Grist Labs sur GitHub",
"GitHub Sponsors page": "Page de sponsors GitHub",
"Manage Sponsorship": "Gérer le parrainage",
"Opt in to Telemetry": "S'inscrire à Telemetry",
"Opt out of Telemetry": "S'inscrire à Telemetry",
"Opt in to Telemetry": "S'inscrire à l'envoi de données de télémétrie",
"Opt out of Telemetry": "Se désinscrire de l'envoi de données de télémétrie",
"Support Grist": "Support Grist",
"Telemetry": "Telemetry",
"This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Cette instance est autorisée à utiliser la télémétrie. Seul l'administrateur de l'espace est autorisé à modifier ce paramètre.",
"This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Cette instance est autorisée à utiliser la télémétrie. Seul l'administrateur de l'espace est autorisé à modifier ce paramètre."
"Telemetry": "Télémétrie",
"This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Cette instance accepte l'envoi de données de télémétrie. Seul l'administrateur de l'espace est autorisé à modifier ce paramètre.",
"This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Cette instance est autorisée à utiliser la télémétrie. Seul l'administrateur de l'espace est autorisé à modifier ce paramètre.",
"You have opted out of telemetry.": "Vous avez choisi de ne pas envoyer de données de télémétrie.",
"We only collect usage statistics, as detailed in our {{link}}, never document contents.": "Nous ne collectons que des statistiques d'usage, comme détaillé dans notre {{link}}, jamais le contenu des documents.",
"You can opt out of telemetry at any time from this page.": "Vous pouvez vous désinscrire de la télémétrie à tout moment depuis cette page.",
"You have opted in to telemetry. Thank you!": "Merci de vous être inscrit à la télémétrie !"
},
"buildViewSectionDom": {
"No data": "Aucune donnée",
@ -1129,7 +1146,7 @@
"Anyone with link ": "Toute personne possédant le lien ",
"Cancel": "Annuler",
"Close": "Fermer",
"Add {{member}} to your team": "Ajouter des {{member}} à votre équipe",
"Add {{member}} to your team": "Ajouter {{member}} à votre équipe",
"Confirm": "Confirmer",
"member": "membre",
"{{collaborator}} limit exceeded": "la limite de {{collaborator}} a été atteinte",
@ -1139,39 +1156,39 @@
"Collaborator": "Collaborateur",
"Copy Link": "Copier le lien",
"Create a team to share with more people": "Créer une équipe pour partager avec plus de personnes",
"Grist support": "Support Grist",
"Grist support": "Support utilisateur Grist",
"Guest": "Invité",
"Invite multiple": "Invitation multiple",
"Invite people to {{resourceType}}": "Inviter des personnes à {{resourceType}}",
"Link copied to clipboard": "Lien copié dans le presse-papiers",
"Manage members of team site": "Gérer les membres de l'espace d'équipe",
"No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "L'absence d'accès par défaut permet d'accorder l'accès à des documents ou à des espaces de travail spécifiques, plutôt qu'à l'ensemble de l'espace d'équipe.",
"Off": "Off",
"On": "On",
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.": "Une fois que vous avez supprimé votre propre accès, vous ne pourrez pas le récupérer sans l'aide d'une autre personne disposant d'un accès suffisant au {{name}}.",
"No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "L'absence d'accès par défaut permet d' accorder l'accès à des documents ou à des espaces de travail spécifiques, plutôt qu'à l'ensemble de l'espace d'équipe.",
"Off": "Désactivé",
"On": "Activé",
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.": "Une fois que vous avez supprimé votre propre accès, vous ne pourrez pas le récupérer sans l'aide d'une autre personne disposant d'un accès suffisant à {{name}}.",
"Open Access Rules": "Ouvrir les règles d'accès",
"Outside collaborator": "Collaborateur externe",
"Public Access": "Accès public",
"Public access": "Accès public",
"Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "L'accès public hérite de {{parent}}. Pour le supprimer, changer l'option 'Accès hérité' à 'Aucun'",
"Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "L'accès public est hérité de {{parent}}. Pour le supprimer, changer l'option 'Accès hérité' à 'Aucun'.",
"Public access: ": "Accès public : ",
"Remove my access": "Supprimer mon accès",
"Save & ": "Sauvegarder & ",
"Team member": "Membres",
"User may not modify their own access.": "L'utilisateur ne peut pas modifier son propre accès.",
"Team member": "Membres de l'espace d'équipe",
"User may not modify their own access.": "L'utilisateur ne peut pas modifier ses propres accès.",
"Your role for this team site": "Votre rôle pour cet espace d'équipe",
"Your role for this {{resourceType}}": "Votre rôle pour cet {{resourceType}}",
"free collaborator": "Collaborateur gratuit",
"free collaborator": "collaborateur gratuit",
"guest": "invité",
"No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "L'absence d'accès par défaut permet d'accorder l'accès à des documents ou à des espaces de travail spécifiques, plutôt qu'à l'ensemble de l'espace d'équipe.",
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.": "Une fois que vous avez supprimé votre propre accès, vous ne pourrez pas le récupérer sans l'aide d'une autre personne disposant d'un accès suffisant au {{resourceType}}.",
"User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.": "L'utilisateur a un accès visuel à {{resource}} résultant d'un accès manuel aux ressources internes. S'il est supprimé ici, cet utilisateur perdra l'accès aux ressources internes.",
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.": "Une fois que vous avez supprimé vos propres accès, vous ne pourrez pas les récupérer sans l'aide d'une autre personne disposant d'un accès suffisant au {{resourceType}}.",
"User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.": "L'utilisateur a un accès en lecture seule à {{resource}} résultant d'un accès des ressources à l'intérieur. S'il est supprimé ici, cet utilisateur perdra l'accès aux ressources à l'intérieur.",
"You are about to remove your own access to this {{resourceType}}": "Vous êtes sur le point de supprimer votre propre accès à {{resourceType}}",
"User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.": "L'utilisateur hérite ses permissions de {{parent})}. Pour supprimer cela, paramétrez 'Héritage d'accès à 'Aucun'."
},
"SearchModel": {
"Search all tables": "Rechercher toutes les tables",
"Search all pages": "Rechercher toutes les pages"
"Search all pages": "Rechercher dans toutes les pages"
},
"searchDropdown": {
"Search": "Chercher"

@ -28,7 +28,14 @@
"Sign Out": "Logg ut",
"Sign in": "Logg inn",
"Switch Accounts": "Bytt konto",
"Toggle Mobile Mode": "Slå av/på mobilmodus"
"Toggle Mobile Mode": "Slå av/på mobilmodus",
"Activation": "Aktivering",
"Support Grist": "Støtt Grist",
"Upgrade Plan": "Oppgrader plan",
"Use This Template": "Bruk denne malen",
"Billing Account": "Faktureringskonto",
"Sign In": "Logg inn",
"Sign Up": "Registrering"
},
"AddNewButton": {
"Add New": "Legg til ny"
@ -53,7 +60,8 @@
"Home Page": "Hjemmeside",
"Personal Site": "Personlig side",
"Team Site": "Lagside",
"Legacy": "Foreldet"
"Legacy": "Foreldet",
"Grist Templates": "Grist-maler"
},
"CellContextMenu": {
"Clear cell": "Tøm celle",
@ -84,7 +92,11 @@
"Duplicate rows_one": "Dupliser rad",
"Reset {{count}} columns_one": "Tilbakestill kolonne",
"Reset {{count}} entire columns_other": "Tilbakestill {{count}} hele kolonner",
"Reset {{count}} columns_other": "Tilbakestill {{count}} kolonner"
"Reset {{count}} columns_other": "Tilbakestill {{count}} kolonner",
"Copy": "Kopier",
"Comment": "Kommentar",
"Cut": "Klipp ut",
"Paste": "Lim inn"
},
"ColumnFilterMenu": {
"All": "Alle",
@ -121,7 +133,9 @@
"Widget needs to {{read}} the current table.": "Miniprogrammet må {{read}} nåværende tabell.",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} ikke-{{columnType}}-kolonner er ikke vist.",
"Widget needs {{fullAccess}} to this document.": "Miniprogrammet trenger {{fullAccess}} til dette dokumentet.",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} ikke-{{columnType}}-kolonne er ikke vist."
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} ikke-{{columnType}}-kolonne er ikke vist.",
"No {{columnType}} columns in table.": "Ingen {{columnType}}-kolonner i tabell.",
"Clear selection": "Tøm utvalg"
},
"DocHistory": {
"Activity": "Aktivitet",
@ -186,7 +200,8 @@
"Trash": "Papirkurv",
"Workspace will be moved to Trash.": "Arbeidsområdet vil bli flyttet til papirkurven.",
"Workspaces": "Arbeidsområder",
"Rename": "Gi nytt navn"
"Rename": "Gi nytt navn",
"Tutorial": "Veiledning"
},
"MakeCopyMenu": {
"It will be overwritten, losing any content not in this document.": "Den vil bli overskrevet, noe som forkaster alt innholdet som ikke er i dokumentet.",
@ -211,7 +226,10 @@
"Overwrite": "Overskriv",
"Replacing the original requires editing rights on the original document.": "Erstatting av originalen krever redigeringsrettigheter til originaldokumentet.",
"Be careful, the original has changes not in this document. Those changes will be overwritten.": "Vær forsiktig, originalen har endringer som ikke finnes i dette dokumentet. De endringene vil bli overskrevet.",
"However, it appears to be already identical.": "Dog later det til at det allerede er identisk."
"However, it appears to be already identical.": "Dog later det til at det allerede er identisk.",
"Remove all data but keep the structure to use as a template": "Fjern all data, men behold strukturen til bruk som mal",
"Remove document history (can significantly reduce file size)": "Fjern dokumenthistorikk (kan redusere filstørrelse drastisk)",
"Download full document and history": "Last ned hele dokumentet og historikken"
},
"NotifyUI": {
"Cannot find personal site, sorry!": "Finner ikke personlig side. Beklager.",
@ -222,7 +240,8 @@
"Report a problem": "Innrapporter et problem",
"Go to your free personal site": "Gå til din kostnadsløse personlige side",
"No notifications": "Ingen merknader",
"Notifications": "Merknader"
"Notifications": "Merknader",
"Manage billing": "Håndter fakturering"
},
"RightPanel": {
"COLUMN TYPE": "Kolonnetype",
@ -253,7 +272,8 @@
"Series_other": "Serier",
"Sort & Filter": "Sorter og filtrer",
"You do not have edit access to this document": "Du har ikke redigeringstilgang til dette dokumentet",
"Widget": "Miniprogram"
"Widget": "Miniprogram",
"Add referenced columns": "Legg til kolonner å vise til"
},
"RowContextMenu": {
"Delete": "Slett",
@ -283,7 +303,9 @@
"Current Version": "Nåværende versjon",
"Download": "Last ned",
"Duplicate Document": "Dupliser dokument",
"Send to Google Drive": "Send til Google Drive"
"Send to Google Drive": "Send til Google Drive",
"Download...": "Last ned …",
"Share": "Del"
},
"ThemeConfig": {
"Switch appearance automatically to match system": "Bytt utseende automatisk for å samsvare med systemet",
@ -356,13 +378,17 @@
"You do not have access to this organization's documents.": "Du har ikke tilgang til denne organisasjonens dokumenter.",
"Error{{suffix}}": "Feil-{{suffix}}",
"Page not found{{suffix}}": "Fant ikke siden-{{suffix}}",
"You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Du er innlogget som {{email}}. Du kan logge inn med en annen konto, eller spørre en administrator om tilgang."
"You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Du er innlogget som {{email}}. Du kan logge inn med en annen konto, eller spørre en administrator om tilgang.",
"Account deleted{{suffix}}": "Konto slettet{{suffix}}",
"Your account has been deleted.": "Kontoen din har blitt slettet.",
"Sign up": "Logg inn"
},
"search": {
"Search in document": "Søk i dokumentet",
"Find Next ": "Finn neste ",
"Find Previous ": "Finn forrige ",
"No results": "Resultatløst"
"No results": "Resultatløst",
"Search": "Søk"
},
"DiscussionEditor": {
"Cancel": "Avbryt",
@ -428,7 +454,9 @@
"Lookup Table": "Oppslagstabell",
"Lookup Column": "Oppslagskolonne",
"Permission to access the document in full when needed": "Tilgang til hele dokumentet når det trengs",
"Save": "Lagre"
"Save": "Lagre",
"Allow editors to edit structure (e.g. modify and delete tables, columns, layouts), and to write formulas, which give access to all data regardless of read restrictions.": "Tillat de som redigerer å endre struktur (f.eks. endre og slette tabeller, kolonner, oppsett), og å skrive formler, som gir tilgang til all data uavhengig av begrensninger.",
"This default should be changed if editors' access is to be limited. ": "Dette forvalget må endres hvis de som redigerer ikke skal ha tilgang, "
},
"ChartView": {
"Toggle chart aggregation": "Veksle diagramsvisning",
@ -474,7 +502,9 @@
"Locale:": "Lokalitet:",
"Document ID copied to clipboard": "Dokument-ID kopiert til utklippstavlen",
"Currency:": "Valuta:",
"Document Settings": "Dokumentinnstillinger"
"Document Settings": "Dokumentinnstillinger",
"Manage Webhooks": "Håndter vevkroker",
"Webhooks": "Vevkroker"
},
"DocumentUsage": {
"Usage": "Bruk",
@ -553,7 +583,16 @@
"Try out changes in a copy, then decide whether to replace the original with your edits.": "Prøv ut endringer i en kopi, og avgjør så hvorvidt du vil erstatte originalen med endringene dine.",
"Unpin to hide the the button while keeping the filter.": "Løsne for å skjule knappen og beholde filteret.",
"Useful for storing the timestamp or author of a new record, data cleaning, and more.": "Nyttig for lagring av tidsstempel eller forfatter av en ny oppføring, datarensing, med mer.",
"The total size of all data in this document, excluding attachments.": "Samlet størrelse av all data i dette dokumentet, fraregnet vedlegg."
"The total size of all data in this document, excluding attachments.": "Samlet størrelse av all data i dette dokumentet, fraregnet vedlegg.",
"You can choose one of our pre-made widgets or embed your own by providing its full URL.": "Du kan velge en av miniprogrammene som finnes, eller bygge inn ditt eget ved å angi dets fulle nettadresse.",
"To configure your calendar, select columns for start": {
"end dates and event titles. Note each column's type.": "Velg kolonner for start-/slutt-dato og begivenhetsnavn. Merk deg hvilken type hver kolonne er."
},
"Calendar": "Kalender",
"To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Klikk på en rad og trykk {{shortcut}} for å lage en ankerlenke som tar brukeren til en gitt celle.",
"Anchor Links": "Ankerlenker",
"Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Finner du ikke riktig kolonner? Klikk «Endre miniprogram» for å velge tabellen med begivenhetsdata.",
"Custom Widgets": "Egendefinerte miniprogrammer"
},
"ViewAsDropdown": {
"Example Users": "Eksempelbrukere",
@ -564,7 +603,8 @@
"Action Log failed to load": "Kunne ikke laste inn handlingslogg",
"Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "Kolonnen {{colId}} ble påfølgende fjernet i handling #{{action.actionNum}}",
"Table {{tableId}} was subsequently removed in action #{{actionNum}}": "Tabellen {{tableId}} ble påfølgende fjernet i handling #{{actionNum}}",
"This row was subsequently removed in action {{action.actionNum}}": "Denne raden ble påfølgende fjernet i handling {{action.actionNum}}"
"This row was subsequently removed in action {{action.actionNum}}": "Denne raden ble påfølgende fjernet i handling {{action.actionNum}}",
"All tables": "Alle tabeller"
},
"ColorSelect": {
"Apply": "Bruk",
@ -630,12 +670,15 @@
"Unfreeze {{count}} columns_other": "Tin {{count}} kolonner",
"Reset {{count}} entire columns_other": "Tilbakestill {{count}} hele kolonner",
"Unfreeze all columns": "Tin alle kolonner",
"Add Column": "Legg til kolonne"
"Add Column": "Legg til kolonne",
"Insert column to the right": "Sett inn kolonne til høyre",
"Insert column to the left": "Sett inn kolonne til venstre"
},
"GristDoc": {
"Import from file": "Importer fra fil",
"Added new linked section to view {{viewName}}": "Ny lenket avsnitt lagt til i visning {{viewName}}",
"Saved linked section {{title}} in view {{name}}": "Lagret lenket {{title}}-avsnitt i {{name}}-visningen"
"Saved linked section {{title}} in view {{name}}": "Lagret lenket {{title}}-avsnitt i {{name}}-visningen",
"go to webhook settings": "gå til vevkroksinnstillinger"
},
"HomeIntro": {
"Browse Templates": "Utforsk maler",
@ -657,7 +700,12 @@
"Get started by inviting your team and creating your first Grist document.": "Begynn ved å invitere laget ditt og ved å opprette ditt første Grist-dokument.",
"Interested in using Grist outside of your team? Visit your free ": "Interessert i bruk av Grist utenfor laget ditt? Besøk ditt kostnadsløse ",
"This workspace is empty.": "Arbeidsområdet er tomt.",
"Any documents created in this site will appear here.": "Alle dokumenter opprettet i denne siden vil vises her."
"Any documents created in this site will appear here.": "Alle dokumenter opprettet i denne siden vil vises her.",
"Welcome to Grist, {{- name}}!": "Velkommen til Grist {{- name}}",
"Visit our {{link}} to learn more about Grist.": "Besøk {{link}} for å lære mer om Grist.",
"Welcome to {{- orgName}}": "Velkommen til {{- orgName}}",
"Sign in": "Logg inn",
"To use Grist, please either sign up or sign in.": "Logg inn eller registrer deg for å bruke Grist."
},
"PageWidgetPicker": {
"Building {{- label}} widget": "Bygger {{- label}}-miniprogram",
@ -687,14 +735,21 @@
"CELL STYLE": "Cellestil",
"Cell Style": "Cellestil",
"Default cell style": "Forvalgt cellestil",
"Open row styles": "Åpne radstiler"
"Open row styles": "Åpne radstiler",
"HEADER STYLE": "Topptekststil",
"Header Style": "Topptekststil",
"Default header style": "Forvalgt topptekststil"
},
"FormulaEditor": {
"Errors in all {{numErrors}} cells": "Feil i alle {{numErrors}} celler",
"Column or field is required": "Kolonne eller felt kreves angitt",
"editingFormula is required": "«editingFormula» kreves",
"Errors in {{numErrors}} of {{numCells}} cells": "Feil i {{numErrors}} av {{numCells}} celler",
"Error in the cell": "Feil i cellen"
"Error in the cell": "Feil i cellen",
"Enter formula or {{button}}.": "Skriv inn formel eller {{button}}.",
"Enter formula.": "Skriv inn formel.",
"Expand Editor": "Utvid tekstbehandler",
"use AI Assistant": "bruk AI-assistent"
},
"VisibleFieldsConfig": {
"Cannot drop items into Hidden Fields": "Kan ikke plassere elementer i skjulte felter",
@ -808,7 +863,8 @@
"Save": "Lagre",
"Provide a table name": "Angi et tabellnavn",
"Override widget title": "Overstyr miniprogramsnavn",
"WIDGET TITLE": "Miniprogramsnavn"
"WIDGET TITLE": "Miniprogramsnavn",
"WIDGET DESCRIPTION": "Miniprogramsbeskrivelse"
},
"menus": {
"Select fields": "Velg felter",
@ -904,7 +960,22 @@
"Importer": {
"Merge rows that match these fields:": "Flett rader som samsvarer med disse feltene:",
"Select fields to match on": "Velg felter å jamføre",
"Update existing records": "Oppdater eksisterende oppføringer"
"Update existing records": "Oppdater eksisterende oppføringer",
"Column mapping": "Kolonnetilknytning",
"Grist column": "Grist-kolonne",
"{{count}} unmatched field_one": "{{count}} usamsvarende felt",
"{{count}} unmatched field in import_one": "{{count}} usamsvarende felt i import",
"Revert": "Angre",
"Skip Import": "Hopp over import",
"{{count}} unmatched field_other": "{{count}} usamsvarende felter",
"New Table": "Ny tabell",
"Skip": "Hopp over",
"Column Mapping": "Kolonnetilknytning",
"Destination table": "Måltabell",
"Skip Table on Import": "Hopp over tabell ved import",
"Import from file": "Importer fra fil",
"{{count}} unmatched field in import_other": "{{count}} usamsvarende felt i import",
"Source column": "Kildekolonne"
},
"LeftPanelCommon": {
"Help Center": "Hjelpesenter"
@ -973,5 +1044,176 @@
},
"EditorTooltip": {
"Convert column to formula": "Konverter kolonne til formel"
},
"FormulaAssistant": {
"Data": "Data",
"Press Enter to apply suggested formula.": "Trykk Enter for å bruke foreslått formel.",
"See our {{helpFunction}} and {{formulaCheat}}, or visit our {{community}} for more help.": "Sjekk {{helpFunction}} og {{formulaCheat}}, eller besøk {{community}} for mer hjelp.",
"Sign up for a free Grist account to start using the Formula AI Assistant.": "Registrer deg for en gratis Grist-konto for å bruke AI-formelassistenten.",
"Clear Conversation": "Tøm samtale",
"New Chat": "Ny sludring",
"Code View": "Kodevisning",
"Apply": "Bruk",
"Learn more": "Lær mer",
"Regenerate": "Regenerer",
"Community": "Gemenskap",
"I can only help with formulas. I cannot build tables, columns, and views, or write access rules.": "Jeg kan kun hjelpe deg med formler. Jeg kan ikke bygge tabeller, kolonner, visninger, eller skrive tilgangsregler.",
"Hi, I'm the Grist Formula AI Assistant.": "Hei. Jeg er Grists AI-formelassistent.",
"Preview": "Forhåndsvis",
"Ask the bot.": "Spør botten.",
"Function List": "Funksjonsliste",
"For higher limits, contact the site owner.": "Kontakt sidens eier for høyere grenser.",
"Tips": "Tips",
"Save": "Lagre",
"Sign Up for Free": "Registrer deg gratis",
"Formula Cheat Sheet": "Formel-jukseark",
"Grist's AI Assistance": "Grists AI-assistanse",
"Formula AI Assistant is only available for logged in users.": "AI-formelassistenten er kun tilgjengelig for innloggede brukere.",
"Grist's AI Formula Assistance. ": "Grists AI-formelassistanse ",
"upgrade to the Pro Team plan": "oppgrader til proff lagplan",
"You have used all available credits.": "Du har brukt alle tilgjengelige planpoeng.",
"upgrade your plan": "oppgrader din plan",
"Formula Help. ": "Formelhjelp. ",
"You have {{numCredits}} remaining credits.": "Du har {{numCredits}} planpoeng igjen.",
"Capabilities": "Ferdigeter",
"What do you need help with?": "Hva trenger du hjelp med?",
"Cancel": "Avbryt",
"Need help? Our AI assistant can help.": "Hjelp? Vår AI-assistent kan hjelpe deg.",
"AI Assistant": "AI-assistant",
"For higher limits, {{upgradeNudge}}.": "{{upgradeNudge}} for høyere grenser.",
"There are some things you should know when working with me:": "Det er noen ting du burde vite når du jobber med meg:"
},
"FloatingPopup": {
"Maximize": "Maksimer",
"Minimize": "Minimer"
},
"Clipboard": {
"Unavailable Command": "Utilgjengelig kommando",
"Got it": "Skjønner"
},
"SupportGristPage": {
"You have opted out of telemetry.": "Du har reservert deg mot datainnsamling.",
"Support Grist": "Støtt Grist",
"Opt out of Telemetry": "Skru av datainnsamling",
"GitHub Sponsors page": "GitHub Sponsor-side",
"Sponsor Grist Labs on GitHub": "Spons Grist Labs på GitHub",
"Manage Sponsorship": "Håndter sponsing",
"Help Center": "Hjelpesenter",
"We only collect usage statistics, as detailed in our {{link}}, never document contents.": "Kun bruksstatistikk samles inn og aldri dokumentinnhold. Dette er beskrevet nøye på {{link}}.",
"You can opt out of telemetry at any time from this page.": "Du kan motsette deg datainnsamling når som helst fra denne siden.",
"Home": "Hjem",
"This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Denne instansen har reservert seg mot datainnsamling. Kun sidens administrator har tilgang til å endre dette.",
"Telemetry": "Telemetri",
"Opt in to Telemetry": "Tillat datainnsamling",
"You have opted in to telemetry. Thank you!": "Du har reservert deg mot datainnsamling. Takk.",
"This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Denne instansen har påslått datainnsamling. Kun sidens administrator har mulighet til å endre dette.",
"GitHub": "GitHub"
},
"UserManager": {
"Anyone with link ": "Alle med lenken ",
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.": "Når du har fjernet din egen tilgang vil du ikke kunne logge inn igjen uten hjelp fra noen andre som har tilstrekkelig tilgang til {{name}}.",
"{{limitAt}} of {{limitTop}} {{collaborator}}s": "{{limitAt}} av {{limitTop}} {{collaborator}}s",
"Your role for this team site": "Din rolle på denne lagsiden",
"Copy Link": "Kopier lenke",
"User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.": "Bruker har visningstilgang for {{resource}}, som følge av manuelt satt tilgang til ressursene som finnes der. Hvis fjernet her vil brukeren miste tilgang til ressursene i {{resource}}.",
"User may not modify their own access.": "Brukere kan ikke endre egen tilgang.",
"member": "medlem",
"Add {{member}} to your team": "Legg til {{member}} på laget ditt",
"Collaborator": "Medarbeider",
"Link copied to clipboard": "Lenke kopiert til utklippstavlen",
"team site": "lagside",
"Create a team to share with more people": "Opprett et lag for å dele med flere",
"guest": "gjest",
"Public access: ": "Offentlig tilgang: ",
"Team member": "Lagmedlem",
"Manage members of team site": "Håndter medlemmer av lagside",
"Off": "Av",
"free collaborator": "ledig medarbeider",
"Save & ": "Lagre og ",
"Outside collaborator": "Medarbeider annensteds fra",
"{{collaborator}} limit exceeded": "{{collaborator}}-grense overskredet",
"User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.": "Bruker nedarver tilganger fra {{parent})}. Fjern ved å sette «Nedarv tilgang» til «Ingen».",
"Your role for this {{resourceType}}": "Din rolle i denne {{resourceType}}",
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.": "Når du har fjernet din egen tilgang vil du ikke kunne logge inn igjen uten hjelp fra noen andre som har tilstrekkelig tilgang til {{resourceType}}.",
"Close": "Lukk",
"Allow anyone with the link to open.": "Tillat alle med lenken å åpne.",
"No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "Ingen forvalgt tilgang tillater tilgang å innvilges til individuelle dokumenter eller arbeidsoråder, snarere enn hele lagsiden.",
"Invite people to {{resourceType}}": "Inviter noen til {{resourceType}}",
"Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Offentlig tilgang nedarvet fra {{parent}}. Fjern den ved å sette «Nedarv tilgang» til «Ingen».",
"Remove my access": "Fjern min tilgang",
"Public access": "Offentlig tilgang",
"Public Access": "Offentlig tilgang",
"Cancel": "Avbryt",
"Grist support": "Grist-støtte",
"You are about to remove your own access to this {{resourceType}}": "Du er i ferd med å fjerne din egen tilgang til denne {{resourceType}}",
"User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Bruker nedarver tilganger fra {{parent}}. For å fjerne sett «Nedarv tilgang» til «Ingen».",
"Guest": "Gjest",
"Invite multiple": "Inviter flere",
"Confirm": "Bekreft",
"On": "På",
"Open Access Rules": "Åpne tilgangsregler",
"No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "Ingen forvalgt tilgang tillater tilgang å innvilges til individuelle dokumenter eller arbeidsoråder, snarere enn hele lagsiden."
},
"PagePanels": {
"Open Creator Panel": "Åpne skaperpanel",
"Close Creator Panel": "Lukk skaperpanel"
},
"buildViewSectionDom": {
"Not all data is shown": "Ikke all dataen vises",
"No row selected in {{title}}": "Ingen rader valgt i {{title}}",
"No data": "Ingen data"
},
"ColumnTitle": {
"Column ID copied to clipboard": "Kolonne-ID kopiert til utklippstavle",
"Add description": "Legg til beskrivelse",
"Column description": "Kolonnebeskrivelse",
"COLUMN ID: ": "Kolonne-ID: ",
"Provide a column label": "Angi en kolonneetikett",
"Close": "Lukk",
"Cancel": "Avbryt",
"Column label": "Kolonneetikett",
"Save": "Lagre"
},
"SupportGristNudge": {
"Support Grist": "Støtt Grist",
"Close": "Lukk",
"Opt in to Telemetry": "Tillat datainnsamling",
"Help Center": "Hjelpesenter",
"Opted In": "Tillatt",
"Contribute": "Bidra",
"Support Grist page": "«Støtt Grist»-siden"
},
"WelcomeSitePicker": {
"You have access to the following Grist sites.": "Du har tilgang til følgende Grist-sider.",
"Welcome back": "Velkommen tilbake",
"You can always switch sites using the account menu.": "Du kan alltid bytte sider fra kontomenyen."
},
"FieldContextMenu": {
"Copy anchor link": "Kopier ankerlenke",
"Hide field": "Skjul felt",
"Copy": "Kopier",
"Paste": "Lim inn",
"Clear field": "Tøm felt",
"Cut": "Klipp ut"
},
"DescriptionTextArea": {
"DESCRIPTION": "Beskrivelse"
},
"WebhookPage": {
"Webhook Settings": "Vevkroksinnstillinger",
"Clear Queue": "Tøm kø"
},
"FloatingEditor": {
"Collapse Editor": "Fold sammen tekstbehandler"
},
"GridView": {
"Click to insert": "Klikk for å sette inn"
},
"searchDropdown": {
"Search": "Søk"
},
"SearchModel": {
"Search all tables": "Søk i alle tabeller",
"Search all pages": "Søk på alle sider"
}
}

@ -70,7 +70,9 @@
"Sign Out": "Wyloguj się",
"Sign in": "Zaloguj się",
"Switch Accounts": "Przełącz konta",
"Toggle Mobile Mode": "Przełącz na tryb mobilny"
"Toggle Mobile Mode": "Przełącz na tryb mobilny",
"Activation": "Aktywacja",
"Sign In": "Zaloguj się"
},
"ViewAsDropdown": {
"View As": "Wyświetl jako",
@ -791,7 +793,7 @@
},
"OnBoardingPopups": {
"Next": "Następny",
"Finish": "Skończyć"
"Finish": "Zakończ"
},
"SortFilterConfig": {
"Revert": "Przywrócić",

@ -207,7 +207,9 @@
"{{wrongTypeCount}} non-{{columnType}} column is not shown": "{{wrongTypeCount}} a não-{{columnType}} coluna não é mostrada",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown": "{{wrongTypeCount}} as não-{{columnType}} colunas não são mostradas",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} a não-{{columnType}} coluna não é mostrada",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} as não-{{columnType}} colunas não são mostradas"
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} as não-{{columnType}} colunas não são mostradas",
"No {{columnType}} columns in table.": "Não há colunas {{columnType}} na tabela.",
"Clear selection": "Limpar seleção"
},
"DataTables": {
"Click to copy": "Clique para copiar",

@ -164,7 +164,9 @@
"Select Custom Widget": "Выбор пользовательского виджета",
"Pick a column": "Выберать столбец",
"Read selected table": "Просмотр выбранной таблицы",
"Widget does not require any permissions.": "Виджет не требует никаких разрешений."
"Widget does not require any permissions.": "Виджет не требует никаких разрешений.",
"No {{columnType}} columns in table.": "Нет {{columnType}} столбцов в таблице.",
"Clear selection": "Очистить выбор"
},
"AccountWidget": {
"Access Details": "Сведения о доступе",
@ -769,7 +771,10 @@
"Contact support": "Обратитесь в службу поддержки",
"Sign in to access this organization's documents.": "Войдите, чтобы получить доступ к документам этой организации.",
"The requested page could not be found.{{separator}}Please check the URL and try again.": "Запрашиваемая страница не может быть найдена.{{separator}}Пожалуйста, проверьте URL-адрес и повторите попытку.",
"You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Вы вошли как {{email}}. Вы можете войти с другой учетной записью или запросить доступ у администратора."
"You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Вы вошли как {{email}}. Вы можете войти с другой учетной записью или запросить доступ у администратора.",
"Account deleted{{suffix}}": "Аккаунт удален{{suffix}}",
"Your account has been deleted.": "Ваш аккаунт был удален.",
"Sign up": "Регистрация"
},
"CellStyle": {
"CELL STYLE": "СТИЛЬ ЯЧЕЙКИ",

@ -61,7 +61,9 @@
"Save Document": "Зберегти документ",
"Send to Google Drive": "Надіслати на Google Drive",
"Show in folder": "Показати в папці",
"Work on a Copy": "Працювати над копією"
"Work on a Copy": "Працювати над копією",
"Download...": "Завантажити...",
"Share": "Поділитися"
},
"MakeCopyMenu": {
"Be careful, the original has changes not in this document. Those changes will be overwritten.": "Зверніть увагу! В оригіналі є зміни, яких немає в цьому документі. Ці зміни будуть перезаписані.",
@ -86,7 +88,10 @@
"Workspace": "Робочий простір",
"You do not have write access to the selected workspace": "Ви не маєте права на запис у вибраному робочому просторі",
"You do not have write access to this site": "Ви не маєте права на запис на цьому сайті",
"However, it appears to be already identical.": "Втім, схоже, що вони вже ідентичні."
"However, it appears to be already identical.": "Втім, схоже, що вони вже ідентичні.",
"Remove all data but keep the structure to use as a template": "Видалити всі дані, але зберегти структуру, щоб використовувати як шаблон",
"Remove document history (can significantly reduce file size)": "Видалити історію документа (може значно зменшити розмір файлу)",
"Download full document and history": "Завантажити повний документ та історію"
},
"SortConfig": {
"Add Column": "Додати стовпець",
@ -134,7 +139,7 @@
"Freeze {{count}} columns_other": "Закріпити {{count}} стовпців",
"Hide {{count}} columns_other": "Сховати {{count}} стовпців",
"Insert column to the {{to}}": "Вставити стовпець у {{to}}",
"More sort options ...": "Більше варіантів сортування…",
"More sort options ...": "Більше опцій сортування…",
"Rename column": "Перейменувати стовпець",
"Reset {{count}} columns_one": "Скинути стовпець",
"Reset {{count}} entire columns_one": "Скинути весь стовпець",
@ -144,7 +149,9 @@
"Sorted (#{{count}})_other": "Відсортовано (#{{count}})",
"Unfreeze all columns": "Відкріпити всі стовпці",
"Unfreeze {{count}} columns_one": "Відкріпити цей стовпець",
"Unfreeze {{count}} columns_other": "Відкріпити {{count}} стовпців"
"Unfreeze {{count}} columns_other": "Відкріпити {{count}} стовпців",
"Insert column to the right": "Вставити стовпець праворуч",
"Insert column to the left": "Вставити стовпець ліворуч"
},
"ThemeConfig": {
"Switch appearance automatically to match system": "Автоматично змінювати оформлення відповідно до системи",
@ -170,7 +177,12 @@
"Welcome to {{orgName}}": "Ласкаво просимо до {{orgName}}",
"You have read-only access to this site. Currently there are no documents.": "Ви маєте доступ до цього сайту лише для читання. Наразі документів немає.",
"personal site": "особистий сайт",
"{{signUp}} to save your work. ": "{{signUp}}, щоб зберегти вашу роботу. "
"{{signUp}} to save your work. ": "{{signUp}}, щоб зберегти вашу роботу. ",
"Welcome to Grist, {{- name}}!": "Ласкаво просимо до Grist, {{- name}}!",
"Visit our {{link}} to learn more about Grist.": "Відвідайте наш {{link}}, щоб дізнатися більше про Grist.",
"Welcome to {{- orgName}}": "Ласкаво просимо до {{- orgName}}",
"Sign in": "Увійти в систему",
"To use Grist, please either sign up or sign in.": "Щоб користуватися Grist, будь ласка, зареєструйтеся або увійдіть."
},
"NotifyUI": {
"Go to your free personal site": "Перейдіть на свій безкоштовний персональний сайт",
@ -181,7 +193,8 @@
"Notifications": "Повідомлення",
"Renew": "Поновити",
"Report a problem": "Повідомити про проблему",
"Upgrade Plan": "Змінити тарифний план"
"Upgrade Plan": "Змінити тарифний план",
"Manage billing": "Управління рахунком"
},
"ViewSectionMenu": {
"Revert": "Повернутися",
@ -268,7 +281,9 @@
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} не-{{columnType}} стовпець не відображається",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} не-{{columnType}} стовпці не відображаються",
"Widget does not require any permissions.": "Віджет не вимагає ніяких дозволів.",
"Widget needs to {{read}} the current table.": "Віджету необхідно {{read}} поточну таблицю."
"Widget needs to {{read}} the current table.": "Віджету необхідно {{read}} поточну таблицю.",
"No {{columnType}} columns in table.": "У таблиці немає стовпців типу {{columnType}}.",
"Clear selection": "Очистити вибір"
},
"ACUserManager": {
"Invite new member": "Запросити нового користувача",
@ -310,7 +325,9 @@
"Remove column {{- colId }} from {{- tableId }} rules": "Видалити стовпець {{- colId }} з правил {{- tableId }}",
"When adding table rules, automatically add a rule to grant OWNER full access.": "При додаванні правил таблиці, автоматично додавати правило для надання ВЛАСНИКУ повного доступу.",
"Seed rules": "Успадковані правила",
"Allow everyone to copy the entire document, or view it in full in fiddle mode.\nUseful for examples and templates, but not for sensitive data.": "Дозволити кожному скопіювати весь документ або переглянути його повністю в режимі створення нових копій.\nКорисно для прикладів і шаблонів, але не для конфіденційних даних."
"Allow everyone to copy the entire document, or view it in full in fiddle mode.\nUseful for examples and templates, but not for sensitive data.": "Дозволити кожному скопіювати весь документ або переглянути його повністю в режимі створення нових копій.\nКорисно для прикладів і шаблонів, але не для конфіденційних даних.",
"Allow editors to edit structure (e.g. modify and delete tables, columns, layouts), and to write formulas, which give access to all data regardless of read restrictions.": "Дозволити редакторам редагувати структуру (наприклад, змінювати та видаляти таблиці, стовпці, макети), а також писати формули, які надають доступ до всіх даних незалежно від обмежень на читання.",
"This default should be changed if editors' access is to be limited. ": "Цей параметр слід змінити, якщо ви хочете обмежити доступ редакторів. "
},
"AccountPage": {
"API": "API",
@ -341,7 +358,14 @@
"Sign in": "Увійти в систему",
"Toggle Mobile Mode": "Переключити в мобільний режим",
"Document Settings": "Параметри документа",
"Switch Accounts": "Змінити обліковий запис"
"Switch Accounts": "Змінити обліковий запис",
"Activation": "Активація",
"Support Grist": "Підтримати Grist",
"Upgrade Plan": "Оновити План",
"Use This Template": "Використати цей шаблон",
"Billing Account": "Рахунок оплати",
"Sign In": "Увійти в систему",
"Sign Up": "Зареєструватися"
},
"ViewAsDropdown": {
"View As": "Переглянути як",
@ -352,7 +376,8 @@
"Action Log failed to load": "Не вдалося завантажити журнал дій",
"Table {{tableId}} was subsequently removed in action #{{actionNum}}": "Таблиця {{tableId}} згодом була видалена під час події #{{actionNum}}",
"This row was subsequently removed in action {{action.actionNum}}": "Ця строка згодом була видалена під час події {{action.actionNum}}",
"Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "Колонка {{colId}} згодом була видалена під час події #{{action.actionNum}}"
"Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "Колонка {{colId}} згодом була видалена під час події #{{action.actionNum}}",
"All tables": "Всі таблиці"
},
"ApiKey": {
"Click to show": "Натисніть, щоб показати",
@ -377,7 +402,8 @@
"Home Page": "Домашня сторінка",
"Legacy": "Застаріла версія",
"Personal Site": "Особистий сайт",
"Team Site": "Сайт команди"
"Team Site": "Сайт команди",
"Grist Templates": "Шаблони від Grist"
},
"CellContextMenu": {
"Clear cell": "Очистити клітинку",
@ -398,7 +424,11 @@
"Reset {{count}} columns_one": "Скинути стовпець",
"Reset {{count}} columns_other": "Скинути {{count}} стовпці",
"Reset {{count}} entire columns_one": "Скинути весь стовпець",
"Reset {{count}} entire columns_other": "Скинути {{count}} всі стовпці"
"Reset {{count}} entire columns_other": "Скинути {{count}} всі стовпці",
"Copy": "Копіювати",
"Comment": "Коментар",
"Cut": "Вирізати",
"Paste": "Вставити"
},
"ColorSelect": {
"Apply": "Застосовувати",
@ -487,7 +517,9 @@
"API": "API",
"Document ID copied to clipboard": "Ідентифікатор документа скопійований у буфер",
"Ok": "ОК",
"Engine (experimental {{span}} change at own risk):": "Обчислювальна система (експериментальна версія {{span}} змінюйте на власний ризик):"
"Engine (experimental {{span}} change at own risk):": "Обчислювальна система (експериментальна версія {{span}} змінюйте на власний ризик):",
"Manage Webhooks": "Керування веб-хуками",
"Webhooks": "Веб-хуки"
},
"DocTour": {
"No valid document tour": "Немає дійсного огляду документа",
@ -516,7 +548,8 @@
"GristDoc": {
"Added new linked section to view {{viewName}}": "Додано новий пов’язаний розділ для перегляду {{viewName}}",
"Import from file": "Імпортувати з файлу",
"Saved linked section {{title}} in view {{name}}": "Збережений зв'язаний розділ {{title}}, що відображається в {{name}}"
"Saved linked section {{title}} in view {{name}}": "Збережений зв'язаний розділ {{title}}, що відображається в {{name}}",
"go to webhook settings": "перейти до налаштувань веб-хуків"
},
"HomeLeftPane": {
"Delete": "Видалити",
@ -526,17 +559,33 @@
"Manage Users": "Керувати користувачами",
"Delete {{workspace}} and all included documents?": "Видалити {{workspace}} та всі включені документи?",
"Create Workspace": "Створити робочій простір",
"Examples & Templates": "Приклади та шаблони",
"Examples & Templates": "Шаблони",
"Import Document": "Імпортувати документ",
"Trash": "Кошик",
"Workspace will be moved to Trash.": "Робочий простір буде переміщено до кошика.",
"Rename": "Перейменувати",
"Workspaces": "Робочій простір"
"Workspaces": "Робочій простір",
"Tutorial": "Туторіал"
},
"Importer": {
"Merge rows that match these fields:": "Об'єднати рядки, які відповідають цим полям:",
"Update existing records": "Оновити існуючі записи",
"Select fields to match on": "Оберіть поля для зіставлення"
"Select fields to match on": "Оберіть поля для зіставлення",
"Column mapping": "Співставлення колонок",
"Grist column": "Стовпець у Grist",
"{{count}} unmatched field_one": "{{count}} невідповідне поле",
"{{count}} unmatched field in import_one": "{{count}} невідповідне поле при імпорті",
"Revert": "Повернути",
"Skip Import": "Пропустити імпорт",
"{{count}} unmatched field_other": "{{count}} невідповідних полів",
"New Table": "Нова таблиця",
"Skip": "Пропустити",
"Column Mapping": "Співставлення колонок",
"Destination table": "Таблиця призначення",
"Skip Table on Import": "Пропустити таблицю при імпорті",
"Import from file": "Імпорт з файлу",
"{{count}} unmatched field in import_other": "{{count}} невідповідних полів при імпорті",
"Source column": "Вихідний стовпець"
},
"LeftPanelCommon": {
"Help Center": "Центр допомоги"
@ -609,12 +658,13 @@
"Select Widget": "Виберіть віджет",
"Series_one": "Серії",
"Series_other": "Серії",
"Sort & Filter": "Сортування та фільтрація",
"Sort & Filter": "Порядок та фільтр",
"TRANSFORM": "ПЕРЕТВОРИТИ",
"Theme": "Тема",
"WIDGET TITLE": "НАЗВА ВІДЖЕТА",
"Widget": "Віджет",
"You do not have edit access to this document": "Ви не маєте права на редагування цього документа"
"You do not have edit access to this document": "Ви не маєте права на редагування цього документа",
"Add referenced columns": "Додати стовпець за посиланням"
},
"RowContextMenu": {
"Copy anchor link": "Скопіювати якірне посилання",
@ -679,7 +729,7 @@
"Make On-Demand": "Встановити статус \"на вимогу\""
},
"ViewLayoutMenu": {
"Advanced Sort & Filter": "Розширене сортування та фільтр",
"Advanced Sort & Filter": "Розширені порядок та фільтр",
"Copy anchor link": "Скопіювати якірне посилання",
"Data selection": "Вибір даних",
"Delete record": "Видалити запис",
@ -718,7 +768,8 @@
"Override widget title": "Перевизначити назву віджета",
"Provide a table name": "Вкажіть ім'я таблиці",
"Save": "Зберегти",
"WIDGET TITLE": "НАЗВА ВІДЖЕТА"
"WIDGET TITLE": "НАЗВА ВІДЖЕТА",
"WIDGET DESCRIPTION": "ОПИС ВІДЖЕТУ"
},
"breadcrumbs": {
"fiddle": "fiddle",
@ -749,7 +800,10 @@
"You do not have access to this organization's documents.": "У вас немає доступу до документів цієї організації.",
"Sign in to access this organization's documents.": "Увійдіть, щоб отримати доступ до документів цієї організації.",
"The requested page could not be found.{{separator}}Please check the URL and try again.": "Не вдалося знайти запитувану сторінку.{{separator}}Будь ласка, перевірте URL-адресу і спробуйте ще раз.",
"You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Ви увійшли як {{email}}. Ви можете увійти під іншим обліковим записом або попросити доступ у адміністратора."
"You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Ви увійшли як {{email}}. Ви можете увійти під іншим обліковим записом або попросити доступ у адміністратора.",
"Account deleted{{suffix}}": "Обліковий запис видалено{{suffix}}",
"Your account has been deleted.": "Ваш обліковий запис видалено.",
"Sign up": "Зареєструватися"
},
"menus": {
"* Workspaces are available on team plans. ": "* Робочі простори доступні в командних тарифах. ",
@ -783,7 +837,8 @@
"Find Next ": "Знайти наступне ",
"Find Previous ": "Знайти попереднє ",
"Search in document": "Пошук у документі",
"No results": "Немає результатів"
"No results": "Немає результатів",
"Search": "Пошук"
},
"sendToDrive": {
"Sending file to Google Drive": "Надсилання файлу на Google Диск"
@ -809,7 +864,10 @@
"Default cell style": "Стиль клітинки за замовчуванням",
"Mixed style": "Змішаний стиль",
"Open row styles": "Відкрити стилі рядків",
"Cell Style": "Стиль клітинки"
"Cell Style": "Стиль клітинки",
"HEADER STYLE": "СТИЛЬ ЗАГОЛОВКА",
"Header Style": "Стиль Заголовка",
"Default header style": "За замовчуванням"
},
"ChoiceTextBox": {
"CHOICES": "ВАРІАНТИ"

@ -230,6 +230,14 @@ describe('DocApi', function () {
// Contains the tests. This is where you want to add more test.
function testDocApi() {
async function generateDocAndUrl(docName: string = "Dummy") {
const wid = (await userApi.getOrgWorkspaces('current')).find((w) => w.name === 'Private')!.id;
const docId = await userApi.newDoc({name: docName}, wid);
const docUrl = `${serverUrl}/api/docs/${docId}`;
const tableUrl = `${serverUrl}/api/docs/${docId}/tables/Table1`;
return { docUrl, tableUrl, docId };
}
it("creator should be owner of a created ws", async () => {
const kiwiEmail = 'kiwi@getgrist.com';
const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id;
@ -547,9 +555,7 @@ function testDocApi() {
}
it("GET /docs/{did}/tables/{tid}/data retrieves data in column format", async function () {
const resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/data`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, {
const data = {
id: [1, 2, 3, 4],
A: ['hello', '', '', ''],
B: ['', 'world', '', ''],
@ -557,68 +563,73 @@ function testDocApi() {
D: [null, null, null, null],
E: ['HELLO', '', '', ''],
manualSort: [1, 2, 3, 4]
});
};
const respWithTableId = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/data`, chimpy);
assert.equal(respWithTableId.status, 200);
assert.deepEqual(respWithTableId.data, data);
const respWithTableRef = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/1/data`, chimpy);
assert.equal(respWithTableRef.status, 200);
assert.deepEqual(respWithTableRef.data, data);
});
it("GET /docs/{did}/tables/{tid}/records retrieves data in records format", async function () {
const resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/records`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data,
{
records:
[
{
id: 1,
fields: {
A: 'hello',
B: '',
C: '',
D: null,
E: 'HELLO',
},
const data = {
records:
[
{
id: 1,
fields: {
A: 'hello',
B: '',
C: '',
D: null,
E: 'HELLO',
},
{
id: 2,
fields: {
A: '',
B: 'world',
C: '',
D: null,
E: '',
},
},
{
id: 2,
fields: {
A: '',
B: 'world',
C: '',
D: null,
E: '',
},
{
id: 3,
fields: {
A: '',
B: '',
C: '',
D: null,
E: '',
},
},
{
id: 3,
fields: {
A: '',
B: '',
C: '',
D: null,
E: '',
},
{
id: 4,
fields: {
A: '',
B: '',
C: '',
D: null,
E: '',
},
},
{
id: 4,
fields: {
A: '',
B: '',
C: '',
D: null,
E: '',
},
]
});
},
]
};
const respWithTableId = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/records`, chimpy);
assert.equal(respWithTableId.status, 200);
assert.deepEqual(respWithTableId.data, data);
const respWithTableRef = await axios.get(
`${serverUrl}/api/docs/${docIds.Timesheets}/tables/1/records`, chimpy);
assert.equal(respWithTableRef.status, 200);
assert.deepEqual(respWithTableRef.data, data);
});
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], {
const data = {
id: 1,
fields: {
manualSort: 1,
@ -628,7 +639,19 @@ function testDocApi() {
D: null,
E: 'HELLO',
},
});
};
const respWithTableId = await axios.get(
`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/records`,
{...chimpy, params }
);
assert.equal(respWithTableId.status, 200);
assert.deepEqual(respWithTableId.data.records[0], data);
const respWithTableRef = await axios.get(
`${serverUrl}/api/docs/${docIds.Timesheets}/tables/1/records`,
{...chimpy, params }
);
assert.equal(respWithTableRef.status, 200);
assert.deepEqual(respWithTableRef.data.records[0], data);
});
it("GET /docs/{did}/tables/{tid}/records handles errors and hidden columns", async function () {
@ -683,119 +706,121 @@ function testDocApi() {
});
it("GET /docs/{did}/tables/{tid}/columns retrieves columns", async function () {
const resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/columns`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data,
{
columns: [
{
id: 'A',
fields: {
colRef: 2,
parentId: 1,
parentPos: 1,
type: 'Text',
widgetOptions: '',
isFormula: false,
formula: '',
label: 'A',
description: '',
untieColIdFromLabel: false,
summarySourceCol: 0,
displayCol: 0,
visibleCol: 0,
rules: null,
recalcWhen: 0,
recalcDeps: null
}
},
{
id: 'B',
fields: {
colRef: 3,
parentId: 1,
parentPos: 2,
type: 'Text',
widgetOptions: '',
isFormula: false,
formula: '',
label: 'B',
description: '',
untieColIdFromLabel: false,
summarySourceCol: 0,
displayCol: 0,
visibleCol: 0,
rules: null,
recalcWhen: 0,
recalcDeps: null
}
},
{
id: 'C',
fields: {
colRef: 4,
parentId: 1,
parentPos: 3,
type: 'Text',
widgetOptions: '',
isFormula: false,
formula: '',
label: 'C',
description: '',
untieColIdFromLabel: false,
summarySourceCol: 0,
displayCol: 0,
visibleCol: 0,
rules: null,
recalcWhen: 0,
recalcDeps: null
}
},
{
id: 'D',
fields: {
colRef: 5,
parentId: 1,
parentPos: 3,
type: 'Any',
widgetOptions: '',
isFormula: true,
formula: '',
label: 'D',
description: '',
untieColIdFromLabel: false,
summarySourceCol: 0,
displayCol: 0,
visibleCol: 0,
rules: null,
recalcWhen: 0,
recalcDeps: null
}
},
{
id: 'E',
fields: {
colRef: 6,
parentId: 1,
parentPos: 4,
type: 'Any',
widgetOptions: '',
isFormula: true,
formula: '$A.upper()',
label: 'E',
description: '',
untieColIdFromLabel: false,
summarySourceCol: 0,
displayCol: 0,
visibleCol: 0,
rules: null,
recalcWhen: 0,
recalcDeps: null
}
const data = {
columns: [
{
id: 'A',
fields: {
colRef: 2,
parentId: 1,
parentPos: 1,
type: 'Text',
widgetOptions: '',
isFormula: false,
formula: '',
label: 'A',
description: '',
untieColIdFromLabel: false,
summarySourceCol: 0,
displayCol: 0,
visibleCol: 0,
rules: null,
recalcWhen: 0,
recalcDeps: null
}
]
}
);
},
{
id: 'B',
fields: {
colRef: 3,
parentId: 1,
parentPos: 2,
type: 'Text',
widgetOptions: '',
isFormula: false,
formula: '',
label: 'B',
description: '',
untieColIdFromLabel: false,
summarySourceCol: 0,
displayCol: 0,
visibleCol: 0,
rules: null,
recalcWhen: 0,
recalcDeps: null
}
},
{
id: 'C',
fields: {
colRef: 4,
parentId: 1,
parentPos: 3,
type: 'Text',
widgetOptions: '',
isFormula: false,
formula: '',
label: 'C',
description: '',
untieColIdFromLabel: false,
summarySourceCol: 0,
displayCol: 0,
visibleCol: 0,
rules: null,
recalcWhen: 0,
recalcDeps: null
}
},
{
id: 'D',
fields: {
colRef: 5,
parentId: 1,
parentPos: 3,
type: 'Any',
widgetOptions: '',
isFormula: true,
formula: '',
label: 'D',
description: '',
untieColIdFromLabel: false,
summarySourceCol: 0,
displayCol: 0,
visibleCol: 0,
rules: null,
recalcWhen: 0,
recalcDeps: null
}
},
{
id: 'E',
fields: {
colRef: 6,
parentId: 1,
parentPos: 4,
type: 'Any',
widgetOptions: '',
isFormula: true,
formula: '$A.upper()',
label: 'E',
description: '',
untieColIdFromLabel: false,
summarySourceCol: 0,
displayCol: 0,
visibleCol: 0,
rules: null,
recalcWhen: 0,
recalcDeps: null
}
}
]
};
const respWithTableId = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/columns`, chimpy);
assert.equal(respWithTableId.status, 200);
assert.deepEqual(respWithTableId.data, data);
const respWithTableRef = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/1/columns`, chimpy);
assert.equal(respWithTableRef.status, 200);
assert.deepEqual(respWithTableRef.data, data);
});
it('GET /docs/{did}/tables/{tid}/columns retrieves hidden columns when "hidden" is set', async function () {
@ -869,6 +894,13 @@ function testDocApi() {
]
});
// POST /columns: Create new columns using tableRef in URL
resp = await axios.post(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/5/columns`, {
columns: [{id: "NewCol6", fields: {}}],
}, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, {columns: [{id: "NewCol6"}]});
// POST /columns to invalid table ID
resp = await axios.post(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/NoSuchTable/columns`,
{columns: [{}]}, chimpy);
@ -1032,6 +1064,7 @@ function testDocApi() {
{colId: "NewCol4", label: 'NewCol4'},
{colId: "NewCol4_2", label: 'NewCol4_2'},
// NewCol5 is hidden by ACL
{colId: "NewCol6", label: 'NewCol6'},
]);
resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/NewTable2_2/columns`, chimpy);
@ -1055,13 +1088,13 @@ function testDocApi() {
});
describe("/docs/{did}/tables/{tid}/columns", function () {
async function generateDocAndUrl(docName: string = "Dummy") {
const wid = (await userApi.getOrgWorkspaces('current')).find((w) => w.name === 'Private')!.id;
const docId = await userApi.newDoc({name: docName}, wid);
const url = `${serverUrl}/api/docs/${docId}/tables/Table1/columns`;
return { url, docId };
async function generateDocAndUrlForColumns(name: string) {
const { tableUrl, docId } = await generateDocAndUrl(name);
return {
docId,
url: `${tableUrl}/columns`,
};
}
describe("PUT /docs/{did}/tables/{tid}/columns", function () {
async function getColumnFieldsMapById(url: string, params: any) {
const result = await axios.get(url, {...chimpy, params});
@ -1079,7 +1112,7 @@ function testDocApi() {
expectedFieldsByColId: Record<string, object>,
opts?: { getParams?: any }
) {
const {url} = await generateDocAndUrl('ColumnsPut');
const {url} = await generateDocAndUrlForColumns('ColumnsPut');
const body: ColumnsPut = { columns };
const resp = await axios.put(url, body, {...chimpy, params});
assert.equal(resp.status, 200);
@ -1150,7 +1183,7 @@ function testDocApi() {
it('should forbid update by viewers', async function () {
// given
const { url, docId } = await generateDocAndUrl('ColumnsPut');
const { url, docId } = await generateDocAndUrlForColumns('ColumnsPut');
await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}});
// when
@ -1162,7 +1195,7 @@ function testDocApi() {
it("should return 404 when table is not found", async function() {
// given
const { url } = await generateDocAndUrl('ColumnsPut');
const { url } = await generateDocAndUrlForColumns('ColumnsPut');
const notFoundUrl = url.replace("Table1", "NonExistingTable");
// when
@ -1176,7 +1209,7 @@ function testDocApi() {
describe("DELETE /docs/{did}/tables/{tid}/columns/{colId}", function () {
it('should delete some column', async function() {
const {url} = await generateDocAndUrl('ColumnDelete');
const {url} = await generateDocAndUrlForColumns('ColumnDelete');
const deleteUrl = url + '/A';
const resp = await axios.delete(deleteUrl, chimpy);
@ -1190,7 +1223,7 @@ function testDocApi() {
});
it('should return 404 if table not found', async function() {
const {url} = await generateDocAndUrl('ColumnDelete');
const {url} = await generateDocAndUrlForColumns('ColumnDelete');
const deleteUrl = url.replace("Table1", "NonExistingTable") + '/A';
const resp = await axios.delete(deleteUrl, chimpy);
@ -1199,7 +1232,7 @@ function testDocApi() {
});
it('should return 404 if column not found', async function() {
const {url} = await generateDocAndUrl('ColumnDelete');
const {url} = await generateDocAndUrlForColumns('ColumnDelete');
const deleteUrl = url + '/NonExistingColId';
const resp = await axios.delete(deleteUrl, chimpy);
@ -1208,7 +1241,7 @@ function testDocApi() {
});
it('should forbid column deletion by viewers', async function() {
const {url, docId} = await generateDocAndUrl('ColumnDelete');
const {url, docId} = await generateDocAndUrlForColumns('ColumnDelete');
await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}});
const deleteUrl = url + '/A';
const resp = await axios.delete(deleteUrl, kiwi);
@ -2584,6 +2617,25 @@ function testDocApi() {
assert.equal(resp2.data, 'A,B\nSanta,1\nBob,11\nAlice,2\nFelix,22\n');
});
it('GET /docs/{did}/download/csv with header=colId shows columns id in the header instead of their name',
async function () {
const { docUrl } = await generateDocAndUrl('csvWithColIdAsHeader');
const AColRef = 2;
const userActions = [
['AddRecord', 'Table1', null, {A: 'a1', B: 'b1'}],
['UpdateRecord', '_grist_Tables_column', AColRef, { untieColIdFromLabel: true }],
['UpdateRecord', '_grist_Tables_column', AColRef, {
label: 'Column label for A',
colId: 'AColId'
}]
];
const resp = await axios.post(`${docUrl}/apply`, userActions, chimpy);
assert.equal(resp.status, 200);
const csvResp = await axios.get(`${docUrl}/download/csv?tableId=Table1&header=colId`, chimpy);
assert.equal(csvResp.status, 200);
assert.equal(csvResp.data, 'AColId,B,C\na1,b1,\n');
});
it("GET /docs/{did}/download/csv respects permissions", async function () {
// kiwi has no access to TestDoc
const resp = await axios.get(`${serverUrl}/api/docs/${docIds.TestDoc}/download/csv?tableId=Table1`, kiwi);

Loading…
Cancel
Save