From 6059bdcf66ee9b8a6f764e4876e269977ccf165d Mon Sep 17 00:00:00 2001 From: Paul Fitzpatrick Date: Mon, 30 Oct 2023 09:52:42 -0400 Subject: [PATCH 01/14] rename variable to avoid shadowing another (#712) This fixes a small lint issue in some widget bundling code. --- app/server/lib/WidgetRepository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/server/lib/WidgetRepository.ts b/app/server/lib/WidgetRepository.ts index 3637e978..51ee6b83 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(); } } From 98dc10dec7598c3b10bd84741dbb93bfd27208cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 09:53:21 -0400 Subject: [PATCH 02/14] automated update to translation keys (#711) Co-authored-by: Paul's Grist Bot --- static/locales/en.client.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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" From 07bb90b5a6fa1d04e52adacaec29a27ab7bb73d9 Mon Sep 17 00:00:00 2001 From: Paul Fitzpatrick Date: Mon, 30 Oct 2023 21:13:21 -0400 Subject: [PATCH 04/14] allow bundled widgets to be hidden from dropdown, and nested (#714) This makes a few refinements to bundling widgets: * A widget with `published: false` is not shown in the custom widget dropdown in the UI. This is so widgets can be bundled with the app for "native" use (like the calendar widget) without immediately resulting in an extra listing in the UI. (There are improvements we'd like to make to the UI to better communicate widget provenance and quality eventually, which would be a helpful alternative to just a binary flag.) * A relative path to the custom widget manifest is respected. This will make the bundling process marginally neater. --- app/client/ui/CustomSectionConfig.ts | 10 +- app/common/CustomWidget.ts | 5 + app/server/lib/WidgetRepository.ts | 2 +- test/nbrowser/CustomWidgets.ts | 269 +++++++++++++++------------ 4 files changed, 157 insertions(+), 129 deletions(-) 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/server/lib/WidgetRepository.ts b/app/server/lib/WidgetRepository.ts index 51ee6b83..f79a049f 100644 --- a/app/server/lib/WidgetRepository.ts +++ b/app/server/lib/WidgetRepository.ts @@ -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/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'); + }); }); - }); + } }); }); From 9557f8c4e4c61b434ee508d4fedcec063209d900 Mon Sep 17 00:00:00 2001 From: son_ gcs Date: Tue, 31 Oct 2023 09:53:03 +0100 Subject: [PATCH 05/14] Added translation using Weblate (Thai) --- static/locales/th.client.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 static/locales/th.client.json 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 @@ +{} From d29e23efe83d71f0e3daf921898cac2efcdce8cc Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 31 Oct 2023 14:03:11 +0000 Subject: [PATCH 06/14] Translated using Weblate (Spanish) Currently translated at 99.8% (997 of 998 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/es/ --- static/locales/es.client.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/static/locales/es.client.json b/static/locales/es.client.json index 25d1373b..f119908b 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,9 @@ "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." }, "DescriptionConfig": { "DESCRIPTION": "DESCRIPCIÓN" From 7e9ecb50233914e30757daa38d4a43c9d7ce7732 Mon Sep 17 00:00:00 2001 From: Ariejan de Vroom Date: Thu, 2 Nov 2023 10:35:33 +0100 Subject: [PATCH 07/14] Added translation using Weblate (Dutch) --- static/locales/nl.client.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 static/locales/nl.client.json diff --git a/static/locales/nl.client.json b/static/locales/nl.client.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/static/locales/nl.client.json @@ -0,0 +1 @@ +{} From 1685b88973f19304d6ed47e7d76cbfa42583ecbe Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Thu, 2 Nov 2023 13:37:19 +0100 Subject: [PATCH 08/14] Added translation using Weblate (Chinese (Traditional)) --- static/locales/zh-Hant.client.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 static/locales/zh-Hant.client.json 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 @@ +{} From b59b5054bd7e6e3bc733c04fd747d6b2600a082c Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 31 Oct 2023 14:03:57 +0000 Subject: [PATCH 09/14] Translated using Weblate (Spanish) Currently translated at 100.0% (998 of 998 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/es/ --- static/locales/es.client.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/locales/es.client.json b/static/locales/es.client.json index f119908b..ae8f1200 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -1108,7 +1108,8 @@ "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.", "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." + "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" From 00cbbeec1779c2306b32ec8992aab1379659cde8 Mon Sep 17 00:00:00 2001 From: Riccardo Polignieri Date: Wed, 1 Nov 2023 13:51:15 +0000 Subject: [PATCH 10/14] Translated using Weblate (Italian) Currently translated at 100.0% (998 of 998 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/it/ --- static/locales/it.client.json | 50 ++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) 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" From 0c609adef068bb20badcdde7e247e08a450ac7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C4=8Dek=20Prijatelj?= Date: Tue, 31 Oct 2023 20:18:09 +0000 Subject: [PATCH 11/14] Translated using Weblate (Slovenian) Currently translated at 100.0% (998 of 998 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/sl/ --- static/locales/sl.client.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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}}.", From 80c7bbd62897a28eb10344d0e823261eedfb3679 Mon Sep 17 00:00:00 2001 From: Ariejan de Vroom Date: Thu, 2 Nov 2023 09:41:20 +0000 Subject: [PATCH 12/14] Translated using Weblate (Dutch) Currently translated at 0.3% (3 of 998 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/nl/ --- static/locales/nl.client.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/static/locales/nl.client.json b/static/locales/nl.client.json index 0967ef42..81d8e1af 100644 --- a/static/locales/nl.client.json +++ b/static/locales/nl.client.json @@ -1 +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" + } +} From c8eb1ad4a9468a494609c3358f6e5099de8d66e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BB=D0=B0=D0=B4=D0=B8=D0=BC=D0=B8=D1=80=20=D0=92?= Date: Sun, 5 Nov 2023 18:33:11 +0000 Subject: [PATCH 13/14] Translated using Weblate (Russian) Currently translated at 99.3% (992 of 998 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/ru/ --- static/locales/ru.client.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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": "ОПИСАНИЕ" From 10822d3b860bd8a2f40c00fef80e396803a0f553 Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 6 Nov 2023 09:24:59 +0100 Subject: [PATCH 14/14] getHostType: consider APP_DOC_INTERNAL_URL as native (#715) The getHostType() now returns "native" when the host corresponds to the value of APP_DOC_INTERNAL_URL. T While trying to scale, with a different internal and public URL for doc workers, and having configured the org to be specified in the path (GRIST_ORG_IN_PATH=true), the APP_DOC_INTERNAL_URL parameter was not treated as internal which made the connection between home server and doc workers impossible. --------- https://github.com/gristlabs/grist-core/pull/715 Co-authored-by: Florent FAYOLLE --- README.md | 1 + app/common/gristUrls.ts | 12 +++++++-- test/common/gristUrls.ts | 55 +++++++++++++++++++++++++++++++++++++- test/server/lib/DocApi2.ts | 3 ++- 4 files changed, 67 insertions(+), 4 deletions(-) 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/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/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/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';