diff --git a/README.md b/README.md index 53b11a3a..2beeea35 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/client/ui/CustomSectionConfig.ts b/app/client/ui/CustomSectionConfig.ts index 2a5b3260..dc8dd7af 100644 --- a/app/client/ui/CustomSectionConfig.ts +++ b/app/client/ui/CustomSectionConfig.ts @@ -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) { diff --git a/app/common/CustomWidget.ts b/app/common/CustomWidget.ts index 94e925ac..dc091e16 100644 --- a/app/common/CustomWidget.ts +++ b/app/common/CustomWidget.ts @@ -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. */ diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index ef94a416..3ab254c9 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -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 { diff --git a/app/server/lib/WidgetRepository.ts b/app/server/lib/WidgetRepository.ts index 3637e978..f79a049f 100644 --- a/app/server/lib/WidgetRepository.ts +++ b/app/server/lib/WidgetRepository.ts @@ -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, diff --git a/static/locales/en.client.json b/static/locales/en.client.json index d8b71f16..5cb89c48 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -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" diff --git a/static/locales/es.client.json b/static/locales/es.client.json index 25d1373b..ae8f1200 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -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" diff --git a/static/locales/it.client.json b/static/locales/it.client.json index 1fdc0e51..d2c30830 100644 --- a/static/locales/it.client.json +++ b/static/locales/it.client.json @@ -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" diff --git a/static/locales/nl.client.json b/static/locales/nl.client.json new file mode 100644 index 00000000..81d8e1af --- /dev/null +++ b/static/locales/nl.client.json @@ -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" + } +} diff --git a/static/locales/ru.client.json b/static/locales/ru.client.json index 670a9eed..26845809 100644 --- a/static/locales/ru.client.json +++ b/static/locales/ru.client.json @@ -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": "ОПИСАНИЕ" diff --git a/static/locales/sl.client.json b/static/locales/sl.client.json index 3d8f0db2..0e6e6b1c 100644 --- a/static/locales/sl.client.json +++ b/static/locales/sl.client.json @@ -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}}.", diff --git a/static/locales/th.client.json b/static/locales/th.client.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/static/locales/th.client.json @@ -0,0 +1 @@ +{} diff --git a/static/locales/zh-Hant.client.json b/static/locales/zh-Hant.client.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/static/locales/zh-Hant.client.json @@ -0,0 +1 @@ +{} diff --git a/test/common/gristUrls.ts b/test/common/gristUrls.ts index a12877a0..6ce7c5c5 100644 --- a/test/common/gristUrls.ts +++ b/test/common/gristUrls.ts @@ -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'); + }); + }); }); diff --git a/test/nbrowser/CustomWidgets.ts b/test/nbrowser/CustomWidgets.ts index fffe764e..4730631d 100644 --- a/test/nbrowser/CustomWidgets.ts +++ b/test/nbrowser/CustomWidgets.ts @@ -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'), - 'P1', - ); + // 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'), + 'P1', + ); - // 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'), - ` - - - + // 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'), + ` + + +
- - - ` - ); - // 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");', - ); + + + ` + ); - // 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'), + 'P3', + ); - // 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 { - 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 { + 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'); + }); }); - }); + } }); }); diff --git a/test/server/lib/DocApi2.ts b/test/server/lib/DocApi2.ts index fea62cb8..29345282 100644 --- a/test/server/lib/DocApi2.ts +++ b/test/server/lib/DocApi2.ts @@ -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';