mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +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