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
						8053c81d02
					
				@ -250,6 +250,7 @@ Variable | Purpose
 | 
			
		||||
-------- | -------
 | 
			
		||||
ALLOWED_WEBHOOK_DOMAINS | comma-separated list of permitted domains to use in webhooks (e.g. webhook.site,zapier.com). You can set this to `*` to allow all domains, but if doing so, we recommend using a carefully locked-down proxy (see `GRIST_HTTPS_PROXY`) if you do not entirely trust users. Otherwise services on your internal network may become vulnerable to manipulation.
 | 
			
		||||
APP_DOC_URL | doc worker url, set when starting an individual doc worker (other servers will find doc worker urls via redis)
 | 
			
		||||
APP_DOC_INTERNAL_URL | like `APP_DOC_URL` but used by the home server to reach the server using an internal domain name resolution (like in a docker environment). Defaults to `APP_DOC_URL`
 | 
			
		||||
APP_HOME_URL | url prefix for home api (home and doc servers need this)
 | 
			
		||||
APP_STATIC_URL | url prefix for static resources
 | 
			
		||||
APP_STATIC_INCLUDE_CUSTOM_CSS | set to "true" to include custom.css (from APP_STATIC_URL) in static pages
 | 
			
		||||
 | 
			
		||||
@ -551,10 +551,12 @@ export class CustomSectionConfig extends Disposable {
 | 
			
		||||
    // Options for the select-box (all widgets definitions and Custom URL)
 | 
			
		||||
    const options = Computed.create(holder, use => [
 | 
			
		||||
      {label: 'Custom URL', value: 'custom'},
 | 
			
		||||
      ...(use(this._widgets) || []).map(w => ({
 | 
			
		||||
        label: w.source?.name ? `${w.name} (${w.source.name})` : w.name,
 | 
			
		||||
        value: (w.source?.pluginId || '') + ':' + w.widgetId,
 | 
			
		||||
      })),
 | 
			
		||||
      ...(use(this._widgets) || [])
 | 
			
		||||
           .filter(w => w?.published !== false)
 | 
			
		||||
           .map(w => ({
 | 
			
		||||
             label: w.source?.name ? `${w.name} (${w.source.name})` : w.name,
 | 
			
		||||
             value: (w.source?.pluginId || '') + ':' + w.widgetId,
 | 
			
		||||
           })),
 | 
			
		||||
    ]);
 | 
			
		||||
    function buildPrompt(level: AccessLevel|null) {
 | 
			
		||||
      if (!level) {
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,11 @@ export interface ICustomWidget {
 | 
			
		||||
   */
 | 
			
		||||
  renderAfterReady?: boolean;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * If set to false, do not offer to user in UI.
 | 
			
		||||
   */
 | 
			
		||||
  published?: boolean;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * If the widget came from a plugin, we track that here.
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
@ -167,6 +167,12 @@ export interface OrgUrlInfo {
 | 
			
		||||
  orgInPath?: string;     // If /o/{orgInPath} should be used to access the requested org.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isDocInternalUrl(host: string) {
 | 
			
		||||
  if (!process.env.APP_DOC_INTERNAL_URL) { return false; }
 | 
			
		||||
  const internalUrl = new URL('/', process.env.APP_DOC_INTERNAL_URL);
 | 
			
		||||
  return internalUrl.host === host;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Given host (optionally with port), baseDomain, and pluginUrl, determine whether to interpret host
 | 
			
		||||
 * as a custom domain, a native domain, or a plugin domain.
 | 
			
		||||
@ -184,8 +190,10 @@ export function getHostType(host: string, options: {
 | 
			
		||||
 | 
			
		||||
  const hostname = host.split(":")[0];
 | 
			
		||||
  if (!options.baseDomain) { return 'native'; }
 | 
			
		||||
  if (hostname !== 'localhost' && !hostname.endsWith(options.baseDomain)) { return 'custom'; }
 | 
			
		||||
  return 'native';
 | 
			
		||||
  if (hostname === 'localhost' || isDocInternalUrl(host) || hostname.endsWith(options.baseDomain)) {
 | 
			
		||||
    return 'native';
 | 
			
		||||
  }
 | 
			
		||||
  return 'custom';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getOrgUrlInfo(newOrg: string, currentHost: string, options: OrgUrlOptions): OrgUrlInfo {
 | 
			
		||||
 | 
			
		||||
@ -209,8 +209,8 @@ class CachedWidgetRepository extends WidgetRepositoryImpl {
 | 
			
		||||
    return list;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public testOverrideUrl(url: string) {
 | 
			
		||||
    super.testOverrideUrl(url);
 | 
			
		||||
  public testOverrideUrl(overrideUrl: string) {
 | 
			
		||||
    super.testOverrideUrl(overrideUrl);
 | 
			
		||||
    this._cache.reset();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -267,7 +267,7 @@ export function getWidgetsInPlugins(gristServer: GristServer,
 | 
			
		||||
        gristServer.getTag() + '/widgets/' + plugin.id + '/';
 | 
			
		||||
    places.push({
 | 
			
		||||
      urlBase,
 | 
			
		||||
      dir: plugin.path,
 | 
			
		||||
      dir: path.resolve(plugin.path, path.dirname(components.widgets)),
 | 
			
		||||
      file: path.join(plugin.path, components.widgets),
 | 
			
		||||
      name: plugin.manifest.name || plugin.id,
 | 
			
		||||
      pluginId: plugin.id,
 | 
			
		||||
 | 
			
		||||
@ -944,7 +944,8 @@
 | 
			
		||||
        "Mixed types": "Mixed types",
 | 
			
		||||
        "Revert field settings for {{colId}} to common": "Revert field settings for {{colId}} to common",
 | 
			
		||||
        "Save field settings for {{colId}} as common": "Save field settings for {{colId}} as common",
 | 
			
		||||
        "Use separate field settings for {{colId}}": "Use separate field settings for {{colId}}"
 | 
			
		||||
        "Use separate field settings for {{colId}}": "Use separate field settings for {{colId}}",
 | 
			
		||||
        "Changing column type": "Changing column type"
 | 
			
		||||
    },
 | 
			
		||||
    "FieldEditor": {
 | 
			
		||||
        "It should be impossible to save a plain data value into a formula column": "It should be impossible to save a plain data value into a formula column",
 | 
			
		||||
@ -1051,7 +1052,10 @@
 | 
			
		||||
        "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Can't find the right columns? Click 'Change Widget' to select the table with events data.",
 | 
			
		||||
        "To configure your calendar, select columns for start": {
 | 
			
		||||
            "end dates and event titles. Note each column's type.": "To configure your calendar, select columns for start/end dates and event titles. Note each column's type."
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
        "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.",
 | 
			
		||||
        "Lookups return data from related tables.": "Lookups return data from related tables.",
 | 
			
		||||
        "Use reference columns to relate data in different tables.": "Use reference columns to relate data in different tables."
 | 
			
		||||
    },
 | 
			
		||||
    "DescriptionConfig": {
 | 
			
		||||
        "DESCRIPTION": "DESCRIPTION"
 | 
			
		||||
 | 
			
		||||
@ -960,7 +960,8 @@
 | 
			
		||||
        "Apply Formula to Data": "Aplicar Fórmula a Datos",
 | 
			
		||||
        "CELL FORMAT": "FORMATO DE CELDA",
 | 
			
		||||
        "Changing multiple column types": "Cambiar varios tipos de columna",
 | 
			
		||||
        "Mixed format": "Formato mixto"
 | 
			
		||||
        "Mixed format": "Formato mixto",
 | 
			
		||||
        "Changing column type": "Cambiar el tipo de columna"
 | 
			
		||||
    },
 | 
			
		||||
    "CurrencyPicker": {
 | 
			
		||||
        "Invalid currency": "Moneda inválida"
 | 
			
		||||
@ -1105,7 +1106,10 @@
 | 
			
		||||
            "end dates and event titles. Note each column's type.": "Para configurar tu calendario, selecciona las columnas para las fechas de inicio y fin y los títulos de los eventos. Ten en cuenta el tipo de cada columna."
 | 
			
		||||
        },
 | 
			
		||||
        "Calendar": "Calendario",
 | 
			
		||||
        "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "¿No encuentras las columnas adecuadas? Haz clic en \"Cambiar widget\" para seleccionar la tabla con los datos de los eventos."
 | 
			
		||||
        "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "¿No encuentras las columnas adecuadas? Haz clic en \"Cambiar widget\" para seleccionar la tabla con los datos de los eventos.",
 | 
			
		||||
        "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "Un UUID es una cadena generada aleatoriamente que resulta útil para identificadores únicos y claves de los enlaces.",
 | 
			
		||||
        "Lookups return data from related tables.": "Las búsquedas devuelven datos de tablas relacionadas.",
 | 
			
		||||
        "Use reference columns to relate data in different tables.": "Utilizar las columnas de referencia para relacionar los datos de distintas tablas."
 | 
			
		||||
    },
 | 
			
		||||
    "DescriptionConfig": {
 | 
			
		||||
        "DESCRIPTION": "DESCRIPCIÓN"
 | 
			
		||||
 | 
			
		||||
@ -239,7 +239,10 @@
 | 
			
		||||
        "There was an error: {{message}}": "Si è verificato un errore: {{message}}",
 | 
			
		||||
        "You are now signed out.": "Adesso sei scollegato.",
 | 
			
		||||
        "You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Sei collegato come {{email}}. Puoi accedere con un account diverso, o chiedere l'accesso a un amministratore.",
 | 
			
		||||
        "You do not have access to this organization's documents.": "Non hai accesso ai documenti di questa organizzazione."
 | 
			
		||||
        "You do not have access to this organization's documents.": "Non hai accesso ai documenti di questa organizzazione.",
 | 
			
		||||
        "Account deleted{{suffix}}": "Account eliminato {{suffix}}",
 | 
			
		||||
        "Your account has been deleted.": "Il tuo account è stato cancellato.",
 | 
			
		||||
        "Sign up": "Iscriviti"
 | 
			
		||||
    },
 | 
			
		||||
    "duplicatePage": {
 | 
			
		||||
        "Note that this does not copy data, but creates another view of the same data.": "Notare che questo non copia i dati ma crea un'altra vista dagli stessi dati.",
 | 
			
		||||
@ -306,7 +309,8 @@
 | 
			
		||||
        "DATA FROM TABLE": "DATI DALLA TABELLA",
 | 
			
		||||
        "Revert field settings for {{colId}} to common": "Ripristina le impostazioni dei campi per {{colId}} a quelli comuni",
 | 
			
		||||
        "Use separate field settings for {{colId}}": "Usa impostazioni dei campi separate per {{colId}}",
 | 
			
		||||
        "Save field settings for {{colId}} as common": "Salva le impostazioni dei campi per {{colId}} come quelli comuni"
 | 
			
		||||
        "Save field settings for {{colId}} as common": "Salva le impostazioni dei campi per {{colId}} come quelli comuni",
 | 
			
		||||
        "Changing column type": "Cambio tipo colonna"
 | 
			
		||||
    },
 | 
			
		||||
    "FormulaEditor": {
 | 
			
		||||
        "Error in the cell": "Errore nella cella",
 | 
			
		||||
@ -387,7 +391,8 @@
 | 
			
		||||
        "Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "La colonna {{colId}} è stata successivamente rimossa nell'azione #{{action.actionNum}}",
 | 
			
		||||
        "Table {{tableId}} was subsequently removed in action #{{actionNum}}": "La tabella {{tableId}} è stata successivamente rimossa nell'azione #{{actionNum}}",
 | 
			
		||||
        "Action Log failed to load": "Impossibile caricare il log delle azioni",
 | 
			
		||||
        "This row was subsequently removed in action {{action.actionNum}}": "Questa riga è stata successivamente rimossa nell'azione {{action.actionNum}}"
 | 
			
		||||
        "This row was subsequently removed in action {{action.actionNum}}": "Questa riga è stata successivamente rimossa nell'azione {{action.actionNum}}",
 | 
			
		||||
        "All tables": "Tutte le tabelle"
 | 
			
		||||
    },
 | 
			
		||||
    "App": {
 | 
			
		||||
        "Memory Error": "Errore di memoria",
 | 
			
		||||
@ -558,7 +563,9 @@
 | 
			
		||||
        "Add": "Aggiungi",
 | 
			
		||||
        "Enter Custom URL": "Inserisci URL personalizzata",
 | 
			
		||||
        "Learn more about custom widgets": "Scopri di più sui widget personalizzati",
 | 
			
		||||
        "Select Custom Widget": "Seleziona un Widget personalizzato"
 | 
			
		||||
        "Select Custom Widget": "Seleziona un Widget personalizzato",
 | 
			
		||||
        "No {{columnType}} columns in table.": "Nessuna colonna {{columnType}} nella tabella.",
 | 
			
		||||
        "Clear selection": "Deseleziona tutto"
 | 
			
		||||
    },
 | 
			
		||||
    "DataTables": {
 | 
			
		||||
        "Click to copy": "Clicca per copiare",
 | 
			
		||||
@ -756,7 +763,27 @@
 | 
			
		||||
        "Sorted (#{{count}})_other": "Ordinati (#{{count}})",
 | 
			
		||||
        "Unfreeze {{count}} columns_other": "Sblocca {{count}} colonne",
 | 
			
		||||
        "Insert column to the left": "Inserisci colonna a sinistra",
 | 
			
		||||
        "Insert column to the right": "Inserisci colonna a destra"
 | 
			
		||||
        "Insert column to the right": "Inserisci colonna a destra",
 | 
			
		||||
        "Detect Duplicates in...": "Rilevati duplicati in...",
 | 
			
		||||
        "UUID": "UUID",
 | 
			
		||||
        "Shortcuts": "Scorciatoie",
 | 
			
		||||
        "Show hidden columns": "Mostra colonne nascoste",
 | 
			
		||||
        "Created At": "Creato il",
 | 
			
		||||
        "Authorship": "Autore",
 | 
			
		||||
        "Last Updated By": "Ultimo aggiornamento da",
 | 
			
		||||
        "Hidden Columns": "Colonne nascoste",
 | 
			
		||||
        "Lookups": "Campi relativi",
 | 
			
		||||
        "No reference columns.": "Non ci sono colonne referenziate.",
 | 
			
		||||
        "Apply on record changes": "Applica quando il record è modificato",
 | 
			
		||||
        "Duplicate in {{- label}}": "Duplicato in {{- label}}",
 | 
			
		||||
        "Created By": "Creato da",
 | 
			
		||||
        "Last Updated At": "Ultimo aggiornamento il",
 | 
			
		||||
        "Apply to new records": "Applica ai nuovi record",
 | 
			
		||||
        "Search columns": "Cerca colonne",
 | 
			
		||||
        "Timestamp": "Data e ora",
 | 
			
		||||
        "no reference column": "nessuna colonna referenziata",
 | 
			
		||||
        "Adding UUID column": "Aggiungere colonna UUID",
 | 
			
		||||
        "Adding duplicates column": "Aggiungere colonna duplicati"
 | 
			
		||||
    },
 | 
			
		||||
    "GristDoc": {
 | 
			
		||||
        "Import from file": "Importa da file",
 | 
			
		||||
@ -928,7 +955,8 @@
 | 
			
		||||
        "Choice List": "Scelta da lista",
 | 
			
		||||
        "Attachment": "Allegato",
 | 
			
		||||
        "Numeric": "Numerico",
 | 
			
		||||
        "Choice": "Scelta"
 | 
			
		||||
        "Choice": "Scelta",
 | 
			
		||||
        "Search columns": "Cerca colonne"
 | 
			
		||||
    },
 | 
			
		||||
    "modals": {
 | 
			
		||||
        "Cancel": "Annulla",
 | 
			
		||||
@ -1019,7 +1047,15 @@
 | 
			
		||||
        "Anchor Links": "Link interno",
 | 
			
		||||
        "Custom Widgets": "Widget personalizzati",
 | 
			
		||||
        "You can choose one of our pre-made widgets or embed your own by providing its full URL.": "Puoi scegliere uno dei nostri widget pronti all'uso, o inserirne uno fatto da te, immettendo la sua URL completa.",
 | 
			
		||||
        "To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Per creare un link che porta l'utente a una cella specifica, fai clic su una riga e premi {{shortcut}}."
 | 
			
		||||
        "To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Per creare un link che porta l'utente a una cella specifica, fai clic su una riga e premi {{shortcut}}.",
 | 
			
		||||
        "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "Un UUID è una stringa generata automaticamente, uitle come identificatore univoco e chiave per i link.",
 | 
			
		||||
        "To configure your calendar, select columns for start": {
 | 
			
		||||
            "end dates and event titles. Note each column's type.": "Per configurare il calendario, seleziona le colonne per le date di inizio/fine, e i titoli degli eventi. Nota il tipo di ciascuna colonna."
 | 
			
		||||
        },
 | 
			
		||||
        "Calendar": "Calendario",
 | 
			
		||||
        "Lookups return data from related tables.": "Un lookup restituisce dati dalle tabelle collegate.",
 | 
			
		||||
        "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Non trovi la colonna giusta? Fai clic su \"Cambia widget\" per selezionare la tabella con i dati degli eventi.",
 | 
			
		||||
        "Use reference columns to relate data in different tables.": "Usa colonne di riferimenti per collegare dati da altre tabelle."
 | 
			
		||||
    },
 | 
			
		||||
    "DescriptionConfig": {
 | 
			
		||||
        "DESCRIPTION": "DESCRIZIONE"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								static/locales/nl.client.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								static/locales/nl.client.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "ACUserManager": {
 | 
			
		||||
        "Invite new member": "Nieuw lid uitnodigen",
 | 
			
		||||
        "We'll email an invite to {{email}}": "We sturen een uitnodiging naar {{email}}",
 | 
			
		||||
        "Enter email address": "E-mailadres ingeven"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -216,7 +216,8 @@
 | 
			
		||||
        "Mixed types": "Смешанные типы",
 | 
			
		||||
        "Revert field settings for {{colId}} to common": "Вернуть настройки полей для {{colId}} к общим",
 | 
			
		||||
        "Save field settings for {{colId}} as common": "Сохранить настройки полей для {{colId}} как общие",
 | 
			
		||||
        "Use separate field settings for {{colId}}": "Использовать отдельные настройки полей для {{colId}}"
 | 
			
		||||
        "Use separate field settings for {{colId}}": "Использовать отдельные настройки полей для {{colId}}",
 | 
			
		||||
        "Changing column type": "Изменение типа столбца"
 | 
			
		||||
    },
 | 
			
		||||
    "FieldConfig": {
 | 
			
		||||
        "TRIGGER FORMULA": "ТРИГГЕРНАЯ ФОРМУЛА",
 | 
			
		||||
@ -1050,7 +1051,8 @@
 | 
			
		||||
            "end dates and event titles. Note each column's type.": "Чтобы настроить календарь, выберите столбцы для дат начала/окончания и названий событий. Обратите внимание на тип каждого столбца."
 | 
			
		||||
        },
 | 
			
		||||
        "Calendar": "Календарь",
 | 
			
		||||
        "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Не можете найти нужные столбцы? Нажмите «Изменить виджет», чтобы выбрать таблицу с данными о событиях."
 | 
			
		||||
        "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Не можете найти нужные столбцы? Нажмите «Изменить виджет», чтобы выбрать таблицу с данными о событиях.",
 | 
			
		||||
        "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "UUID - это случайно сгенерированная строка, которая полезна для уникальных идентификаторов и ключевых ссылок."
 | 
			
		||||
    },
 | 
			
		||||
    "DescriptionConfig": {
 | 
			
		||||
        "DESCRIPTION": "ОПИСАНИЕ"
 | 
			
		||||
 | 
			
		||||
@ -674,7 +674,10 @@
 | 
			
		||||
        "Try out changes in a copy, then decide whether to replace the original with your edits.": "Preizkusite spremembe v kopiji, nato pa se odločite, ali boste izvirnik zamenjali s svojimi popravki.",
 | 
			
		||||
        "The Raw Data page lists all data tables in your document, including summary tables and tables not included in page layouts.": "Na strani z neobdelanimi podatki so navedene vse podatkovne tabele v vašem dokumentu, vključno s tabelami s povzetki in tabelami, ki niso vključene v postavitve strani.",
 | 
			
		||||
        "Reference columns are the key to {{relational}} data in Grist.": "Referenčni stolpci so ključ do {{relational}} podatkov v Gristu.",
 | 
			
		||||
        "Cells in a reference column always identify an {{entire}} record in that table, but you may select which column from that record to show.": "Celice v referenčnem stolpcu vedno identificirajo {{entire}} zapis v tej tabeli, vendar lahko izberete, kateri stolpec iz tega zapisa želite prikazati."
 | 
			
		||||
        "Cells in a reference column always identify an {{entire}} record in that table, but you may select which column from that record to show.": "Celice v referenčnem stolpcu vedno identificirajo {{entire}} zapis v tej tabeli, vendar lahko izberete, kateri stolpec iz tega zapisa želite prikazati.",
 | 
			
		||||
        "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "UUID je naključno ustvarjen niz, ki je uporaben za edinstvene identifikatorje in ključe povezav.",
 | 
			
		||||
        "Lookups return data from related tables.": "Iskanje vrne podatke iz povezanih tabel.",
 | 
			
		||||
        "Use reference columns to relate data in different tables.": "Uporabite referenčne stolpce za povezavo podatkov v različnih tabelah."
 | 
			
		||||
    },
 | 
			
		||||
    "UserManager": {
 | 
			
		||||
        "Anyone with link ": "Vsakdo s povezavo ",
 | 
			
		||||
@ -1136,7 +1139,8 @@
 | 
			
		||||
        "Save field settings for {{colId}} as common": "Shrani nastavitve polja za {{colId}} kot običajne",
 | 
			
		||||
        "Changing multiple column types": "Spreminjanje več tipov stolpca",
 | 
			
		||||
        "Use separate field settings for {{colId}}": "Uporabite ločene nastavitve polja za {{colId}}",
 | 
			
		||||
        "Apply Formula to Data": "Izvedi formulo nad podatki"
 | 
			
		||||
        "Apply Formula to Data": "Izvedi formulo nad podatki",
 | 
			
		||||
        "Changing column type": "Spreminjanje vrste stolpca"
 | 
			
		||||
    },
 | 
			
		||||
    "FormulaEditor": {
 | 
			
		||||
        "Enter formula or {{button}}.": "Vnesite formulo ali {{button}}.",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								static/locales/th.client.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/locales/th.client.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
{}
 | 
			
		||||
							
								
								
									
										1
									
								
								static/locales/zh-Hant.client.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/locales/zh-Hant.client.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
{}
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import {decodeUrl, IGristUrlState, parseFirstUrlPart} from 'app/common/gristUrls';
 | 
			
		||||
import {decodeUrl, getHostType, IGristUrlState, parseFirstUrlPart} from 'app/common/gristUrls';
 | 
			
		||||
import {assert} from 'chai';
 | 
			
		||||
import * as testUtils from 'test/server/testUtils';
 | 
			
		||||
 | 
			
		||||
describe('gristUrls', function() {
 | 
			
		||||
 | 
			
		||||
@ -76,4 +77,56 @@ describe('gristUrls', function() {
 | 
			
		||||
      assert.deepEqual(parseFirstUrlPart('o', ''), {path: ''});
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('getHostType', function() {
 | 
			
		||||
    const defaultOptions = {
 | 
			
		||||
      baseDomain: 'getgrist.com',
 | 
			
		||||
      pluginUrl: 'https://plugin.getgrist.com',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let oldEnv: testUtils.EnvironmentSnapshot;
 | 
			
		||||
 | 
			
		||||
    beforeEach(function () {
 | 
			
		||||
      oldEnv = new testUtils.EnvironmentSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(function () {
 | 
			
		||||
      oldEnv.restore();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should interpret localhost as "native"', function() {
 | 
			
		||||
      assert.equal(getHostType('localhost', defaultOptions), 'native');
 | 
			
		||||
      assert.equal(getHostType('localhost:8080', defaultOptions), 'native');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should interpret base domain as "native"', function() {
 | 
			
		||||
      assert.equal(getHostType('getgrist.com', defaultOptions), 'native');
 | 
			
		||||
      assert.equal(getHostType('www.getgrist.com', defaultOptions), 'native');
 | 
			
		||||
      assert.equal(getHostType('foo.getgrist.com', defaultOptions), 'native');
 | 
			
		||||
      assert.equal(getHostType('foo.getgrist.com:8080', defaultOptions), 'native');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should interpret plugin domain as "plugin"', function() {
 | 
			
		||||
      assert.equal(getHostType('plugin.getgrist.com', defaultOptions), 'plugin');
 | 
			
		||||
      assert.equal(getHostType('PLUGIN.getgrist.com', { pluginUrl: 'https://pLuGin.getgrist.com' }), 'plugin');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should interpret other domains as "custom"', function() {
 | 
			
		||||
      assert.equal(getHostType('foo.com', defaultOptions), 'custom');
 | 
			
		||||
      assert.equal(getHostType('foo.bar.com', defaultOptions), 'custom');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should interpret doc internal url as "native"', function() {
 | 
			
		||||
      process.env.APP_DOC_INTERNAL_URL = 'https://doc-worker-123.internal/path';
 | 
			
		||||
      assert.equal(getHostType('doc-worker-123.internal', defaultOptions), 'native');
 | 
			
		||||
      assert.equal(getHostType('doc-worker-123.internal:8080', defaultOptions), 'custom');
 | 
			
		||||
      assert.equal(getHostType('doc-worker-124.internal', defaultOptions), 'custom');
 | 
			
		||||
 | 
			
		||||
      process.env.APP_DOC_INTERNAL_URL = 'https://doc-worker-123.internal:8080/path';
 | 
			
		||||
      assert.equal(getHostType('doc-worker-123.internal:8080', defaultOptions), 'native');
 | 
			
		||||
      assert.equal(getHostType('doc-worker-123.internal', defaultOptions), 'custom');
 | 
			
		||||
      assert.equal(getHostType('doc-worker-124.internal:8080', defaultOptions), 'custom');
 | 
			
		||||
      assert.equal(getHostType('doc-worker-123.internal:8079', defaultOptions), 'custom');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -734,146 +734,167 @@ describe('CustomWidgets', function () {
 | 
			
		||||
      oldEnv = new EnvironmentSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    after(async function() {
 | 
			
		||||
    afterEach(async function() {
 | 
			
		||||
      oldEnv.restore();
 | 
			
		||||
      await server.restart();
 | 
			
		||||
      await gu.reloadDoc();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('can add widgets via plugins', async function () {
 | 
			
		||||
      // Double-check that using one external widget, we see
 | 
			
		||||
      // just that widget listed.
 | 
			
		||||
      widgets = [widget1];
 | 
			
		||||
      await useManifest(manifestEndpoint);
 | 
			
		||||
      await enableWidgetsAndShowPanel();
 | 
			
		||||
      await toggle();
 | 
			
		||||
      assert.deepEqual(await options(), [
 | 
			
		||||
        CUSTOM_URL, widget1.name,
 | 
			
		||||
      ]);
 | 
			
		||||
    for (const variant of ['flat', 'nested'] as const) {
 | 
			
		||||
      it(`can add widgets via plugins (${variant} layout)`, async function () {
 | 
			
		||||
        // Double-check that using one external widget, we see
 | 
			
		||||
        // just that widget listed.
 | 
			
		||||
        widgets = [widget1];
 | 
			
		||||
        await useManifest(manifestEndpoint);
 | 
			
		||||
        await enableWidgetsAndShowPanel();
 | 
			
		||||
        await toggle();
 | 
			
		||||
        assert.deepEqual(await options(), [
 | 
			
		||||
          CUSTOM_URL, widget1.name,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
      // Get a temporary directory that will be cleaned up,
 | 
			
		||||
      // and populated it as follows:
 | 
			
		||||
      //   plugins/
 | 
			
		||||
      //     my-widgets/
 | 
			
		||||
      //       manifest.yml   # a plugin manifest, listing widgets.json
 | 
			
		||||
      //       widgets.json   # a widget set manifest, grist-widget style
 | 
			
		||||
      //       p1.html        # one of the widgets in widgets.json
 | 
			
		||||
      //       p2.html        # another of the widgets in widgets.json
 | 
			
		||||
      //       grist-plugin-api.js   # a dummy api file, to check it is overridden
 | 
			
		||||
      const dir = await createTmpDir();
 | 
			
		||||
      const pluginDir = path.join(dir, 'plugins', 'my-widgets');
 | 
			
		||||
      await fse.mkdirp(pluginDir);
 | 
			
		||||
        // Get a temporary directory that will be cleaned up,
 | 
			
		||||
        // and populated it as follows ('flat' variant)
 | 
			
		||||
        //   plugins/
 | 
			
		||||
        //     my-widgets/
 | 
			
		||||
        //       manifest.yml   # a plugin manifest, listing widgets.json
 | 
			
		||||
        //       widgets.json   # a widget set manifest, grist-widget style
 | 
			
		||||
        //       p1.html        # one of the widgets in widgets.json
 | 
			
		||||
        //       p2.html        # another of the widgets in widgets.json
 | 
			
		||||
        //       grist-plugin-api.js   # a dummy api file, to check it is overridden
 | 
			
		||||
        // In 'nested' variant, widgets.json and the files it refers to are in
 | 
			
		||||
        // a subdirectory.
 | 
			
		||||
        const dir = await createTmpDir();
 | 
			
		||||
        const pluginDir = path.join(dir, 'plugins', 'my-widgets');
 | 
			
		||||
        const widgetDir = variant === 'nested' ? path.join(pluginDir, 'nested') : pluginDir;
 | 
			
		||||
        await fse.mkdirp(pluginDir);
 | 
			
		||||
        await fse.mkdirp(widgetDir);
 | 
			
		||||
 | 
			
		||||
      // A plugin, with some widgets in it.
 | 
			
		||||
      await fse.writeFile(path.join(pluginDir, 'manifest.yml'), `
 | 
			
		||||
        name: My Widgets
 | 
			
		||||
        components:
 | 
			
		||||
          widgets: widgets.json
 | 
			
		||||
      `);
 | 
			
		||||
        // A plugin, with some widgets in it.
 | 
			
		||||
        await fse.writeFile(
 | 
			
		||||
          path.join(pluginDir, 'manifest.yml'),
 | 
			
		||||
          `name: My Widgets\n` +
 | 
			
		||||
          `components:\n` +
 | 
			
		||||
          `  widgets: ${variant === 'nested' ? 'nested/' : ''}widgets.json\n`
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
      // A list of a pair of custom widgets, with the widget
 | 
			
		||||
      // source in the same directory.
 | 
			
		||||
      await fse.writeFile(
 | 
			
		||||
        path.join(pluginDir, 'widgets.json'),
 | 
			
		||||
        JSON.stringify([
 | 
			
		||||
          {
 | 
			
		||||
            widgetId: 'p1',
 | 
			
		||||
            name: 'P1',
 | 
			
		||||
            url: './p1.html',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            widgetId: 'p2',
 | 
			
		||||
            name: 'P2',
 | 
			
		||||
            url: './p2.html',
 | 
			
		||||
          },
 | 
			
		||||
        ]),
 | 
			
		||||
      );
 | 
			
		||||
        // A list of a pair of custom widgets, with the widget
 | 
			
		||||
        // source in the same directory.
 | 
			
		||||
        await fse.writeFile(
 | 
			
		||||
          path.join(widgetDir, 'widgets.json'),
 | 
			
		||||
          JSON.stringify([
 | 
			
		||||
            {
 | 
			
		||||
              widgetId: 'p1',
 | 
			
		||||
              name: 'P1',
 | 
			
		||||
              url: './p1.html',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              widgetId: 'p2',
 | 
			
		||||
              name: 'P2',
 | 
			
		||||
              url: './p2.html',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              widgetId: 'p3',
 | 
			
		||||
              name: 'P3',
 | 
			
		||||
              url: './p3.html',
 | 
			
		||||
              published: false,
 | 
			
		||||
            },
 | 
			
		||||
          ]),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
      // The first widget - just contains the text P1.
 | 
			
		||||
      await fse.writeFile(
 | 
			
		||||
        path.join(pluginDir, 'p1.html'),
 | 
			
		||||
        '<html><body>P1</body></html>',
 | 
			
		||||
      );
 | 
			
		||||
        // The first widget - just contains the text P1.
 | 
			
		||||
        await fse.writeFile(
 | 
			
		||||
          path.join(widgetDir, 'p1.html'),
 | 
			
		||||
          '<html><body>P1</body></html>',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
      // The second widget. This contains the text P2
 | 
			
		||||
      // if grist is defined after loading grist-plugin-api.js
 | 
			
		||||
      // (but the js bundled with the widget just throws an
 | 
			
		||||
      // alert).
 | 
			
		||||
      await fse.writeFile(
 | 
			
		||||
        path.join(pluginDir, 'p2.html'),
 | 
			
		||||
        `
 | 
			
		||||
        <html>
 | 
			
		||||
        <script src="./grist-plugin-api.js"></script>
 | 
			
		||||
        <body>
 | 
			
		||||
        // The second widget. This contains the text P2
 | 
			
		||||
        // if grist is defined after loading grist-plugin-api.js
 | 
			
		||||
        // (but the js bundled with the widget just throws an
 | 
			
		||||
        // alert).
 | 
			
		||||
        await fse.writeFile(
 | 
			
		||||
          path.join(widgetDir, 'p2.html'),
 | 
			
		||||
          `
 | 
			
		||||
          <html>
 | 
			
		||||
          <head><script src="./grist-plugin-api.js"></script></head>
 | 
			
		||||
          <body>
 | 
			
		||||
          <div id="readout"></div>
 | 
			
		||||
          <script>
 | 
			
		||||
             if (typeof grist !== 'undefined') {
 | 
			
		||||
               document.getElementById('readout').innerText = 'P2';
 | 
			
		||||
             }
 | 
			
		||||
            if (typeof grist !== 'undefined') {
 | 
			
		||||
              document.getElementById('readout').innerText = 'P2';
 | 
			
		||||
            }
 | 
			
		||||
          </script>
 | 
			
		||||
        </body>
 | 
			
		||||
        </html>
 | 
			
		||||
        `
 | 
			
		||||
      );
 | 
			
		||||
      // A dummy grist-plugin-api.js - hopefully the actual
 | 
			
		||||
      // js for the current version of Grist will be served in
 | 
			
		||||
      // its place.
 | 
			
		||||
      await fse.writeFile(
 | 
			
		||||
        path.join(pluginDir, 'grist-plugin-api.js'),
 | 
			
		||||
        'alert("Error: built in api version used");',
 | 
			
		||||
      );
 | 
			
		||||
          </body>
 | 
			
		||||
          </html>
 | 
			
		||||
          `
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
      // Restart server and reload doc now plugins are in place.
 | 
			
		||||
      process.env.GRIST_USER_ROOT = dir;
 | 
			
		||||
      await server.restart();
 | 
			
		||||
      await gu.reloadDoc();
 | 
			
		||||
        // The third widget - just contains the text P3.
 | 
			
		||||
        await fse.writeFile(
 | 
			
		||||
          path.join(widgetDir, 'p3.html'),
 | 
			
		||||
          '<html><body>P3</body></html>',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
      // Continue using one external widget.
 | 
			
		||||
      await useManifest(manifestEndpoint);
 | 
			
		||||
      await enableWidgetsAndShowPanel();
 | 
			
		||||
        // A dummy grist-plugin-api.js - hopefully the actual
 | 
			
		||||
        // js for the current version of Grist will be served in
 | 
			
		||||
        // its place.
 | 
			
		||||
        await fse.writeFile(
 | 
			
		||||
          path.join(widgetDir, 'grist-plugin-api.js'),
 | 
			
		||||
          'alert("Error: built in api version used");',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
      // Check we see one external widget and two bundled ones.
 | 
			
		||||
      await toggle();
 | 
			
		||||
      assert.deepEqual(await options(), [
 | 
			
		||||
        CUSTOM_URL, widget1.name, 'P1 (My Widgets)', 'P2 (My Widgets)',
 | 
			
		||||
      ]);
 | 
			
		||||
        // Restart server and reload doc now plugins are in place.
 | 
			
		||||
        process.env.GRIST_USER_ROOT = dir;
 | 
			
		||||
        await server.restart();
 | 
			
		||||
        await gu.reloadDoc();
 | 
			
		||||
 | 
			
		||||
      // Prepare to check content of widgets.
 | 
			
		||||
      async function getWidgetText(): Promise<string> {
 | 
			
		||||
        return gu.doInIframe(await getCustomWidgetFrame(), () => {
 | 
			
		||||
          return driver.executeScript(
 | 
			
		||||
            () => document.body.innerText
 | 
			
		||||
          );
 | 
			
		||||
        // Continue using one external widget.
 | 
			
		||||
        await useManifest(manifestEndpoint);
 | 
			
		||||
        await enableWidgetsAndShowPanel();
 | 
			
		||||
 | 
			
		||||
        // Check we see one external widget and two bundled ones.
 | 
			
		||||
        await toggle();
 | 
			
		||||
        assert.deepEqual(await options(), [
 | 
			
		||||
          CUSTOM_URL, widget1.name, 'P1 (My Widgets)', 'P2 (My Widgets)',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Prepare to check content of widgets.
 | 
			
		||||
        async function getWidgetText(): Promise<string> {
 | 
			
		||||
          return gu.doInIframe(await getCustomWidgetFrame(), () => {
 | 
			
		||||
            return driver.executeScript(
 | 
			
		||||
              () => document.body.innerText
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check built-in P1 works as expected.
 | 
			
		||||
        await select(/P1/);
 | 
			
		||||
        assert.equal(await current(), 'P1 (My Widgets)');
 | 
			
		||||
        await gu.waitToPass(async () => {
 | 
			
		||||
          assert.equal(await getWidgetText(), 'P1');
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Check built-in P1 works as expected.
 | 
			
		||||
      await select(/P1/);
 | 
			
		||||
      assert.equal(await current(), 'P1 (My Widgets)');
 | 
			
		||||
      await gu.waitToPass(async () => {
 | 
			
		||||
        assert.equal(await getWidgetText(), 'P1');
 | 
			
		||||
      });
 | 
			
		||||
        // Check external W1 works as expected.
 | 
			
		||||
        await toggle();
 | 
			
		||||
        await select(/W1/);
 | 
			
		||||
        assert.equal(await current(), 'W1');
 | 
			
		||||
        await gu.waitToPass(async () => {
 | 
			
		||||
          assert.equal(await getWidgetText(), 'W1');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
      // Check external W1 works as expected.
 | 
			
		||||
      await toggle();
 | 
			
		||||
      await select(/W1/);
 | 
			
		||||
      assert.equal(await current(), 'W1');
 | 
			
		||||
      await gu.waitToPass(async () => {
 | 
			
		||||
        assert.equal(await getWidgetText(), 'W1');
 | 
			
		||||
      });
 | 
			
		||||
        // Check build-in P2 works as expected.
 | 
			
		||||
        await toggle();
 | 
			
		||||
        await select(/P2/);
 | 
			
		||||
        assert.equal(await current(), 'P2 (My Widgets)');
 | 
			
		||||
        await gu.waitToPass(async () => {
 | 
			
		||||
          assert.equal(await getWidgetText(), 'P2');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
      // Check build-in P2 works as expected.
 | 
			
		||||
      await toggle();
 | 
			
		||||
      await select(/P2/);
 | 
			
		||||
      assert.equal(await current(), 'P2 (My Widgets)');
 | 
			
		||||
      await gu.waitToPass(async () => {
 | 
			
		||||
        assert.equal(await getWidgetText(), 'P2');
 | 
			
		||||
        // Make sure widget setting is sticky.
 | 
			
		||||
        await gu.reloadDoc();
 | 
			
		||||
        await gu.waitToPass(async () => {
 | 
			
		||||
          assert.equal(await getWidgetText(), 'P2');
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Make sure widget setting is sticky.
 | 
			
		||||
      await gu.reloadDoc();
 | 
			
		||||
      await gu.waitToPass(async () => {
 | 
			
		||||
        assert.equal(await getWidgetText(), 'P2');
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -17,9 +17,10 @@ describe('DocApi2', function() {
 | 
			
		||||
  let owner: UserAPI;
 | 
			
		||||
  let wsId: number;
 | 
			
		||||
  testUtils.setTmpLogLevel('error');
 | 
			
		||||
  const oldEnv = new testUtils.EnvironmentSnapshot();
 | 
			
		||||
  let oldEnv: testUtils.EnvironmentSnapshot;
 | 
			
		||||
 | 
			
		||||
  before(async function() {
 | 
			
		||||
    oldEnv = new testUtils.EnvironmentSnapshot();
 | 
			
		||||
    const tmpDir = await createTmpDir();
 | 
			
		||||
    process.env.GRIST_DATA_DIR = tmpDir;
 | 
			
		||||
    process.env.STRIPE_ENDPOINT_SECRET = 'TEST_WITHOUT_ENDPOINT_SECRET';
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user