(core) updates from grist-core

pull/723/head
Paul Fitzpatrick 6 months ago
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"

@ -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,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,
]);
// 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);
// A plugin, with some widgets in it.
await fse.writeFile(path.join(pluginDir, 'manifest.yml'), `
name: My Widgets
components:
widgets: widgets.json
`);
// 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',
},
]),
);
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 ('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\n` +
`components:\n` +
` widgets: ${variant === 'nested' ? 'nested/' : ''}widgets.json\n`
);
// The first widget - just contains the text P1.
await fse.writeFile(
path.join(pluginDir, 'p1.html'),
'<html><body>P1</body></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(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)',
]);
// Prepare to check content of widgets.
async function getWidgetText(): Promise<string> {
return gu.doInIframe(await getCustomWidgetFrame(), () => {
return driver.executeScript(
() => document.body.innerText
);
});
}
// Restart server and reload doc now plugins are in place.
process.env.GRIST_USER_ROOT = dir;
await server.restart();
await gu.reloadDoc();
// 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');
});
// Continue using one external widget.
await useManifest(manifestEndpoint);
await enableWidgetsAndShowPanel();
// 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 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 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 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');
});
// Make sure widget setting is sticky.
await gu.reloadDoc();
await gu.waitToPass(async () => {
assert.equal(await getWidgetText(), 'P2');
// 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');
});
// 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…
Cancel
Save