(core) updates from grist-core

This commit is contained in:
Paul Fitzpatrick 2024-10-07 10:12:15 -04:00
commit e8f9da9b5c
11 changed files with 369 additions and 42 deletions

View File

@ -275,7 +275,7 @@ Grist can be configured in many ways. Here are the main environment variables it
| GRIST_ENABLE_REQUEST_FUNCTION | enables the REQUEST function. This function performs HTTP requests in a similar way to `requests.request`. This function presents a significant security risk, since it can let users call internal endpoints when Grist is available publicly. This function can also cause performance issues. Unset by default. |
| GRIST_HIDE_UI_ELEMENTS | comma-separated list of UI features to disable. Allowed names of parts: `helpCenter,billing,templates,createSite,multiSite,multiAccounts,sendToDrive,tutorials,supportGrist`. If a part also exists in GRIST_UI_FEATURES, it will still be disabled. |
| GRIST_HOST | hostname to use when listening on a port. |
| GRIST_HTTPS_PROXY | if set, use this proxy for webhook payload delivery. |
| GRIST_HTTPS_PROXY | if set, use this proxy for webhook payload delivery or fetching custom widgets repository from url. |
| GRIST_ID_PREFIX | for subdomains of form o-*, expect or produce o-${GRIST_ID_PREFIX}*. |
| GRIST_IGNORE_SESSION | if set, Grist will not use a session for authentication. |
| GRIST_INCLUDE_CUSTOM_SCRIPT_URL | if set, will load the referenced URL in a `<script>` tag on all app pages. |

View File

@ -580,8 +580,10 @@ let dummyDocWorkerMap: DummyDocWorkerMap|null = null;
export function getDocWorkerMap(): IDocWorkerMap {
if (process.env.REDIS_URL) {
log.info("Creating Redis-based DocWorker");
return new DocWorkerMap();
} else {
log.info("Creating local/dummy DocWorker");
dummyDocWorkerMap = dummyDocWorkerMap || new DummyDocWorkerMap();
return dummyDocWorkerMap;
}

View File

@ -158,8 +158,7 @@ export class OpenAIAssistant implements Assistant {
private _maxTokens = process.env.ASSISTANT_MAX_TOKENS ?
parseInt(process.env.ASSISTANT_MAX_TOKENS, 10) : undefined;
public constructor() {
const apiKey = process.env.ASSISTANT_API_KEY || process.env.OPENAI_API_KEY;
public constructor(apiKey: string | undefined) {
const endpoint = process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT;
if (!apiKey && !endpoint) {
throw new Error('Please set either OPENAI_API_KEY or ASSISTANT_CHAT_COMPLETION_ENDPOINT');
@ -485,11 +484,13 @@ class EchoAssistant implements Assistant {
* Instantiate an assistant, based on environment variables.
*/
export function getAssistant() {
if (process.env.OPENAI_API_KEY === 'test') {
const apiKey = process.env.ASSISTANT_API_KEY || process.env.OPENAI_API_KEY;
if (apiKey === 'test') {
return new EchoAssistant();
}
if (process.env.OPENAI_API_KEY || process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT) {
return new OpenAIAssistant();
if (apiKey || process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT) {
return new OpenAIAssistant(apiKey);
}
throw new Error('Please set OPENAI_API_KEY or ASSISTANT_CHAT_COMPLETION_ENDPOINT');
}

View File

@ -638,6 +638,7 @@ export class DocManager extends EventEmitter {
throw new Error('Grist docs must be uploaded individually');
}
const first = uploadInfo.files[0].origName;
log.debug(`DocManager._doImportDoc: Received doc with name ${first}`);
const ext = extname(first);
const basename = path.basename(first, ext).trim() || "Untitled upload";
let id: string;
@ -662,6 +663,7 @@ export class DocManager extends EventEmitter {
}
await options.register?.(id, basename);
if (ext === '.grist') {
log.debug(`DocManager._doImportDoc: Importing .grist doc`);
// If the import is a grist file, copy it to the docs directory.
// TODO: We should be skeptical of the upload file to close a possible
// security vulnerability. See https://phab.getgrist.com/T457.

View File

@ -1,6 +1,9 @@
import {DocumentMetadata, HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
import {DocumentMetadata} from 'app/gen-server/lib/homedb/HomeDBManager';
import log from 'app/server/lib/log';
// Callback that persists the updated metadata to storage for each document.
export type SaveDocsMetadataFunc = (metadata: { [docId: string]: DocumentMetadata }) => Promise<any>;
/**
* HostedMetadataManager handles pushing document metadata changes to the Home database when
* a doc is updated. Currently updates doc updatedAt time and usage.
@ -29,7 +32,7 @@ export class HostedMetadataManager {
* Create an instance of HostedMetadataManager.
* The minPushDelay is the default delay in seconds between metadata pushes to the database.
*/
constructor(private _dbManager: HomeDBManager, minPushDelay: number = 60) {
constructor(private _saveDocsMetadata: SaveDocsMetadataFunc, minPushDelay: number = 60) {
this._minPushDelayMs = minPushDelay * 1000;
}
@ -68,7 +71,7 @@ export class HostedMetadataManager {
}
public setDocsMetadata(docUpdateMap: {[docId: string]: DocumentMetadata}): Promise<any> {
return this._dbManager.setDocsMetadata(docUpdateMap);
return this._saveDocsMetadata(docUpdateMap);
}
/**

View File

@ -8,7 +8,6 @@ import {DocumentUsage} from 'app/common/DocUsage';
import {buildUrlId, parseUrlId} from 'app/common/gristUrls';
import {KeyedOps} from 'app/common/KeyedOps';
import {DocReplacementOptions, NEW_DOCUMENT_CODE} from 'app/common/UserAPI';
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
import {checksumFile} from 'app/server/lib/checksumFile';
import {DocSnapshotInventory, DocSnapshotPruner} from 'app/server/lib/DocSnapshots';
import {IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
@ -19,7 +18,7 @@ import {
ExternalStorageCreator, ExternalStorageSettings,
Unchanged
} from 'app/server/lib/ExternalStorage';
import {HostedMetadataManager} from 'app/server/lib/HostedMetadataManager';
import {HostedMetadataManager, SaveDocsMetadataFunc} from 'app/server/lib/HostedMetadataManager';
import {EmptySnapshotProgress, IDocStorageManager, SnapshotProgress} from 'app/server/lib/IDocStorageManager';
import {LogMethods} from "app/server/lib/LogMethods";
import {fromCallback} from 'app/server/lib/serverUtils';
@ -27,6 +26,7 @@ import * as fse from 'fs-extra';
import * as path from 'path';
import uuidv4 from "uuid/v4";
import {OpenMode, SQLiteDB} from './SQLiteDB';
import {Features} from "app/common/Features";
// Check for a valid document id.
const docIdRegex = /^[-=_\w~%]+$/;
@ -52,6 +52,13 @@ function checkValidDocId(docId: string): void {
}
}
export interface HostedStorageCallbacks {
// Saves the given metadata for the specified documents.
setDocsMetadata: SaveDocsMetadataFunc,
// Retrieves account features enabled for the given document.
getDocFeatures: (docId: string) => Promise<Features | undefined>
}
export interface HostedStorageOptions {
secondsBeforePush: number;
secondsBeforeFirstRetry: number;
@ -133,7 +140,7 @@ export class HostedStorageManager implements IDocStorageManager {
private _docWorkerId: string,
private _disableS3: boolean,
private _docWorkerMap: IDocWorkerMap,
dbManager: HomeDBManager,
callbacks: HostedStorageCallbacks,
createExternalStorage: ExternalStorageCreator,
options: HostedStorageOptions = defaultOptions
) {
@ -144,7 +151,7 @@ export class HostedStorageManager implements IDocStorageManager {
if (!externalStoreDoc) { this._disableS3 = true; }
const secondsBeforePush = options.secondsBeforePush;
if (options.pushDocUpdateTimes) {
this._metadataManager = new HostedMetadataManager(dbManager);
this._metadataManager = new HostedMetadataManager(callbacks.setDocsMetadata);
}
this._uploads = new KeyedOps(key => this._pushToS3(key), {
delayBeforeOperationMs: secondsBeforePush * 1000,
@ -178,7 +185,7 @@ export class HostedStorageManager implements IDocStorageManager {
return path.join(dir, 'meta.json');
},
async docId => {
const features = await dbManager.getDocFeatures(docId);
const features = await callbacks.getDocFeatures(docId);
return features?.snapshotWindow;
},
);
@ -639,7 +646,7 @@ export class HostedStorageManager implements IDocStorageManager {
const existsLocally = await fse.pathExists(this.getPath(docName));
if (existsLocally) {
if (!docStatus.docMD5 || docStatus.docMD5 === DELETED_TOKEN) {
if (!docStatus.docMD5 || docStatus.docMD5 === DELETED_TOKEN || docStatus.docMD5 === 'unknown') {
// New doc appears to already exist, but may not exist in S3.
// Let's check.
const head = await this._ext.head(docName);

View File

@ -9,6 +9,7 @@ import {GristServer} from 'app/server/lib/GristServer';
import LRUCache from 'lru-cache';
import * as url from 'url';
import { AsyncCreate } from 'app/common/AsyncCreate';
import { proxyAgent } from 'app/server/lib/ProxyAgent';
// Static url for UrlWidgetRepository
const STATIC_URL = process.env.GRIST_WIDGET_LIST_URL;
@ -109,7 +110,7 @@ export class UrlWidgetRepository implements IWidgetRepository {
return [];
}
try {
const response = await fetch(this._staticUrl);
const response = await fetch(this._staticUrl, { agent: proxyAgent(new URL(this._staticUrl)) });
if (!response.ok) {
if (response.status === 404) {
throw new ApiError('WidgetRepository: Remote widget list not found', 404);

View File

@ -89,7 +89,9 @@ export function makeGristConfig(options: MakeGristConfigOptions): GristLoadConfi
supportedLngs: readLoadedLngs(req?.i18n),
namespaces: readLoadedNamespaces(req?.i18n),
featureComments: isAffirmative(process.env.COMMENTS),
featureFormulaAssistant: Boolean(process.env.OPENAI_API_KEY || process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT),
featureFormulaAssistant: Boolean(process.env.OPENAI_API_KEY ||
process.env.ASSISTANT_API_KEY ||
process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT),
assistantService: process.env.OPENAI_API_KEY ? 'OpenAI' : undefined,
permittedCustomWidgets: getPermittedCustomWidgets(server),
supportEmail: SUPPORT_EMAIL,

View File

@ -71,7 +71,7 @@
"Sign Out": "Amaitu saioa",
"Sign in": "Hasi saioa",
"Switch Accounts": "Aldatu kontua",
"Support Grist": "Babestu Grist",
"Support Grist": "Eman babesa Grist-i",
"Sign In": "Hasi saioa",
"Sign Up": "Eman izena",
"Use This Template": "Txantiloi hau erabili",
@ -148,7 +148,8 @@
"Cut": "Ebaki",
"Paste": "Itsatsi",
"Clear cell": "Garbitu gelaxka",
"Filter by this value": "Iragazi balio honen arabera"
"Filter by this value": "Iragazi balio honen arabera",
"Copy with headers": "Kopiatu goiburuekin"
},
"ColorSelect": {
"Apply": "Aplikatu",
@ -192,7 +193,15 @@
"No {{columnType}} columns in table.": "Ez dago {{columnType}} zutaberik taulan.",
" (optional)": " (aukerakoa)",
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} ez dira {{columnType}} zutabeak erakusten",
"Full document access": "Sarbide osoa dokumentura"
"Full document access": "Sarbide osoa dokumentura",
"Accept": "Onartu",
"Developer:": "Garatzailea:",
"Last updated:": "Azkenekoz eguneratua:",
"Missing description and author information.": "Ez dago deskribapen ezta egilearen informaziorik ere.",
"Reject": "Baztertu",
"ACCESS LEVEL": "SARBIDE-MAILA",
"Custom URL": "URL pertsonalizatua",
"Widget": "Widgeta"
},
"DataTables": {
"Click to copy": "Egin klik kopiatzeko",
@ -257,7 +266,11 @@
"Workspace not found": "Ez da lan-eremua aurkitu",
"You are on the {{siteName}} site. You also have access to the following sites:": "{{siteName}} gunean zaude. Honako gune hauetara ere sar zaitezke:",
"You are on your personal site. You also have access to the following sites:": "Zure leku pertsonalean zaude. Honako gune hauetara ere sar zaitezke:",
"You may delete a workspace forever once it has no documents in it.": "Lan-eremu bat betiko ezabatzeko ezin du barruan dokumenturik izan."
"You may delete a workspace forever once it has no documents in it.": "Lan-eremu bat betiko ezabatzeko ezin du barruan dokumenturik izan.",
"Create my first document": "Sortu nire lehen dokumentua",
"Any documents created in this site will appear here.": "Gune honetan sortutako dokumentuak hemen agertuko dira.",
"personal site": "gune pertsonala",
"You have read-only access to this site. Currently there are no documents.": "Soilik irakurtzeko sarbidea duzu gune honetan. Unean ez dago dokumenturik."
},
"DocPageModel": {
"Add Empty Table": "Gehitu taula hutsa",
@ -493,7 +506,8 @@
"Get started by exploring templates, or creating your first Grist document.": "Has zaitez txantiloiak arakatuz edo zure lehen Grist dokumentua sortuz.",
"Get started by inviting your team and creating your first Grist document.": "Has zaitez zure taldea gonbidatuz eta zure lehen Grist dokumentua sortuz.",
"Interested in using Grist outside of your team? Visit your free ": "Grist zure taldetik kanpo erabili nahi duzu? Bisitatu zure doako ",
"Sprouts Program": "Kimuen programa"
"Sprouts Program": "Kimuen programa",
"Only show documents": "Erakutsi dokumentuak bakarrik"
},
"HomeLeftPane": {
"All Documents": "Dokumentu guztiak",
@ -720,7 +734,8 @@
"API Console": "API kontsola"
},
"TopBar": {
"Manage Team": "Kudeatu taldea"
"Manage Team": "Kudeatu taldea",
"Manage team": "Kudeatu taldea"
},
"TriggerFormulas": {
"Cancel": "Utzi",
@ -844,7 +859,9 @@
"You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "{{email}} gisa hasi duzu saioa. Beste kontu batekin hasi dezakezu saioa, edo administratzaile bati sarbidea eskatu.",
"Build your own form": "Sortu zure formularioa",
"Powered by": "Honi esker:",
"Account deleted{{suffix}}": "Kontua ezabatu da{{suffix}}"
"Account deleted{{suffix}}": "Kontua ezabatu da{{suffix}}",
"Failed to log in.{{separator}}Please try again or contact support.": "Saioaren hasierak huts egin du.{{separator}}Saiatu berriro edo jarri harremanetan laguntza eskatzeko.",
"Sign-in failed{{suffix}}": "Saioaren hasierak huts egin du{{suffix}}"
},
"menus": {
"Select fields": "Hautatu eremuak",
@ -1083,7 +1100,14 @@
"Select the table to link to.": "Hautatu lotu beharreko taula.",
"Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.": "Erabili 𝚺 ikonoa laburpen-taulak edo taula dinamikoak sortzeko, guztizkoetarako edo guztizko partzialetarako.",
"Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Ez dituzu zutabe egokiak aurkitzen? Egin klik \"Aldatu widgeta\"-n gertaeren datuak dituen taula hautatzeko.",
"Clicking {{EyeHideIcon}} in each cell hides the field from this view without deleting it.": "Gelaxka bakoitzeko {{EyeHideIcon}}-en klik eginez gero, eremua ikuspegi honetatik ezkutatuko da ezabatu gabe."
"Clicking {{EyeHideIcon}} in each cell hides the field from this view without deleting it.": "Gelaxka bakoitzeko {{EyeHideIcon}}-en klik eginez gero, eremua ikuspegi honetatik ezkutatuko da ezabatu gabe.",
"Creates a reverse column in target table that can be edited from either end.": "Helburuko taulan bi muturretatik editatu daitekeen alderantzizko zutabe bat sortzen du.",
"To allow multiple assignments, change the type of the Reference column to Reference List.": "Esleipen bat baino gehiago baimentzeko, aldatu erreferentzia-zutabearen mota Erreferentzia zerrendara.",
"This limitation occurs when one end of a two-way reference is configured as a single Reference.": "Muga hau bi noranzkoko erreferentzia bateko muturretako bat erreferentzia bakar gisa konfiguratuta dagoenean ematen da.",
"Community widgets are created and maintained by Grist community members.": "Komunitatearen widgetak Gristen komunitateko kideek sortzen eta mantentzen dituztenak dira.",
"To allow multiple assignments, change the referenced column's type to Reference List.": "Esleipen bat baino gehiago baimentzeko, aldatu erreferentzia-zutabearen mota Erreferentzia zerrendara.",
"This limitation occurs when one column in a two-way reference has the Reference type.": "Muga hau bi noranzkoko erreferentzia bateko zutabetako bat erreferentzia mota gisa konfiguratuta dagoenean ematen da.",
"Two-way references are not currently supported for Formula or Trigger Formula columns": "Bi noranzkoko erreferentziak ez dira unean bateragarriak Formula edo formula-abiarazle zutabeekin."
},
"ColumnTitle": {
"Column ID copied to clipboard": "Zutabearen IDa arbelera kopiatu da",
@ -1283,7 +1307,7 @@
},
"UserManager": {
"Add {{member}} to your team": "Gehitu {{member}} zure taldean",
"Allow anyone with the link to open.": "Utzi esteka duen edonori irekitzen.",
"Allow anyone with the link to open.": "Baimendu esteka duen edonori irekitzea.",
"Confirm": "Baieztatu",
"Copy Link": "Kopiatu esteka",
"Guest": "Gonbidatua",
@ -1330,11 +1354,11 @@
"Help Center": "Laguntza Gunea",
"Opt in to Telemetry": "Bidali telemetria",
"Support Grist": "Eman babesa Grist-i",
"Support Grist page": "Eman babesa Grist-en orriari",
"Support Grist page": "Grist-en laguntza orria",
"Opted In": "Izena emanda",
"Close": "Itxi",
"Contribute": "Hartu parte",
"Thank you! Your trust and support is greatly appreciated. Opt out any time from the {{link}} in the user menu.": "Mila esker! Zure konfiantza eta babesa oso estimatua da. Edozein unetan bidaltzeari utzi diezaiokezu erabiltzailearen menuko {{link}}tik.",
"Thank you! Your trust and support is greatly appreciated. Opt out any time from the {{link}} in the user menu.": "Mila esker! Zure konfiantza eta babesa oso estimatuak dira. Edozein unetan bidaltzeari utzi diezaiokezu erabiltzailearen menuko {{link}}tik.",
"Admin Panel": "Administratzailearen mahaigaina"
},
"SupportGristPage": {
@ -1537,7 +1561,8 @@
"Grist allows for very powerful formulas, using Python. We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor if your hardware supports it (most will), to run formulas in each document within a sandbox isolated from other documents and isolated from the network.": "Gristek formula oso boteretsuak onartzen ditu, Python erabiliz. Dokumentu bakoitzean beste dokumentu batzuetatik eta saretik isolatutako sandbox baten barruan formulak exekutatzeko, GRIST_SANDBOX_FLAVOR aldagaia gvisor-era aldatzea gomendatzen dugu, zure hardwarea bateragarria bada (gehienak badira).",
"Session Secret": "Saioaren gakoa",
"Enable Grist Enterprise": "Gaitu Grist Enterprise",
"Enterprise": "Enterprise"
"Enterprise": "Enterprise",
"checking": "egiaztatzen"
},
"Columns": {
"Remove Column": "Kendu zutabea"
@ -1649,7 +1674,8 @@
"3 minute video tour": "3 minutuko bideo-bisitaldia",
"Complete our basics tutorial": "Burutu oinarrizko tutoriala",
"Complete the tutorial": "Burutu tutoriala",
"Learn the basic of reference columns, linked widgets, column types, & cards.": "Ikasi erreferentzia-zutabeen, lotutako widgeten, zutabe-moten eta txartelen oinarriak."
"Learn the basic of reference columns, linked widgets, column types, & cards.": "Ikasi erreferentzia-zutabeen, lotutako widgeten, zutabe-moten eta txartelen oinarriak.",
"Learn the basics of reference columns, linked widgets, column types, & cards.": "Ikasi erreferentzia-zutabeen, lotutako widgeten, zutabe moten, eta txartelen oinarriak."
},
"OnboardingPage": {
"Go hands-on with the Grist Basics tutorial": "Murgildu Gristen oinarrizko tutorialean",
@ -1680,5 +1706,87 @@
"Table {{tableName}} will no longer be visible": "{{tableName}} taula ez da ikusgai egongo aurrerantzean",
"raw data page": "datu gordinen orria",
"Delete": "Ezabatu"
},
"CustomWidgetGallery": {
"Add Your Own Widget": "Gehitu zure widgeta",
"Cancel": "Utzi",
"Change Widget": "Aldatu widgeta",
"Developer:": "Garatzailea:",
"Last updated:": "Azkenekoz eguneratua:",
"Search": "Bilatu",
"Widget URL": "Widgetaren URLa",
"Add Widget": "Gehitu widgeta",
"(Missing info)": "(Informaziorik gabe)",
"Grist Widget": "Grist widgeta",
"No matching widgets": "Ez dago bat datorren widgetik",
"Add a widget from outside this gallery.": "Gehitu galeria honetatik kanpoko widgetak.",
"Choose Custom Widget": "Aukeratu widget pertsonalizatua",
"Community Widget": "Komunitatearen widgetak",
"Custom URL": "URL pertsonalizatua",
"Learn more about Custom Widgets": "Ikasi gehiago widget pertsonalizatuei buruz"
},
"HomeIntroCards": {
"Help center": "Laguntza gunea",
"Learn more {{webinarsLinks}}": "Ikasi gehiago {{webinarsLinks}}",
"Start a new document": "Hasi dokuemntu berria",
"Templates": "Txantiloiak",
"Blank document": "Dokumentu zuria",
"Import file": "Inportatu fitxategia",
"3 minute video tour": "3 minutuko bideo-bisitaldia",
"Finish our basics tutorial": "Amaitu oinarrizko tutoriala",
"Tutorial": "Tutoriala",
"Webinars": "Web-mintegiak",
"Find solutions and explore more resources {{helpCenterLink}}": "Ikusi adibideak eta arakatu baliabide gehiago {{helpCenterLink}}"
},
"ReverseReferenceConfig": {
"Delete": "Ezabatu",
"Table": "Taula",
"Target table": "Helmugako taula",
"Delete column {{column}} in table {{table}}?": "{{table}} taulako {{column}} zutabea ezabatu nahi duzu?",
"Column": "Zutabea",
"Add two-way reference": "Gehitu bi noranzkoko erreferentzia",
"Two-way Reference": "Bi noranzkoko erreferentzia",
"Delete two-way reference?": "Bi noranzkoko erreferentzia ezabatu?",
"It is the reverse of the reference column {{column}} in table {{table}}.": "{{table}} taulako {{column}} zutabearen alderantzizko erreferentzia da."
},
"SupportGristButton": {
"Close": "Itxi",
"Help Center": "Laguntza gunea",
"Admin Panel": "Administratzailearen mahaigaina",
"Support Grist": "Eman babesa Grist-i",
"Opted In": "Telemetria bidaltzen da",
"Thank you! Your trust and support is greatly appreciated. Opt out any time from the {{link}} in the user menu.": "Mila esker! Zure konfiantza eta babesa oso estimatuak dira. Edozein unetan bidaltzeari utzi diezaiokezu erabiltzailearen menuko {{link}}tik.",
"Opt in to Telemetry": "Bidali telemetria"
},
"buildReassignModal": {
"Cancel": "Utzi",
"Each {{targetTable}} record may only be assigned to a single {{sourceTable}} record.": "{{targetTable}} erregistro bakoitzari {{sourceTable}} erregistro bakarra esleitu dakioke.",
"Reassign to {{sourceTable}} record {{sourceName}}.": "Berresleitu {{sourceTable}}(e)ko {{sourceName}} erregistroari.",
"Record already assigned_one": "Erregistroa esleituta dago lehendik ere",
"Record already assigned_other": "Erregistroa esleituta dago lehendik ere",
"Reassign": "Berresleitu",
"Reassign to new {{sourceTable}} records.": "Berresleitu {{sourceTable}}(e)ko erregistro berri bati.",
"{{targetTable}} record {{targetName}} is already assigned to {{sourceTable}} record {{oldSourceName}}.": "{{targetTable}}(e)ko {{targetName}} erregistroa lehendik dago {{sourceTable}}(e)ko {{oldSourceName}} erregistroari esleituta."
},
"AdminPanelName": {
"Admin Panel": "Administratzailearen mahaigaina"
},
"markdown": {
"# New Markdown Function\n *\n * We can _write_ [the usual Markdown](https:": {
"": {
"markdownguide.org) *inside*\n * a Grainjs element.": "# Markdown funtzio berria\n *\n * Grainjs elementu baten *barruan*\n * [ohiko Markdown sintaxia](https://markdownguide.org) _idatz_ dezakegu."
}
},
"The toggle is **off**": "Ezaugarria **desaktibatuta** dago",
"The toggle is **on**": "Ezaugarria **aktibatuta** dago"
},
"markdown.d": {
"# New Markdown Function\n *\n * We can _write_ [the usual Markdown](https:": {
"": {
"markdownguide.org) *inside*\n * a Grainjs element.": "# Markdown funtzio berria\n *\n * Grainjs elementu baten *barruan*\n * [ohiko Markdown sintaxia](https://markdownguide.org) _idatz_ dezakegu."
}
},
"The toggle is **off**": "Ezaugarria **desaktibatuta** dago",
"The toggle is **on**": "Ezaugarria **aktibatuta** dago"
}
}

View File

@ -133,7 +133,8 @@
"Cut": "Izreži",
"Paste": "Prilepi",
"Clear values": "Izbriši vrednosti",
"Clear cell": "Čista celica"
"Clear cell": "Čista celica",
"Copy with headers": "Kopiraj z glavami"
},
"DocMenu": {
"Document will be moved to Trash.": "Dokument se bo premaknil v koš.",
@ -174,7 +175,11 @@
"Restore": "Obnovi",
"Move {{name}} to workspace": "Premakni {{name}} v delovni prostor",
"You are on the {{siteName}} site. You also have access to the following sites:": "Nahajate se na spletnem mestu {{siteName}}. Prav tako imate dostop do naslednjih spletnih mest:",
"Examples & Templates": "Primeri & predloge"
"Examples & Templates": "Primeri & predloge",
"Any documents created in this site will appear here.": "Vsi dokumenti, ustvarjeni na tem mestu, bodo prikazani tukaj.",
"Create my first document": "Ustvari moj prvi dokument",
"personal site": "osebno spletno mesto",
"You have read-only access to this site. Currently there are no documents.": "Do tega mesta imate dostop samo za branje. Trenutno ni dokumentov."
},
"GridViewMenus": {
"Rename column": "Preimenuj stolpec",
@ -407,7 +412,15 @@
"{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} stolpci, ki niso{{columnType}}, niso prikazani",
"Widget needs to {{read}} the current table.": "Widget mora {{read}} trenutno tabelo.",
"No {{columnType}} columns in table.": "V tabeli ni stolpcev tipa {{columnType}}.",
"Clear selection": "Briši izbor"
"Clear selection": "Briši izbor",
"Custom URL": "URL po meri",
"Developer:": "Razvijalec:",
"ACCESS LEVEL": "STOPNJA DOSTOPOV",
"Reject": "Zavrni",
"Accept": "Sprejmi",
"Last updated:": "Zadnja posodobitev:",
"Missing description and author information.": "Manjka opis in podatki o avtorju.",
"Widget": "Widget"
},
"DocHistory": {
"Activity": "Dejavnost",
@ -784,7 +797,14 @@
"Build simple forms right in Grist and share in a click with our new widget. {{learnMoreButton}}": "Ustvari preproste obrazce neposredno v Gristu in jih deli z enim klikom z našim novim pripomočkom. {{learnMoreButton}}",
"These rules are applied after all column rules have been processed, if applicable.": "Ta pravila se uporabijo, ko so obdelana vsa pravila stolpcev, če so na voljo.",
"Filter displayed dropdown values with a condition.": "Filtriraj prikazane vrednosti .",
"Example: {{example}}": "Primer: {{example}}"
"Example: {{example}}": "Primer: {{example}}",
"Creates a reverse column in target table that can be edited from either end.": "Ustvari vzvratni stolpec v ciljni tabeli, ki ga je mogoče urejati z obeh koncev.",
"To allow multiple assignments, change the type of the Reference column to Reference List.": "Če želiš dovoliti več dodelitev, spremeni vrsto stolpca Reference v Seznam referenc.",
"To allow multiple assignments, change the referenced column's type to Reference List.": "Če želiš omogočiti več dodelitev, spremeni vrsto referenčnega stolpca v Referenčni seznam.",
"Two-way references are not currently supported for Formula or Trigger Formula columns": "Dvosmerne reference trenutno niso podprte za stolpce formule ali formule sprožilca",
"Community widgets are created and maintained by Grist community members.": "Pripomočke skupnosti ustvarjajo in vzdržujejo člani skupnosti Grist.",
"This limitation occurs when one end of a two-way reference is configured as a single Reference.": "Do te omejitve pride, ko je en konec dvosmerne reference konfiguriran kot ena sama referenca.",
"This limitation occurs when one column in a two-way reference has the Reference type.": "Do te omejitve pride, ko ima en stolpec v dvosmernem sklicu vrsto Reference."
},
"UserManager": {
"Anyone with link ": "Vsakdo s povezavo ",
@ -922,7 +942,8 @@
"Visit our {{link}} to learn more about Grist.": "Obiščite našo spletno stran {{link}} da izveste več o Grisstu.",
"Sign in": "Prijavi se",
"To use Grist, please either sign up or sign in.": "Če želiš uporabljati Grist, se prijavi ali prvič prijavi.",
"Learn more in our {{helpCenterLink}}.": "Izvedi več v našem {{helpCenterLink}}."
"Learn more in our {{helpCenterLink}}.": "Izvedi več v našem {{helpCenterLink}}.",
"Only show documents": "Pokaži samo dokumente"
},
"WelcomeSitePicker": {
"You have access to the following Grist sites.": "Imate dostop do naslednjih Grist spletnih mest .",
@ -1101,7 +1122,8 @@
"Rule {{length}}": "Pravilo {{length}}"
},
"TopBar": {
"Manage Team": "Upravljanje ekipe"
"Manage Team": "Upravljanje ekipe",
"Manage team": "Uredi ekipo"
},
"UserManagerModel": {
"View & Edit": "Ogled in urejanje",
@ -1175,7 +1197,9 @@
"Build your own form": "Ustvari svoj obrazec",
"Powered by": "Poganja ga",
"An unknown error occurred.": "Prišlo je do neznane napake.",
"Form not found": "Ne najdem obrazca"
"Form not found": "Ne najdem obrazca",
"Sign-in failed{{suffix}}": "Prijava ni uspela{{suffix}}",
"Failed to log in.{{separator}}Please try again or contact support.": "Prijava ni uspela.{{separator}}Poskusi znova ali se obrni na podporo."
},
"WidgetTitle": {
"DATA TABLE NAME": "IME PODATKOVNE TABELE",
@ -1560,7 +1584,8 @@
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "Grist podpisuje piškotke uporabniške seje s skrivnim ključem. Ta ključ nastavite prek spremenljivke okolja GRIST_SESSION_SECRET. Grist se vrne na trdo kodirano privzeto vrednost, če ni nastavljena. To obvestilo bomo morda odstranili v prihodnosti, saj so ID-ji sej, ustvarjeni od različice 1.1.16, sami po sebi kriptografsko varni.",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.": "Grist podpisuje piškotke uporabniške seje s skrivnim ključem. Ta ključ nastavite prek spremenljivke okolja GRIST_SESSION_SECRET. Grist se vrne na trdo kodirano privzeto vrednost, če ni nastavljena. To obvestilo bomo morda odstranili v prihodnosti, saj so ID-ji sej, ustvarjeni od različice 1.1.16, sami po sebi kriptografsko varni.",
"Enable Grist Enterprise": "Omogoči Grist Enterprise",
"Enterprise": "Podjetje"
"Enterprise": "Podjetje",
"checking": "preverjanje"
},
"ChoiceEditor": {
"Error in dropdown condition": "Napaka v spustnem meniju",
@ -1649,7 +1674,8 @@
"3 minute video tour": "3 minutni video ogled",
"Complete our basics tutorial": "Dokončaj našo vadnico o osnovah",
"Complete the tutorial": "Dokončaj vadnico",
"Learn the basic of reference columns, linked widgets, column types, & cards.": "Nauči se osnov referenčnih stolpcev, povezanih pripomočkov, vrst stolpcev in kartic."
"Learn the basic of reference columns, linked widgets, column types, & cards.": "Nauči se osnov referenčnih stolpcev, povezanih pripomočkov, vrst stolpcev in kartic.",
"Learn the basics of reference columns, linked widgets, column types, & cards.": "Naučite se osnov referenčnih stolpcev, povezanih pripomočkov, vrst stolpcev in kartic."
},
"OnboardingPage": {
"Back": "Nazaj",
@ -1680,5 +1706,87 @@
"Keep data and delete widget. Table will remain available in {{rawDataLink}}": "Ohranite podatke in izbrišite pripomoček. Tabela bo ostala na voljo v {{rawDataLink}}",
"Table {{tableName}} will no longer be visible": "Tabela {{tableName}} ne bo več vidna",
"raw data page": "stran z neobdelanimi podatki"
},
"AdminPanelName": {
"Admin Panel": "Skrbniška plošča"
},
"CustomWidgetGallery": {
"(Missing info)": "(Manjkajo informacije)",
"Add Widget": "Dodaj pripomoček",
"Add Your Own Widget": "Dodajte svoj pripomoček",
"Add a widget from outside this gallery.": "Dodaj pripomoček zunaj te galerije.",
"Cancel": "Prekliči",
"Change Widget": "Spremeni pripomoček",
"Developer:": "razvijalec:",
"Grist Widget": "Grist Pripomoček",
"Learn more about Custom Widgets": "Izvedite več o pripomočkih po meri",
"No matching widgets": "Ni ujemajočih se pripomočkov",
"Search": "Iskanje",
"Widget URL": "URL pripomočka",
"Choose Custom Widget": "Izberite Pripomoček po meri",
"Community Widget": "Pripomoček skupnosti",
"Custom URL": "URL po meri",
"Last updated:": "Zadnja posodobitev:"
},
"markdown": {
"The toggle is **off**": "Preklop je **izklopljen**",
"The toggle is **on**": "Preklop je **vklopljen**",
"# New Markdown Function\n *\n * We can _write_ [the usual Markdown](https:": {
"": {
"markdownguide.org) *inside*\n * a Grainjs element.": "# Nova funkcija Markdown\n *\n * Lahko _napišemo_ [običajni Markdown](https://markdownguide.org) *znotraj*\n * element Grainjs."
}
}
},
"markdown.d": {
"The toggle is **on**": "Preklop je **vklopljen**",
"The toggle is **off**": "Preklop je **izklopljen**",
"# New Markdown Function\n *\n * We can _write_ [the usual Markdown](https:": {
"": {
"markdownguide.org) *inside*\n * a Grainjs element.": "# Nova funkcija Markdown\n *\n * Lahko _napišemo_ [običajni Markdown](https://markdownguide.org) *znotraj*\n * element Grainjs."
}
}
},
"HomeIntroCards": {
"3 minute video tour": "3 minutni video ogled",
"Blank document": "Prazen dokument",
"Finish our basics tutorial": "Dokončaj našo vadnico o osnovah",
"Learn more {{webinarsLinks}}": "Več o tem {{webinarsLinks}}",
"Start a new document": "Začni nov dokument",
"Templates": "Predloge",
"Tutorial": "Vadnica",
"Find solutions and explore more resources {{helpCenterLink}}": "Poiščite rešitve in raziščite več virov {{helpCenterLink}}",
"Help center": "Center za pomoč",
"Import file": "Uvozi datoteko",
"Webinars": "Spletni seminarji"
},
"ReverseReferenceConfig": {
"Add two-way reference": "Dodaj dvosmerno referenco",
"Delete column {{column}} in table {{table}}?": "Želiš izbrisati stolpec {{column}} v tabeli {{table}}?",
"It is the reverse of the reference column {{column}} in table {{table}}.": "Je obratna stran referenčnega stolpca {{column}} v tabeli {{table}}.",
"Two-way Reference": "Dvosmerna referenca",
"Delete two-way reference?": "Želiš izbrisati dvosmerno referenco?",
"Target table": "Ciljna tabela",
"Column": "Stolpec",
"Delete": "Izbriši",
"Table": "Tabela"
},
"SupportGristButton": {
"Admin Panel": "Skrbniška plošča",
"Close": "Zapri",
"Help Center": "Center za pomoč",
"Opted In": "Omogočeno",
"Opt in to Telemetry": "Omogoči uporabo telemetrije",
"Support Grist": "Podpora Gristu",
"Thank you! Your trust and support is greatly appreciated. Opt out any time from the {{link}} in the user menu.": "Hvala! Vaše zaupanje in podporo zelo cenimo. Kadar koli se odjavite na {{link}} v uporabniškem meniju."
},
"buildReassignModal": {
"Cancel": "Prekliči",
"Each {{targetTable}} record may only be assigned to a single {{sourceTable}} record.": "Vsak zapis {{targetTable}} je lahko dodeljen samo enemu zapisu {{sourceTable}}.",
"Reassign": "Prerazporedi",
"Reassign to new {{sourceTable}} records.": "Ponovno dodelite novim zapisom {{sourceTable}}.",
"Reassign to {{sourceTable}} record {{sourceName}}.": "Znova dodeli zapisu {{sourceTable}} {{sourceName}}.",
"Record already assigned_one": "Zapis je že dodeljen",
"Record already assigned_other": "Zapis je že dodeljen",
"{{targetTable}} record {{targetName}} is already assigned to {{sourceTable}} record {{oldSourceName}}.": "Zapis {{targetTable}} {{targetName}} je že dodeljen zapisu {{sourceTable}} {{oldSourceName}}."
}
}

View File

@ -1,12 +1,13 @@
import {ErrorOrValue, freezeError, mapGetOrSet, MapWithTTL} from 'app/common/AsyncCreate';
import {ObjMetadata, ObjSnapshot, ObjSnapshotWithMetadata} from 'app/common/DocSnapshot';
import {SCHEMA_VERSION} from 'app/common/schema';
import {DocWorkerMap} from 'app/gen-server/lib/DocWorkerMap';
import {DocWorkerMap, getDocWorkerMap} from 'app/gen-server/lib/DocWorkerMap';
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {create} from 'app/server/lib/create';
import {DocManager} from 'app/server/lib/DocManager';
import {makeExceptionalDocSession} from 'app/server/lib/DocSession';
import {IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
import {
DELETED_TOKEN,
ExternalStorage, ExternalStorageCreator,
@ -274,7 +275,7 @@ class TestStore {
public constructor(
private _localDirectory: string,
private _workerId: string,
private _workers: DocWorkerMap,
private _workers: IDocWorkerMap,
private _externalStorageCreate: ExternalStorageCreator) {
}
@ -962,6 +963,98 @@ describe('HostedStorageManager', function() {
});
});
}
describe('minio-without-redis', async () => {
const workerId = 'dw17';
let tmpDir: string;
let oldEnv: EnvironmentSnapshot;
let docWorkerMap: IDocWorkerMap;
let externalStorageCreate: ExternalStorageCreator;
let defaultParams: ConstructorParameters<typeof HostedStorageManager>;
before(async function() {
tmpDir = await createTmpDir();
oldEnv = new EnvironmentSnapshot();
// Disable Redis
delete process.env.REDIS_URL;
const storage = create?.getStorageOptions?.('minio');
const creator = storage?.create;
if (!creator || !storage?.check()) {
return this.skip();
}
externalStorageCreate = creator;
});
after(async () => {
oldEnv.restore();
});
beforeEach(async function() {
// With Redis disabled, this should be the non-redis version of IDocWorkerMap (DummyDocWorkerMap)
docWorkerMap = getDocWorkerMap();
await docWorkerMap.addWorker({
id: workerId,
publicUrl: "none",
internalUrl: "none",
});
await docWorkerMap.setWorkerAvailability(workerId, true);
defaultParams = [
tmpDir,
workerId,
false,
docWorkerMap,
{
setDocsMetadata: async (metadata) => {},
getDocFeatures: async (docId) => undefined,
},
externalStorageCreate,
];
});
it("doesn't wipe local docs when they exist on disk but not remote storage", async function() {
const storageManager = new HostedStorageManager(...defaultParams);
const docId = "NewDoc";
const path = storageManager.getPath(docId);
// Simulate an uploaded .grist file.
await fse.writeFile(path, "");
await storageManager.prepareLocalDoc(docId);
assert.isTrue(await fse.pathExists(path));
});
it("fetches remote docs if they don't exist locally", async function() {
const testStore = new TestStore(
tmpDir,
workerId,
docWorkerMap,
externalStorageCreate
);
let docName: string = "";
let docPath: string = "";
await testStore.run(async () => {
const newDoc = await testStore.docManager.createNewEmptyDoc(docSession, "NewRemoteDoc");
docName = newDoc.docName;
docPath = testStore.storageManager.getPath(docName);
});
// This should be safe since testStore.run closes everything down.
await fse.remove(docPath);
assert.isFalse(await fse.pathExists(docPath));
await testStore.run(async () => {
await testStore.docManager.fetchDoc(docSession, docName);
});
assert.isTrue(await fse.pathExists(docPath));
});
});
});
// This is a performance test, to check if the backup settings are plausible.