mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) updates from grist-core
This commit is contained in:
		
						commit
						e8f9da9b5c
					
				| @ -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.                                                                                                                                                                                                                                                                                    | | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
|  | ||||
| @ -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'); | ||||
| } | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
| @ -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); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
| @ -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,14 +18,15 @@ 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'; | ||||
| import * as fse from 'fs-extra'; | ||||
| import * as path from 'path'; | ||||
| import uuidv4 from "uuid/v4"; | ||||
| import { OpenMode, SQLiteDB } from './SQLiteDB'; | ||||
| 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); | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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" | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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}}." | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user