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