From f345d7824566570c28906f0ce481fe0dd44bac1c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 09:39:34 -0400 Subject: [PATCH 01/13] automated update to translation keys (#691) Co-authored-by: Paul's Grist Bot --- static/locales/en.client.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 27db5206..19f6e659 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -199,7 +199,9 @@ "Widget needs to {{read}} the current table.": "Widget needs to {{read}} the current table.", "Widget needs {{fullAccess}} to this document.": "Widget needs {{fullAccess}} to this document.", "{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} non-{{columnType}} column is not shown", - "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} non-{{columnType}} columns are not shown" + "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} non-{{columnType}} columns are not shown", + "Clear selection": "Clear selection", + "No {{columnType}} columns in table.": "No {{columnType}} columns in table." }, "DataTables": { "Click to copy": "Click to copy", From 1e1077b0f3a83ccbbad12b39a98b51f6ee63ed7f 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: Mon, 9 Oct 2023 16:41:06 +0000 Subject: [PATCH 02/13] Translated using Weblate (Russian) Currently translated at 99.6% (968 of 971 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/ru/ --- static/locales/ru.client.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/locales/ru.client.json b/static/locales/ru.client.json index b78213c7..c6d6c903 100644 --- a/static/locales/ru.client.json +++ b/static/locales/ru.client.json @@ -769,7 +769,10 @@ "Contact support": "Обратитесь в службу поддержки", "Sign in to access this organization's documents.": "Войдите, чтобы получить доступ к документам этой организации.", "The requested page could not be found.{{separator}}Please check the URL and try again.": "Запрашиваемая страница не может быть найдена.{{separator}}Пожалуйста, проверьте URL-адрес и повторите попытку.", - "You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Вы вошли как {{email}}. Вы можете войти с другой учетной записью или запросить доступ у администратора." + "You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Вы вошли как {{email}}. Вы можете войти с другой учетной записью или запросить доступ у администратора.", + "Account deleted{{suffix}}": "Аккаунт удален{{suffix}}", + "Your account has been deleted.": "Ваш аккаунт был удален.", + "Sign up": "Регистрация" }, "CellStyle": { "CELL STYLE": "СТИЛЬ ЯЧЕЙКИ", From 257a78fffe88f244893e04fa714f69045c063e54 Mon Sep 17 00:00:00 2001 From: Jakub Serafin Date: Sun, 8 Oct 2023 17:02:23 +0000 Subject: [PATCH 03/13] Translated using Weblate (Polish) Currently translated at 78.9% (767 of 971 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/pl/ --- static/locales/pl.client.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/locales/pl.client.json b/static/locales/pl.client.json index d0acb3ea..2b9ddd47 100644 --- a/static/locales/pl.client.json +++ b/static/locales/pl.client.json @@ -70,7 +70,9 @@ "Sign Out": "Wyloguj się", "Sign in": "Zaloguj się", "Switch Accounts": "Przełącz konta", - "Toggle Mobile Mode": "Przełącz na tryb mobilny" + "Toggle Mobile Mode": "Przełącz na tryb mobilny", + "Activation": "Aktywacja", + "Sign In": "Zaloguj się" }, "ViewAsDropdown": { "View As": "Wyświetl jako", @@ -791,7 +793,7 @@ }, "OnBoardingPopups": { "Next": "Następny", - "Finish": "Skończyć" + "Finish": "Zakończ" }, "SortFilterConfig": { "Revert": "Przywrócić", From 4d19cef1116e84a890bfca19edeab8cde1e0dcba Mon Sep 17 00:00:00 2001 From: Vincent Viers Date: Wed, 11 Oct 2023 15:35:22 +0000 Subject: [PATCH 04/13] Translated using Weblate (French) Currently translated at 100.0% (973 of 973 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/fr/ --- static/locales/fr.client.json | 67 ++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/static/locales/fr.client.json b/static/locales/fr.client.json index 6e5891e4..a1139695 100644 --- a/static/locales/fr.client.json +++ b/static/locales/fr.client.json @@ -74,8 +74,8 @@ "Accounts": "Comptes", "Add Account": "Ajouter un compte", "Sign Out": "Se déconnecter", - "Upgrade Plan": "Version Premium", - "Support Grist": "Centre d'aide Grist", + "Upgrade Plan": "Changer d'offre", + "Support Grist": "Support utilisateur Grist", "Billing Account": "Facturation", "Activation": "Activer", "Sign In": "Se connecter", @@ -86,7 +86,8 @@ "Action Log failed to load": "Impossible de charger le journal des actions", "Table {{tableId}} was subsequently removed in action #{{actionNum}}": "La table {{tableId}} a été ensuite supprimée dans l'action #{{actionNum}}", "This row was subsequently removed in action {{action.actionNum}}": "Cette ligne a été ensuite supprimée dans l'action {{action.actionNum}}", - "Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "La colonne {{colId}} a ensuite été supprimée dans l'action #{{action.actionNum}}" + "Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "La colonne {{colId}} a ensuite été supprimée dans l'action #{{action.actionNum}}", + "All tables": "Toutes les tables" }, "AddNewButton": { "Add New": "Nouveau" @@ -194,7 +195,9 @@ "Widget needs to {{read}} the current table.": "Le widget a besoin de {{read}} la table actuelle.", "Widget does not require any permissions.": "La vue ne nécessite aucune autorisation.", "{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} colonnes non-{{columnType}} masquées", - "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} colonnes de type non-{{columnType}} masquées" + "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} colonnes de type non-{{columnType}} masquées", + "No {{columnType}} columns in table.": "Pas de colonne de type {{columnType}} dans la table.", + "Clear selection": "Tout désélectionner" }, "DataTables": { "Raw Data Tables": "Données sources", @@ -791,7 +794,10 @@ "Contact support": "Contacter le support", "Something went wrong": "Une erreur s’est produite", "There was an error: {{message}}": "Une erreur s’est produite : {{message}}", - "There was an unknown error.": "Une erreur inconnue s’est produite." + "There was an unknown error.": "Une erreur inconnue s’est produite.", + "Account deleted{{suffix}}": "Compte supprimé {{suffix}}", + "Your account has been deleted.": "Votre compte a été supprimé.", + "Sign up": "S'inscrire" }, "menus": { "Select fields": "Sélectionner les champs", @@ -1013,7 +1019,7 @@ "relational": "relationnelles", "Access Rules": "Règles d'accès", "Learn more.": "En savoir plus.", - "They allow for one record to point (or refer) to another.": "Ils permettent à un enregistrement de pointer (ou de faire référence) vers un autre.", + "They allow for one record to point (or refer) to another.": "Ils permettent à une ligne de pointer (ou de faire référence) vers une autre.", "Add New": "Nouveau", "Access rules give you the power to create nuanced rules to determine who can see or edit which parts of your document.": "Les règles d'accès vous donnent le pouvoir de créer des règles nuancées pour déterminer qui peut voir ou modifier quelles parties de votre document.", "Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.": "Utilisez l'icône 𝚺 pour créer des tables récapitulatives (ou tables croisées dynamiques), pour les totaux ou les sous-totaux.", @@ -1021,7 +1027,12 @@ "Anchor Links": "Ancres", "Custom Widgets": "Vues personnalisées", "To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Pour créer un lien d'ancrage qui amène l'utilisateur à une cellule spécifique, cliquez sur une ligne et appuyez sur {{shortcut}}.", - "You can choose one of our pre-made widgets or embed your own by providing its full URL.": "Vous pouvez choisir l'une de nos vues prédéfinis ou intégrer la vôtre en indiquant son URL complète." + "You can choose one of our pre-made widgets or embed your own by providing its full URL.": "Vous pouvez choisir l'une de nos vues prédéfinies ou intégrer la vôtre en indiquant son URL complète.", + "To configure your calendar, select columns for start": { + "end dates and event titles. Note each column's type.": "Pour configurer votre calendrier, sélectionnez les colonnes pour les dates de début/fin et le nom de l'évènement. Notez le type de chaque colonne." + }, + "Calendar": "Calendrier", + "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Impossible de trouver les bonnes colonnes ? Cliquez sur \"Changer de vue\" pour sélectionner la table contenant les évênements." }, "ColumnTitle": { "Add description": "Ajouter une description", @@ -1074,8 +1085,8 @@ "AI Assistant": "Assistant IA", "Apply": "Appliquer", "Cancel": "Annuler", - "Hi, I'm the Grist Formula AI Assistant.": "Bonjour, je suis l'assistant IA de Grist pour les formules", - "I can only help with formulas. I cannot build tables, columns, and views, or write access rules.": "Je peux aider seulement pour les formules, je ne peut pas créer de tables, de colonnes, de vue ou gérer les droits d'accès.", + "Hi, I'm the Grist Formula AI Assistant.": "Bonjour, je suis l'assistant IA de Grist pour les formules.", + "I can only help with formulas. I cannot build tables, columns, and views, or write access rules.": "Je ne peux aider que pour les formules, je ne peut pas créer de tables, de colonnes, de vue ou gérer les droits d'accès.", "Learn more": "En apprendre plus", "Clear Conversation": "Effacer la conversation", "Code View": "Vue du code", @@ -1100,7 +1111,9 @@ "Close": "Fermer", "Contribute": "Contribuer", "Support Grist": "Support Grist", - "Opt in to Telemetry": "S'inscrire à Telemetry" + "Opt in to Telemetry": "S'inscrire à Telemetry", + "Opted In": "Accepté", + "Support Grist page": "Soutenir Grist" }, "GridView": { "Click to insert": "Cliquer pour insérer" @@ -1116,8 +1129,12 @@ "Opt out of Telemetry": "S'inscrire à Telemetry", "Support Grist": "Support Grist", "Telemetry": "Telemetry", - "This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Cette instance est autorisée à utiliser la télémétrie. Seul l'administrateur de l'espace est autorisé à modifier ce paramètre.", - "This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Cette instance est autorisée à utiliser la télémétrie. Seul l'administrateur de l'espace est autorisé à modifier ce paramètre." + "This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Cette instance accepte l'envoi de données de télémétrie. Seul l'administrateur de l'espace est autorisé à modifier ce paramètre.", + "This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Cette instance est autorisée à utiliser la télémétrie. Seul l'administrateur de l'espace est autorisé à modifier ce paramètre.", + "You have opted out of telemetry.": "Vous avez choisi de ne pas envoyer de données de télémétrie.", + "We only collect usage statistics, as detailed in our {{link}}, never document contents.": "Nous ne collectons que des statistiques d'usage, comme détaillé dans notre {{link}}, jamais le contenu des documents.", + "You can opt out of telemetry at any time from this page.": "Vous pouvez vous désinscrire de la télémétrie à tout moment depuis cette page.", + "You have opted in to telemetry. Thank you!": "Merci de vous être inscrit à la télémétrie !" }, "buildViewSectionDom": { "No data": "Aucune donnée", @@ -1129,7 +1146,7 @@ "Anyone with link ": "Toute personne possédant le lien ", "Cancel": "Annuler", "Close": "Fermer", - "Add {{member}} to your team": "Ajouter des {{member}} à votre équipe", + "Add {{member}} to your team": "Ajouter {{member}} à votre équipe", "Confirm": "Confirmer", "member": "membre", "{{collaborator}} limit exceeded": "la limite de {{collaborator}} a été atteinte", @@ -1139,39 +1156,39 @@ "Collaborator": "Collaborateur", "Copy Link": "Copier le lien", "Create a team to share with more people": "Créer une équipe pour partager avec plus de personnes", - "Grist support": "Support Grist", + "Grist support": "Support utilisateur Grist", "Guest": "Invité", "Invite multiple": "Invitation multiple", "Invite people to {{resourceType}}": "Inviter des personnes à {{resourceType}}", "Link copied to clipboard": "Lien copié dans le presse-papiers", "Manage members of team site": "Gérer les membres de l'espace d'équipe", - "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "L'absence d'accès par défaut permet d'accorder l'accès à des documents ou à des espaces de travail spécifiques, plutôt qu'à l'ensemble de l'espace d'équipe.", - "Off": "Off", - "On": "On", - "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.": "Une fois que vous avez supprimé votre propre accès, vous ne pourrez pas le récupérer sans l'aide d'une autre personne disposant d'un accès suffisant au {{name}}.", + "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "L'absence d'accès par défaut permet d' accorder l'accès à des documents ou à des espaces de travail spécifiques, plutôt qu'à l'ensemble de l'espace d'équipe.", + "Off": "Désactivé", + "On": "Activé", + "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.": "Une fois que vous avez supprimé votre propre accès, vous ne pourrez pas le récupérer sans l'aide d'une autre personne disposant d'un accès suffisant à {{name}}.", "Open Access Rules": "Ouvrir les règles d'accès", "Outside collaborator": "Collaborateur externe", "Public Access": "Accès public", "Public access": "Accès public", - "Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "L'accès public hérite de {{parent}}. Pour le supprimer, changer l'option 'Accès hérité' à 'Aucun'", + "Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "L'accès public est hérité de {{parent}}. Pour le supprimer, changer l'option 'Accès hérité' à 'Aucun'", "Public access: ": "Accès public : ", "Remove my access": "Supprimer mon accès", "Save & ": "Sauvegarder & ", - "Team member": "Membres", - "User may not modify their own access.": "L'utilisateur ne peut pas modifier son propre accès.", + "Team member": "Membres de l'espace d'équipe", + "User may not modify their own access.": "L'utilisateur ne peut pas modifier ses propres accès.", "Your role for this team site": "Votre rôle pour cet espace d'équipe", "Your role for this {{resourceType}}": "Votre rôle pour cet {{resourceType}}", - "free collaborator": "Collaborateur gratuit", + "free collaborator": "collaborateur gratuit", "guest": "invité", "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "L'absence d'accès par défaut permet d'accorder l'accès à des documents ou à des espaces de travail spécifiques, plutôt qu'à l'ensemble de l'espace d'équipe.", - "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.": "Une fois que vous avez supprimé votre propre accès, vous ne pourrez pas le récupérer sans l'aide d'une autre personne disposant d'un accès suffisant au {{resourceType}}.", - "User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.": "L'utilisateur a un accès visuel à {{resource}} résultant d'un accès manuel aux ressources internes. S'il est supprimé ici, cet utilisateur perdra l'accès aux ressources internes.", + "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.": "Une fois que vous avez supprimé vos propres accès, vous ne pourrez pas les récupérer sans l'aide d'une autre personne disposant d'un accès suffisant au {{resourceType}}.", + "User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.": "L'utilisateur a un accès en lecture seule à {{resource}} résultant d'un accès des ressources à l'intérieur. S'il est supprimé ici, cet utilisateur perdra l'accès aux ressources à l'intérieur.", "You are about to remove your own access to this {{resourceType}}": "Vous êtes sur le point de supprimer votre propre accès à {{resourceType}}", "User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.": "L'utilisateur hérite ses permissions de {{parent})}. Pour supprimer cela, paramétrez 'Héritage d'accès à 'Aucun'." }, "SearchModel": { "Search all tables": "Rechercher toutes les tables", - "Search all pages": "Rechercher toutes les pages" + "Search all pages": "Rechercher dans toutes les pages" }, "searchDropdown": { "Search": "Chercher" From 0cf4c3be5e4f84a1954a06cd5939b3aff5e98157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Wed, 11 Oct 2023 11:16:31 +0000 Subject: [PATCH 05/13] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 87.0% (847 of 973 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/nb_NO/ --- static/locales/nb_NO.client.json | 286 ++++++++++++++++++++++++++++--- 1 file changed, 264 insertions(+), 22 deletions(-) diff --git a/static/locales/nb_NO.client.json b/static/locales/nb_NO.client.json index 28a8bd0d..1b7a2448 100644 --- a/static/locales/nb_NO.client.json +++ b/static/locales/nb_NO.client.json @@ -28,7 +28,14 @@ "Sign Out": "Logg ut", "Sign in": "Logg inn", "Switch Accounts": "Bytt konto", - "Toggle Mobile Mode": "Slå av/på mobilmodus" + "Toggle Mobile Mode": "Slå av/på mobilmodus", + "Activation": "Aktivering", + "Support Grist": "Støtt Grist", + "Upgrade Plan": "Oppgrader plan", + "Use This Template": "Bruk denne malen", + "Billing Account": "Faktureringskonto", + "Sign In": "Logg inn", + "Sign Up": "Registrering" }, "AddNewButton": { "Add New": "Legg til ny" @@ -53,7 +60,8 @@ "Home Page": "Hjemmeside", "Personal Site": "Personlig side", "Team Site": "Lagside", - "Legacy": "Foreldet" + "Legacy": "Foreldet", + "Grist Templates": "Grist-maler" }, "CellContextMenu": { "Clear cell": "Tøm celle", @@ -84,7 +92,11 @@ "Duplicate rows_one": "Dupliser rad", "Reset {{count}} columns_one": "Tilbakestill kolonne", "Reset {{count}} entire columns_other": "Tilbakestill {{count}} hele kolonner", - "Reset {{count}} columns_other": "Tilbakestill {{count}} kolonner" + "Reset {{count}} columns_other": "Tilbakestill {{count}} kolonner", + "Copy": "Kopier", + "Comment": "Kommentar", + "Cut": "Klipp ut", + "Paste": "Lim inn" }, "ColumnFilterMenu": { "All": "Alle", @@ -121,7 +133,9 @@ "Widget needs to {{read}} the current table.": "Miniprogrammet må {{read}} nåværende tabell.", "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} ikke-{{columnType}}-kolonner er ikke vist.", "Widget needs {{fullAccess}} to this document.": "Miniprogrammet trenger {{fullAccess}} til dette dokumentet.", - "{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} ikke-{{columnType}}-kolonne er ikke vist." + "{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} ikke-{{columnType}}-kolonne er ikke vist.", + "No {{columnType}} columns in table.": "Ingen {{columnType}}-kolonner i tabell.", + "Clear selection": "Tøm utvalg" }, "DocHistory": { "Activity": "Aktivitet", @@ -186,7 +200,8 @@ "Trash": "Papirkurv", "Workspace will be moved to Trash.": "Arbeidsområdet vil bli flyttet til papirkurven.", "Workspaces": "Arbeidsområder", - "Rename": "Gi nytt navn" + "Rename": "Gi nytt navn", + "Tutorial": "Veiledning" }, "MakeCopyMenu": { "It will be overwritten, losing any content not in this document.": "Den vil bli overskrevet, noe som forkaster alt innholdet som ikke er i dokumentet.", @@ -211,7 +226,10 @@ "Overwrite": "Overskriv", "Replacing the original requires editing rights on the original document.": "Erstatting av originalen krever redigeringsrettigheter til originaldokumentet.", "Be careful, the original has changes not in this document. Those changes will be overwritten.": "Vær forsiktig, originalen har endringer som ikke finnes i dette dokumentet. De endringene vil bli overskrevet.", - "However, it appears to be already identical.": "Dog later det til at det allerede er identisk." + "However, it appears to be already identical.": "Dog later det til at det allerede er identisk.", + "Remove all data but keep the structure to use as a template": "Fjern all data, men behold strukturen til bruk som mal", + "Remove document history (can significantly reduce file size)": "Fjern dokumenthistorikk (kan redusere filstørrelse drastisk)", + "Download full document and history": "Last ned hele dokumentet og historikken" }, "NotifyUI": { "Cannot find personal site, sorry!": "Finner ikke personlig side. Beklager.", @@ -222,7 +240,8 @@ "Report a problem": "Innrapporter et problem", "Go to your free personal site": "Gå til din kostnadsløse personlige side", "No notifications": "Ingen merknader", - "Notifications": "Merknader" + "Notifications": "Merknader", + "Manage billing": "Håndter fakturering" }, "RightPanel": { "COLUMN TYPE": "Kolonnetype", @@ -253,7 +272,8 @@ "Series_other": "Serier", "Sort & Filter": "Sorter og filtrer", "You do not have edit access to this document": "Du har ikke redigeringstilgang til dette dokumentet", - "Widget": "Miniprogram" + "Widget": "Miniprogram", + "Add referenced columns": "Legg til kolonner å vise til" }, "RowContextMenu": { "Delete": "Slett", @@ -283,7 +303,9 @@ "Current Version": "Nåværende versjon", "Download": "Last ned", "Duplicate Document": "Dupliser dokument", - "Send to Google Drive": "Send til Google Drive" + "Send to Google Drive": "Send til Google Drive", + "Download...": "Last ned …", + "Share": "Del" }, "ThemeConfig": { "Switch appearance automatically to match system": "Bytt utseende automatisk for å samsvare med systemet", @@ -356,13 +378,17 @@ "You do not have access to this organization's documents.": "Du har ikke tilgang til denne organisasjonens dokumenter.", "Error{{suffix}}": "Feil-{{suffix}}", "Page not found{{suffix}}": "Fant ikke siden-{{suffix}}", - "You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Du er innlogget som {{email}}. Du kan logge inn med en annen konto, eller spørre en administrator om tilgang." + "You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Du er innlogget som {{email}}. Du kan logge inn med en annen konto, eller spørre en administrator om tilgang.", + "Account deleted{{suffix}}": "Konto slettet{{suffix}}", + "Your account has been deleted.": "Kontoen din har blitt slettet.", + "Sign up": "Logg inn" }, "search": { "Search in document": "Søk i dokumentet", "Find Next ": "Finn neste ", "Find Previous ": "Finn forrige ", - "No results": "Resultatløst" + "No results": "Resultatløst", + "Search": "Søk" }, "DiscussionEditor": { "Cancel": "Avbryt", @@ -428,7 +454,9 @@ "Lookup Table": "Oppslagstabell", "Lookup Column": "Oppslagskolonne", "Permission to access the document in full when needed": "Tilgang til hele dokumentet når det trengs", - "Save": "Lagre" + "Save": "Lagre", + "Allow editors to edit structure (e.g. modify and delete tables, columns, layouts), and to write formulas, which give access to all data regardless of read restrictions.": "Tillat de som redigerer å endre struktur (f.eks. endre og slette tabeller, kolonner, oppsett), og å skrive formler, som gir tilgang til all data uavhengig av begrensninger.", + "This default should be changed if editors' access is to be limited. ": "Dette forvalget må endres hvis de som redigerer ikke skal ha tilgang, " }, "ChartView": { "Toggle chart aggregation": "Veksle diagramsvisning", @@ -474,7 +502,9 @@ "Locale:": "Lokalitet:", "Document ID copied to clipboard": "Dokument-ID kopiert til utklippstavlen", "Currency:": "Valuta:", - "Document Settings": "Dokumentinnstillinger" + "Document Settings": "Dokumentinnstillinger", + "Manage Webhooks": "Håndter vevkroker", + "Webhooks": "Vevkroker" }, "DocumentUsage": { "Usage": "Bruk", @@ -553,7 +583,16 @@ "Try out changes in a copy, then decide whether to replace the original with your edits.": "Prøv ut endringer i en kopi, og avgjør så hvorvidt du vil erstatte originalen med endringene dine.", "Unpin to hide the the button while keeping the filter.": "Løsne for å skjule knappen og beholde filteret.", "Useful for storing the timestamp or author of a new record, data cleaning, and more.": "Nyttig for lagring av tidsstempel eller forfatter av en ny oppføring, datarensing, med mer.", - "The total size of all data in this document, excluding attachments.": "Samlet størrelse av all data i dette dokumentet, fraregnet vedlegg." + "The total size of all data in this document, excluding attachments.": "Samlet størrelse av all data i dette dokumentet, fraregnet vedlegg.", + "You can choose one of our pre-made widgets or embed your own by providing its full URL.": "Du kan velge en av miniprogrammene som finnes, eller bygge inn ditt eget ved å angi dets fulle nettadresse.", + "To configure your calendar, select columns for start": { + "end dates and event titles. Note each column's type.": "Velg kolonner for start-/slutt-dato og begivenhetsnavn. Merk deg hvilken type hver kolonne er." + }, + "Calendar": "Kalender", + "To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Klikk på en rad og trykk {{shortcut}} for å lage en ankerlenke som tar brukeren til en gitt celle.", + "Anchor Links": "Ankerlenker", + "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Finner du ikke riktig kolonner? Klikk «Endre miniprogram» for å velge tabellen med begivenhetsdata.", + "Custom Widgets": "Egendefinerte miniprogrammer" }, "ViewAsDropdown": { "Example Users": "Eksempelbrukere", @@ -564,7 +603,8 @@ "Action Log failed to load": "Kunne ikke laste inn handlingslogg", "Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "Kolonnen {{colId}} ble påfølgende fjernet i handling #{{action.actionNum}}", "Table {{tableId}} was subsequently removed in action #{{actionNum}}": "Tabellen {{tableId}} ble påfølgende fjernet i handling #{{actionNum}}", - "This row was subsequently removed in action {{action.actionNum}}": "Denne raden ble påfølgende fjernet i handling {{action.actionNum}}" + "This row was subsequently removed in action {{action.actionNum}}": "Denne raden ble påfølgende fjernet i handling {{action.actionNum}}", + "All tables": "Alle tabeller" }, "ColorSelect": { "Apply": "Bruk", @@ -630,12 +670,15 @@ "Unfreeze {{count}} columns_other": "Tin {{count}} kolonner", "Reset {{count}} entire columns_other": "Tilbakestill {{count}} hele kolonner", "Unfreeze all columns": "Tin alle kolonner", - "Add Column": "Legg til kolonne" + "Add Column": "Legg til kolonne", + "Insert column to the right": "Sett inn kolonne til høyre", + "Insert column to the left": "Sett inn kolonne til venstre" }, "GristDoc": { "Import from file": "Importer fra fil", "Added new linked section to view {{viewName}}": "Ny lenket avsnitt lagt til i visning {{viewName}}", - "Saved linked section {{title}} in view {{name}}": "Lagret lenket {{title}}-avsnitt i {{name}}-visningen" + "Saved linked section {{title}} in view {{name}}": "Lagret lenket {{title}}-avsnitt i {{name}}-visningen", + "go to webhook settings": "gå til vevkroksinnstillinger" }, "HomeIntro": { "Browse Templates": "Utforsk maler", @@ -657,7 +700,12 @@ "Get started by inviting your team and creating your first Grist document.": "Begynn ved å invitere laget ditt og ved å opprette ditt første Grist-dokument.", "Interested in using Grist outside of your team? Visit your free ": "Interessert i bruk av Grist utenfor laget ditt? Besøk ditt kostnadsløse ", "This workspace is empty.": "Arbeidsområdet er tomt.", - "Any documents created in this site will appear here.": "Alle dokumenter opprettet i denne siden vil vises her." + "Any documents created in this site will appear here.": "Alle dokumenter opprettet i denne siden vil vises her.", + "Welcome to Grist, {{- name}}!": "Velkommen til Grist {{- name}}", + "Visit our {{link}} to learn more about Grist.": "Besøk {{link}} for å lære mer om Grist.", + "Welcome to {{- orgName}}": "Velkommen til {{- orgName}}", + "Sign in": "Logg inn", + "To use Grist, please either sign up or sign in.": "Logg inn eller registrer deg for å bruke Grist." }, "PageWidgetPicker": { "Building {{- label}} widget": "Bygger {{- label}}-miniprogram", @@ -687,14 +735,21 @@ "CELL STYLE": "Cellestil", "Cell Style": "Cellestil", "Default cell style": "Forvalgt cellestil", - "Open row styles": "Åpne radstiler" + "Open row styles": "Åpne radstiler", + "HEADER STYLE": "Topptekststil", + "Header Style": "Topptekststil", + "Default header style": "Forvalgt topptekststil" }, "FormulaEditor": { "Errors in all {{numErrors}} cells": "Feil i alle {{numErrors}} celler", "Column or field is required": "Kolonne eller felt kreves angitt", "editingFormula is required": "«editingFormula» kreves", "Errors in {{numErrors}} of {{numCells}} cells": "Feil i {{numErrors}} av {{numCells}} celler", - "Error in the cell": "Feil i cellen" + "Error in the cell": "Feil i cellen", + "Enter formula or {{button}}.": "Skriv inn formel eller {{button}}.", + "Enter formula.": "Skriv inn formel.", + "Expand Editor": "Utvid tekstbehandler", + "use AI Assistant": "bruk AI-assistent" }, "VisibleFieldsConfig": { "Cannot drop items into Hidden Fields": "Kan ikke plassere elementer i skjulte felter", @@ -808,7 +863,8 @@ "Save": "Lagre", "Provide a table name": "Angi et tabellnavn", "Override widget title": "Overstyr miniprogramsnavn", - "WIDGET TITLE": "Miniprogramsnavn" + "WIDGET TITLE": "Miniprogramsnavn", + "WIDGET DESCRIPTION": "Miniprogramsbeskrivelse" }, "menus": { "Select fields": "Velg felter", @@ -904,7 +960,22 @@ "Importer": { "Merge rows that match these fields:": "Flett rader som samsvarer med disse feltene:", "Select fields to match on": "Velg felter å jamføre", - "Update existing records": "Oppdater eksisterende oppføringer" + "Update existing records": "Oppdater eksisterende oppføringer", + "Column mapping": "Kolonnetilknytning", + "Grist column": "Grist-kolonne", + "{{count}} unmatched field_one": "{{count}} usamsvarende felt", + "{{count}} unmatched field in import_one": "{{count}} usamsvarende felt i import", + "Revert": "Angre", + "Skip Import": "Hopp over import", + "{{count}} unmatched field_other": "{{count}} usamsvarende felter", + "New Table": "Ny tabell", + "Skip": "Hopp over", + "Column Mapping": "Kolonnetilknytning", + "Destination table": "Måltabell", + "Skip Table on Import": "Hopp over tabell ved import", + "Import from file": "Importer fra fil", + "{{count}} unmatched field in import_other": "{{count}} usamsvarende felt i import", + "Source column": "Kildekolonne" }, "LeftPanelCommon": { "Help Center": "Hjelpesenter" @@ -973,5 +1044,176 @@ }, "EditorTooltip": { "Convert column to formula": "Konverter kolonne til formel" + }, + "FormulaAssistant": { + "Data": "Data", + "Press Enter to apply suggested formula.": "Trykk Enter for å bruke foreslått formel.", + "See our {{helpFunction}} and {{formulaCheat}}, or visit our {{community}} for more help.": "Sjekk {{helpFunction}} og {{formulaCheat}}, eller besøk {{community}} for mer hjelp.", + "Sign up for a free Grist account to start using the Formula AI Assistant.": "Registrer deg for en gratis Grist-konto for å bruke AI-formelassistenten.", + "Clear Conversation": "Tøm samtale", + "New Chat": "Ny sludring", + "Code View": "Kodevisning", + "Apply": "Bruk", + "Learn more": "Lær mer", + "Regenerate": "Regenerer", + "Community": "Gemenskap", + "I can only help with formulas. I cannot build tables, columns, and views, or write access rules.": "Jeg kan kun hjelpe deg med formler. Jeg kan ikke bygge tabeller, kolonner, visninger, eller skrive tilgangsregler.", + "Hi, I'm the Grist Formula AI Assistant.": "Hei. Jeg er Grists AI-formelassistent.", + "Preview": "Forhåndsvis", + "Ask the bot.": "Spør botten.", + "Function List": "Funksjonsliste", + "For higher limits, contact the site owner.": "Kontakt sidens eier for høyere grenser.", + "Tips": "Tips", + "Save": "Lagre", + "Sign Up for Free": "Registrer deg gratis", + "Formula Cheat Sheet": "Formel-jukseark", + "Grist's AI Assistance": "Grists AI-assistanse", + "Formula AI Assistant is only available for logged in users.": "AI-formelassistenten er kun tilgjengelig for innloggede brukere.", + "Grist's AI Formula Assistance. ": "Grists AI-formelassistanse ", + "upgrade to the Pro Team plan": "oppgrader til proff lagplan", + "You have used all available credits.": "Du har brukt alle tilgjengelige planpoeng.", + "upgrade your plan": "oppgrader din plan", + "Formula Help. ": "Formelhjelp. ", + "You have {{numCredits}} remaining credits.": "Du har {{numCredits}} planpoeng igjen.", + "Capabilities": "Ferdigeter", + "What do you need help with?": "Hva trenger du hjelp med?", + "Cancel": "Avbryt", + "Need help? Our AI assistant can help.": "Hjelp? Vår AI-assistent kan hjelpe deg.", + "AI Assistant": "AI-assistant", + "For higher limits, {{upgradeNudge}}.": "{{upgradeNudge}} for høyere grenser.", + "There are some things you should know when working with me:": "Det er noen ting du burde vite når du jobber med meg:" + }, + "FloatingPopup": { + "Maximize": "Maksimer", + "Minimize": "Minimer" + }, + "Clipboard": { + "Unavailable Command": "Utilgjengelig kommando", + "Got it": "Skjønner" + }, + "SupportGristPage": { + "You have opted out of telemetry.": "Du har reservert deg mot datainnsamling.", + "Support Grist": "Støtt Grist", + "Opt out of Telemetry": "Skru av datainnsamling", + "GitHub Sponsors page": "GitHub Sponsor-side", + "Sponsor Grist Labs on GitHub": "Spons Grist Labs på GitHub", + "Manage Sponsorship": "Håndter sponsing", + "Help Center": "Hjelpesenter", + "We only collect usage statistics, as detailed in our {{link}}, never document contents.": "Kun bruksstatistikk samles inn og aldri dokumentinnhold. Dette er beskrevet nøye på {{link}}.", + "You can opt out of telemetry at any time from this page.": "Du kan motsette deg datainnsamling når som helst fra denne siden.", + "Home": "Hjem", + "This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Denne instansen har reservert seg mot datainnsamling. Kun sidens administrator har tilgang til å endre dette.", + "Telemetry": "Telemetri", + "Opt in to Telemetry": "Tillat datainnsamling", + "You have opted in to telemetry. Thank you!": "Du har reservert deg mot datainnsamling. Takk.", + "This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Denne instansen har påslått datainnsamling. Kun sidens administrator har mulighet til å endre dette.", + "GitHub": "GitHub" + }, + "UserManager": { + "Anyone with link ": "Alle med lenken ", + "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.": "Når du har fjernet din egen tilgang vil du ikke kunne logge inn igjen uten hjelp fra noen andre som har tilstrekkelig tilgang til {{name}}.", + "{{limitAt}} of {{limitTop}} {{collaborator}}s": "{{limitAt}} av {{limitTop}} {{collaborator}}s", + "Your role for this team site": "Din rolle på denne lagsiden", + "Copy Link": "Kopier lenke", + "User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.": "Bruker har visningstilgang for {{resource}}, som følge av manuelt satt tilgang til ressursene som finnes der. Hvis fjernet her vil brukeren miste tilgang til ressursene i {{resource}}.", + "User may not modify their own access.": "Brukere kan ikke endre egen tilgang.", + "member": "medlem", + "Add {{member}} to your team": "Legg til {{member}} på laget ditt", + "Collaborator": "Medarbeider", + "Link copied to clipboard": "Lenke kopiert til utklippstavlen", + "team site": "lagside", + "Create a team to share with more people": "Opprett et lag for å dele med flere", + "guest": "gjest", + "Public access: ": "Offentlig tilgang: ", + "Team member": "Lagmedlem", + "Manage members of team site": "Håndter medlemmer av lagside", + "Off": "Av", + "free collaborator": "ledig medarbeider", + "Save & ": "Lagre og ", + "Outside collaborator": "Medarbeider annensteds fra", + "{{collaborator}} limit exceeded": "{{collaborator}}-grense overskredet", + "User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.": "Bruker nedarver tilganger fra {{parent})}. Fjern ved å sette «Nedarv tilgang» til «Ingen».", + "Your role for this {{resourceType}}": "Din rolle i denne {{resourceType}}", + "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.": "Når du har fjernet din egen tilgang vil du ikke kunne logge inn igjen uten hjelp fra noen andre som har tilstrekkelig tilgang til {{resourceType}}.", + "Close": "Lukk", + "Allow anyone with the link to open.": "Tillat alle med lenken å åpne.", + "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "Ingen forvalgt tilgang tillater tilgang å innvilges til individuelle dokumenter eller arbeidsoråder, snarere enn hele lagsiden.", + "Invite people to {{resourceType}}": "Inviter noen til {{resourceType}}", + "Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Offentlig tilgang nedarvet fra {{parent}}. Fjern den ved å sette «Nedarv tilgang» til «Ingen».", + "Remove my access": "Fjern min tilgang", + "Public access": "Offentlig tilgang", + "Public Access": "Offentlig tilgang", + "Cancel": "Avbryt", + "Grist support": "Grist-støtte", + "You are about to remove your own access to this {{resourceType}}": "Du er i ferd med å fjerne din egen tilgang til denne {{resourceType}}", + "User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Bruker nedarver tilganger fra {{parent}}. For å fjerne sett «Nedarv tilgang» til «Ingen».", + "Guest": "Gjest", + "Invite multiple": "Inviter flere", + "Confirm": "Bekreft", + "On": "På", + "Open Access Rules": "Åpne tilgangsregler", + "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "Ingen forvalgt tilgang tillater tilgang å innvilges til individuelle dokumenter eller arbeidsoråder, snarere enn hele lagsiden." + }, + "PagePanels": { + "Open Creator Panel": "Åpne skaperpanel", + "Close Creator Panel": "Lukk skaperpanel" + }, + "buildViewSectionDom": { + "Not all data is shown": "Ikke all dataen vises", + "No row selected in {{title}}": "Ingen rader valgt i {{title}}", + "No data": "Ingen data" + }, + "ColumnTitle": { + "Column ID copied to clipboard": "Kolonne-ID kopiert til utklippstavle", + "Add description": "Legg til beskrivelse", + "Column description": "Kolonnebeskrivelse", + "COLUMN ID: ": "Kolonne-ID: ", + "Provide a column label": "Angi en kolonneetikett", + "Close": "Lukk", + "Cancel": "Avbryt", + "Column label": "Kolonneetikett", + "Save": "Lagre" + }, + "SupportGristNudge": { + "Support Grist": "Støtt Grist", + "Close": "Lukk", + "Opt in to Telemetry": "Tillat datainnsamling", + "Help Center": "Hjelpesenter", + "Opted In": "Tillatt", + "Contribute": "Bidra", + "Support Grist page": "«Støtt Grist»-siden" + }, + "WelcomeSitePicker": { + "You have access to the following Grist sites.": "Du har tilgang til følgende Grist-sider.", + "Welcome back": "Velkommen tilbake", + "You can always switch sites using the account menu.": "Du kan alltid bytte sider fra kontomenyen." + }, + "FieldContextMenu": { + "Copy anchor link": "Kopier ankerlenke", + "Hide field": "Skjul felt", + "Copy": "Kopier", + "Paste": "Lim inn", + "Clear field": "Tøm felt", + "Cut": "Klipp ut" + }, + "DescriptionTextArea": { + "DESCRIPTION": "Beskrivelse" + }, + "WebhookPage": { + "Webhook Settings": "Vevkroksinnstillinger", + "Clear Queue": "Tøm kø" + }, + "FloatingEditor": { + "Collapse Editor": "Fold sammen tekstbehandler" + }, + "GridView": { + "Click to insert": "Klikk for å sette inn" + }, + "searchDropdown": { + "Search": "Søk" + }, + "SearchModel": { + "Search all tables": "Søk i alle tabeller", + "Search all pages": "Søk på alle sider" } } From 42d077a010b88ad5e4f184d2cd92ece374639d22 Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Wed, 11 Oct 2023 03:41:14 +0000 Subject: [PATCH 06/13] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (973 of 973 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/pt_BR/ --- static/locales/pt_BR.client.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/locales/pt_BR.client.json b/static/locales/pt_BR.client.json index 98341a2d..75876977 100644 --- a/static/locales/pt_BR.client.json +++ b/static/locales/pt_BR.client.json @@ -207,7 +207,9 @@ "{{wrongTypeCount}} non-{{columnType}} column is not shown": "{{wrongTypeCount}} a não-{{columnType}} coluna não é mostrada", "{{wrongTypeCount}} non-{{columnType}} columns are not shown": "{{wrongTypeCount}} as não-{{columnType}} colunas não são mostradas", "{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} a não-{{columnType}} coluna não é mostrada", - "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} as não-{{columnType}} colunas não são mostradas" + "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} as não-{{columnType}} colunas não são mostradas", + "No {{columnType}} columns in table.": "Não há colunas {{columnType}} na tabela.", + "Clear selection": "Limpar seleção" }, "DataTables": { "Click to copy": "Clique para copiar", From 0a38ce4b66769ed81e04887169262c9e4967a56a Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 10 Oct 2023 17:17:19 +0000 Subject: [PATCH 07/13] Translated using Weblate (Spanish) Currently translated at 100.0% (973 of 973 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/es/ --- static/locales/es.client.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/locales/es.client.json b/static/locales/es.client.json index a1ee9f38..36ae7a71 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -175,7 +175,9 @@ "{{wrongTypeCount}} non-{{columnType}} column is not shown": "{{wrongTypeCount}} columna no {{columnType}} no se muestra", "{{wrongTypeCount}} non-{{columnType}} columns are not shown": "{{wrongTypeCount}} columnas no {{columnType}} no se muestran", "{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} no se muestra la columna {{columnType}}", - "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} no se muestran las columnas {{columnType}}" + "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} no se muestran las columnas {{columnType}}", + "No {{columnType}} columns in table.": "No hay columnas {{columnType}} en la tabla.", + "Clear selection": "Borrar la selección" }, "DocHistory": { "Activity": "Actividad", From dc9bd16358613867863e7f9d26ebf9f9b00352ea Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Wed, 11 Oct 2023 03:40:51 +0000 Subject: [PATCH 08/13] Translated using Weblate (German) Currently translated at 100.0% (973 of 973 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/de/ --- static/locales/de.client.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/locales/de.client.json b/static/locales/de.client.json index 35969d58..ae2ecadf 100644 --- a/static/locales/de.client.json +++ b/static/locales/de.client.json @@ -207,7 +207,9 @@ "{{wrongTypeCount}} non-{{columnType}} column is not shown": "{{wrongTypeCount}} nicht{{columnType}} Spalte wird nicht angezeigt", "{{wrongTypeCount}} non-{{columnType}} columns are not shown": "{{wrongTypeCount}} nicht{{columnType}} Spalte wird nicht angezeigt", "{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "Spalte {{wrongTypeCount}} Nicht-{{columnType}} wird nicht angezeigt", - "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "Spalten {{wrongTypeCount}} Nicht-{{columnType}} werden nicht angezeigt" + "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "Spalten {{wrongTypeCount}} Nicht-{{columnType}} werden nicht angezeigt", + "No {{columnType}} columns in table.": "Keine {{columnType}} Spalten in der Tabelle.", + "Clear selection": "Auswahl löschen" }, "DataTables": { "Click to copy": "Zum Kopieren anklicken", From 09268d921f821db738e227cd9d0235ef5e91a1ce 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: Tue, 10 Oct 2023 20:27:52 +0000 Subject: [PATCH 09/13] Translated using Weblate (Russian) Currently translated at 99.6% (970 of 973 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/ru/ --- static/locales/ru.client.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/locales/ru.client.json b/static/locales/ru.client.json index c6d6c903..e3d49191 100644 --- a/static/locales/ru.client.json +++ b/static/locales/ru.client.json @@ -164,7 +164,9 @@ "Select Custom Widget": "Выбор пользовательского виджета", "Pick a column": "Выберать столбец", "Read selected table": "Просмотр выбранной таблицы", - "Widget does not require any permissions.": "Виджет не требует никаких разрешений." + "Widget does not require any permissions.": "Виджет не требует никаких разрешений.", + "No {{columnType}} columns in table.": "Нет {{columnType}} столбцов в таблице.", + "Clear selection": "Очистить выбор" }, "AccountWidget": { "Access Details": "Сведения о доступе", From ad037e700ce62af94ba1f450cb0faa64cfbea2f6 Mon Sep 17 00:00:00 2001 From: Vincent Viers Date: Wed, 11 Oct 2023 16:02:12 +0000 Subject: [PATCH 10/13] Translated using Weblate (French) Currently translated at 99.8% (972 of 973 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/fr/ --- static/locales/fr.client.json | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/static/locales/fr.client.json b/static/locales/fr.client.json index a1139695..dd0b0b29 100644 --- a/static/locales/fr.client.json +++ b/static/locales/fr.client.json @@ -75,7 +75,7 @@ "Add Account": "Ajouter un compte", "Sign Out": "Se déconnecter", "Upgrade Plan": "Changer d'offre", - "Support Grist": "Support utilisateur Grist", + "Support Grist": "Soutenir Grist", "Billing Account": "Facturation", "Activation": "Activer", "Sign In": "Se connecter", @@ -419,7 +419,7 @@ "Get started by exploring templates, or creating your first Grist document.": "Commencez par explorer des modèles ou créez votre premier document Grist.", "Welcome to Grist!": "Bienvenue sur Grist !", "Help Center": "Centre d'aide", - "Invite Team Members": "Inviter un nouveau membre", + "Invite Team Members": "Inviter un nouveau membre à l'espace d'équipe", "Browse Templates": "Parcourir les modèles", "Create Empty Document": "Créer un document vide", "Import Document": "Importer un Fichier", @@ -524,7 +524,7 @@ "Select Widget": "Choisir la vue", "Select Data": "Choisir les données source", "Group by": "Grouper par", - "Add to Page": "Ajouter à la page" + "Add to Page": "Ajouter à la Page" }, "Pages": { "The following tables will no longer be visible_one": "La donnée source ne sera plus visible", @@ -631,11 +631,11 @@ "Use choice position": "Utiliser l'ordre des choix", "Natural sort": "Trier", "Empty values last": "Valeurs vides en dernier", - "Search Columns": "Rechercher" + "Search Columns": "Rechercher dans les colonnes" }, "SortFilterConfig": { "Save": "Enregistrer", - "Revert": "Restaurer", + "Revert": "Retour", "Sort": "TRI", "Filter": "FILTRE", "Update Sort & Filter settings": "Mettre à jour le tri et le filtre" @@ -652,7 +652,7 @@ "Code View": "Vue du code", "How-to Tutorial": "Tutoriel pratique", "Tour of this Document": "Découvrir le document", - "Delete document tour?": "Delete document tour?", + "Delete document tour?": "Supprimer la visite guidée du document ?", "Delete": "Supprimer", "Return to viewing as yourself": "Revenir à une vue en propre", "Raw Data": "Données source", @@ -693,10 +693,10 @@ "Update formula (Shift+Enter)": "Mettre à jour la formule (Maj+Entrée)" }, "ViewConfigTab": { - "Unmark On-Demand": "Unmark On-Demand", + "Unmark On-Demand": "Ne plus marquer comme \"à la demande\"", "Make On-Demand": "Rendre dynamique", "Advanced settings": "Paramètres avancés", - "Big tables may be marked as \"on-demand\" to avoid loading them into the data engine.": "Big tables may be marked as \"on-demand\" to avoid loading them into the data engine.", + "Big tables may be marked as \"on-demand\" to avoid loading them into the data engine.": "Les grosses tables peuvent être marquées comme \"à la demande\" pour éviter de les charger dans le moteur de calcul.", "Form": "Formulaire", "Compact": "Compact", "Blocks": "Blocs", @@ -723,7 +723,7 @@ "ViewSectionMenu": { "Update Sort&Filter settings": "Mettre à jour le tri et le filtre", "Save": "Enregistrer", - "Revert": "Restaurer", + "Revert": "Retour", "(customized)": "(personnalisé)", "(modified)": "(modifié)", "(empty)": "(vide)", @@ -853,7 +853,7 @@ "Open row styles": "Ouvrir les styles de ligne", "Cell Style": "Style de cellule", "CELL STYLE": "STYLE de CELLULE", - "Default cell style": "Style par défaut", + "Default cell style": "Style par défaut de la cellule", "Mixed style": "Style composite", "Header Style": "Style de l'entête", "Default header style": "Style par défaut", @@ -922,7 +922,7 @@ "Update formula (Shift+Enter)": "Modifier la formule (MAJ+Entrée)" }, "ColumnEditor": { - "COLUMN DESCRIPTION": "DESCRIPTION", + "COLUMN DESCRIPTION": "DESCRIPTION DE LA COLONNE", "COLUMN LABEL": "LIBELLÉ" }, "ACLUsers": { @@ -944,7 +944,7 @@ "CHOICES": "CHOIX" }, "ColumnInfo": { - "COLUMN DESCRIPTION": "DESCRIPTION", + "COLUMN DESCRIPTION": "DESCRIPTION DE LA COLONNE", "COLUMN ID: ": "Identifiant de la colonne : ", "COLUMN LABEL": "LIBELLÉ", "Cancel": "Annuler", @@ -1032,7 +1032,7 @@ "end dates and event titles. Note each column's type.": "Pour configurer votre calendrier, sélectionnez les colonnes pour les dates de début/fin et le nom de l'évènement. Notez le type de chaque colonne." }, "Calendar": "Calendrier", - "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Impossible de trouver les bonnes colonnes ? Cliquez sur \"Changer de vue\" pour sélectionner la table contenant les évênements." + "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Impossible de trouver les bonnes colonnes ? Cliquez sur \"Changer de vue\" pour sélectionner la table contenant les évènements." }, "ColumnTitle": { "Add description": "Ajouter une description", @@ -1041,7 +1041,7 @@ "COLUMN ID: ": "Identifiant de la column : ", "Column description": "Description de la colonne", "Column label": "Libellé de la colonne", - "Provide a column label": "Renommer la colonne", + "Provide a column label": "Donner un nom à la colonne", "Save": "Sauvegarder", "Close": "Fermer" }, @@ -1111,7 +1111,7 @@ "Close": "Fermer", "Contribute": "Contribuer", "Support Grist": "Support Grist", - "Opt in to Telemetry": "S'inscrire à Telemetry", + "Opt in to Telemetry": "S'inscrire à l'envoi de données de télémétrie", "Opted In": "Accepté", "Support Grist page": "Soutenir Grist" }, @@ -1125,10 +1125,10 @@ "Sponsor Grist Labs on GitHub": "Sponsoriser Grist Labs sur GitHub", "GitHub Sponsors page": "Page de sponsors GitHub", "Manage Sponsorship": "Gérer le parrainage", - "Opt in to Telemetry": "S'inscrire à Telemetry", - "Opt out of Telemetry": "S'inscrire à Telemetry", + "Opt in to Telemetry": "S'inscrire à l'envoi de données de télémétrie", + "Opt out of Telemetry": "Se désinscrire de l'envoi de données de télémétrie", "Support Grist": "Support Grist", - "Telemetry": "Telemetry", + "Telemetry": "Télémétrie", "This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Cette instance accepte l'envoi de données de télémétrie. Seul l'administrateur de l'espace est autorisé à modifier ce paramètre.", "This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Cette instance est autorisée à utiliser la télémétrie. Seul l'administrateur de l'espace est autorisé à modifier ce paramètre.", "You have opted out of telemetry.": "Vous avez choisi de ne pas envoyer de données de télémétrie.", @@ -1170,7 +1170,7 @@ "Outside collaborator": "Collaborateur externe", "Public Access": "Accès public", "Public access": "Accès public", - "Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "L'accès public est hérité de {{parent}}. Pour le supprimer, changer l'option 'Accès hérité' à 'Aucun'", + "Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "L'accès public est hérité de {{parent}}. Pour le supprimer, changer l'option 'Accès hérité' à 'Aucun'.", "Public access: ": "Accès public : ", "Remove my access": "Supprimer mon accès", "Save & ": "Sauvegarder & ", From f66ecbd6df910b77925f8f780fb64a67661afce0 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Thu, 12 Oct 2023 19:32:22 +0200 Subject: [PATCH 11/13] feat: allow using the existing numeric table IDs in the API (#690) --- app/server/lib/ActiveDoc.ts | 29 ++- app/server/lib/DocApi.ts | 52 +++--- test/server/lib/DocApi.ts | 359 +++++++++++++++++++----------------- 3 files changed, 248 insertions(+), 192 deletions(-) diff --git a/app/server/lib/ActiveDoc.ts b/app/server/lib/ActiveDoc.ts index 0413f2ea..96bc80ea 100644 --- a/app/server/lib/ActiveDoc.ts +++ b/app/server/lib/ActiveDoc.ts @@ -85,7 +85,7 @@ import {AccessTokenOptions, AccessTokenResult, GristDocAPI, UIRowId} from 'app/p import {compileAclFormula} from 'app/server/lib/ACLFormula'; import {AssistanceSchemaPromptV1Context} from 'app/server/lib/Assistance'; import {AssistanceContext} from 'app/common/AssistancePrompts'; -import {Authorizer} from 'app/server/lib/Authorizer'; +import {Authorizer, RequestWithLogin} from 'app/server/lib/Authorizer'; import {checksumFile} from 'app/server/lib/checksumFile'; import {Client} from 'app/server/lib/Client'; import {DEFAULT_CACHE_TTL, DocManager} from 'app/server/lib/DocManager'; @@ -141,6 +141,7 @@ import remove = require('lodash/remove'); import sum = require('lodash/sum'); import without = require('lodash/without'); import zipObject = require('lodash/zipObject'); +import { getMetaTables } from './DocApi'; bluebird.promisifyAll(tmp); @@ -2791,7 +2792,6 @@ export function tableIdToRef(metaTables: { [p: string]: TableDataAction }, table // Helper that converts a Grist column colId to a ref given the corresponding table. export function colIdToRef(metaTables: {[p: string]: TableDataAction}, tableId: string, colId: string) { - const tableRef = tableIdToRef(metaTables, tableId); const [, , colRefs, columnData] = metaTables._grist_Tables_column; @@ -2804,6 +2804,31 @@ export function colIdToRef(metaTables: {[p: string]: TableDataAction}, tableId: return colRefs[colRowIndex]; } +// Helper that check if tableRef is used instead of tableId and return real tableId +// If metaTables is not define, activeDoc and req allow it to be created +interface MetaTables { + metaTables: { [p: string]: TableDataAction } +} +interface ActiveDocAndReq { + activeDoc: ActiveDoc, req: RequestWithLogin +} +export async function getRealTableId( + tableId: string, + options: MetaTables | ActiveDocAndReq + ): Promise { + if (parseInt(tableId)) { + const metaTables = "metaTables" in options + ? options.metaTables + : await getMetaTables(options.activeDoc, options.req); + const [, , tableRefs, tableData] = metaTables._grist_Tables; + if (tableRefs.indexOf(parseInt(tableId)) >= 0) { + const tableRowIndex = tableRefs.indexOf(parseInt(tableId)); + return tableData.tableId[tableRowIndex]!.toString(); + } + } + return tableId; +} + export function sanitizeApplyUAOptions(options?: ApplyUAOptions): ApplyUAOptions { return pick(options||{}, ['desc', 'otherId', 'linkId', 'parseStrings']); } diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index bcf93c7a..095cd307 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -32,7 +32,7 @@ import { TableOperationsImpl, TableOperationsPlatform } from 'app/plugin/TableOperationsImpl'; -import {ActiveDoc, colIdToRef as colIdToReference, tableIdToRef} from "app/server/lib/ActiveDoc"; +import {ActiveDoc, colIdToRef as colIdToReference, getRealTableId, tableIdToRef} from "app/server/lib/ActiveDoc"; import {appSettings} from "app/server/lib/AppSettings"; import {sendForCompletion} from 'app/server/lib/Assistance'; import { @@ -201,7 +201,7 @@ export class DocWorkerApi { if (!Object.keys(filters).every(col => Array.isArray(filters[col]))) { throw new ApiError("Invalid query: filter values must be arrays", 400); } - const tableId = optTableId || req.params.tableId; + const tableId = await getRealTableId(optTableId || req.params.tableId, {activeDoc, req}); const session = docSessionFromRequest(req); const {tableData} = await handleSandboxError(tableId, [], activeDoc.fetchQuery( session, {tableId, filters}, !immediate)); @@ -262,11 +262,6 @@ export class DocWorkerApi { }) ); - async function getMetaTables(activeDoc: ActiveDoc, req: RequestWithLogin) { - return await handleSandboxError("", [], - activeDoc.fetchMetaTables(docSessionFromRequest(req))); - } - const registerWebhook = async (activeDoc: ActiveDoc, req: RequestWithLogin, webhook: WebhookFields) => { const {fields, url} = await getWebhookSettings(activeDoc, req, null, webhook); if (!fields.eventTypes?.length) { @@ -337,7 +332,8 @@ export class DocWorkerApi { const trigger = webhookId ? activeDoc.triggers.getWebhookTriggerRecord(webhookId) : undefined; let currentTableId = trigger ? tablesTable.getValue(trigger.tableRef, 'tableId')! : undefined; const {url, eventTypes, isReadyColumn, name} = webhook; - const tableId = req.params.tableId || webhook.tableId; + const tableId = await getRealTableId(req.params.tableId || webhook.tableId, {metaTables}); + const fields: Partial = {}; if (url && !isUrlAllowed(url)) { @@ -387,7 +383,7 @@ export class DocWorkerApi { // Get the columns of the specified table in recordish format this._app.get('/api/docs/:docId/tables/:tableId/columns', canView, withDoc(async (activeDoc, req, res) => { - const tableId = req.params.tableId; + const tableId = await getRealTableId(req.params.tableId, {activeDoc, req}); const includeHidden = isAffirmative(req.query.hidden); const columns = await handleSandboxError('', [], activeDoc.getTableCols(docSessionFromRequest(req), tableId, includeHidden)); @@ -498,7 +494,7 @@ export class DocWorkerApi { withDoc(async (activeDoc, req, res) => { const colValues = req.body as BulkColValues; const count = colValues[Object.keys(colValues)[0]].length; - const op = getTableOperations(req, activeDoc); + const op = await getTableOperations(req, activeDoc); const ids = await op.addRecords(count, colValues); res.json(ids); }) @@ -527,7 +523,7 @@ export class DocWorkerApi { } } validateCore(RecordsPost, req, body); - const ops = getTableOperations(req, activeDoc); + const ops = await getTableOperations(req, activeDoc); const records = await ops.create(body.records); res.json({records}); }) @@ -558,7 +554,7 @@ export class DocWorkerApi { this._app.post('/api/docs/:docId/tables/:tableId/columns', canEdit, validate(ColumnsPost), withDoc(async (activeDoc, req, res) => { const body = req.body as Types.ColumnsPost; - const {tableId} = req.params; + const tableId = await getRealTableId(req.params.tableId, {activeDoc, req}); const actions = body.columns.map(({fields, id: colId}) => // AddVisibleColumn adds the column to all widgets of the table. // This isn't necessarily what the user wants, but it seems like a good default. @@ -590,7 +586,7 @@ export class DocWorkerApi { this._app.post('/api/docs/:docId/tables/:tableId/data/delete', canEdit, withDoc(async (activeDoc, req, res) => { const rowIds = req.body; - const op = getTableOperations(req, activeDoc); + const op = await getTableOperations(req, activeDoc); await op.destroy(rowIds); res.json(null); })); @@ -659,7 +655,7 @@ export class DocWorkerApi { const rowIds = columnValues.id; // sandbox expects no id column delete columnValues.id; - const ops = getTableOperations(req, activeDoc); + const ops = await getTableOperations(req, activeDoc); await ops.updateRecords(columnValues, rowIds); res.json(null); }) @@ -669,7 +665,7 @@ export class DocWorkerApi { this._app.patch('/api/docs/:docId/tables/:tableId/records', canEdit, validate(RecordsPatch), withDoc(async (activeDoc, req, res) => { const body = req.body as Types.RecordsPatch; - const ops = getTableOperations(req, activeDoc); + const ops = await getTableOperations(req, activeDoc); await ops.update(body.records); res.json(null); }) @@ -680,7 +676,7 @@ export class DocWorkerApi { withDoc(async (activeDoc, req, res) => { const tablesTable = activeDoc.docData!.getMetaTable("_grist_Tables"); const columnsTable = activeDoc.docData!.getMetaTable("_grist_Tables_column"); - const {tableId} = req.params; + const tableId = await getRealTableId(req.params.tableId, {activeDoc, req}); const tableRef = tablesTable.findMatchingRowId({tableId}); if (!tableRef) { throw new ApiError(`Table not found "${tableId}"`, 404); @@ -693,7 +689,7 @@ export class DocWorkerApi { } return {...col, id}; }); - const ops = getTableOperations(req, activeDoc, "_grist_Tables_column"); + const ops = await getTableOperations(req, activeDoc, "_grist_Tables_column"); await ops.update(columns); res.json(null); }) @@ -711,7 +707,7 @@ export class DocWorkerApi { } return {...table, id}; }); - const ops = getTableOperations(req, activeDoc, "_grist_Tables"); + const ops = await getTableOperations(req, activeDoc, "_grist_Tables"); await ops.update(tables); res.json(null); }) @@ -720,7 +716,7 @@ export class DocWorkerApi { // Add or update records given in records format this._app.put('/api/docs/:docId/tables/:tableId/records', canEdit, validate(RecordsPut), withDoc(async (activeDoc, req, res) => { - const ops = getTableOperations(req, activeDoc); + const ops = await getTableOperations(req, activeDoc); const body = req.body as Types.RecordsPut; const options = { add: !isAffirmative(req.query.noadd), @@ -740,7 +736,7 @@ export class DocWorkerApi { withDoc(async (activeDoc, req, res) => { const tablesTable = activeDoc.docData!.getMetaTable("_grist_Tables"); const columnsTable = activeDoc.docData!.getMetaTable("_grist_Tables_column"); - const {tableId} = req.params; + const tableId = await getRealTableId(req.params.tableId, {activeDoc, req}); const tableRef = tablesTable.findMatchingRowId({tableId}); if (!tableRef) { throw new ApiError(`Table not found "${tableId}"`, 404); @@ -785,7 +781,8 @@ export class DocWorkerApi { this._app.delete('/api/docs/:docId/tables/:tableId/columns/:colId', canEdit, withDoc(async (activeDoc, req, res) => { - const {tableId, colId} = req.params; + const {colId} = req.params; + const tableId = await getRealTableId(req.params.tableId, {activeDoc, req}); const actions = [ [ 'RemoveColumn', tableId, colId ] ]; await handleSandboxError(tableId, [colId], activeDoc.applyUserActions(docSessionFromRequest(req), actions) @@ -1941,12 +1938,21 @@ function getErrorPlatform(tableId: string): TableOperationsPlatform { }; } -function getTableOperations(req: RequestWithLogin, activeDoc: ActiveDoc, tableId?: string): TableOperationsImpl { +export async function getMetaTables(activeDoc: ActiveDoc, req: RequestWithLogin) { + return await handleSandboxError("", [], + activeDoc.fetchMetaTables(docSessionFromRequest(req))); +} + +async function getTableOperations( + req: RequestWithLogin, + activeDoc: ActiveDoc, + tableId?: string): Promise { const options: OpOptions = { parseStrings: !isAffirmative(req.query.noparse) }; + const realTableId = await getRealTableId(tableId ?? req.params.tableId, {activeDoc, req}); const platform: TableOperationsPlatform = { - ...getErrorPlatform(tableId ?? req.params.tableId), + ...getErrorPlatform(realTableId), applyUserActions(actions, opts) { if (!activeDoc) { throw new Error('no document'); } return activeDoc.applyUserActions( diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 8c7750d2..dabb2fe0 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -547,9 +547,7 @@ function testDocApi() { } it("GET /docs/{did}/tables/{tid}/data retrieves data in column format", async function () { - const resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/data`, chimpy); - assert.equal(resp.status, 200); - assert.deepEqual(resp.data, { + const data = { id: [1, 2, 3, 4], A: ['hello', '', '', ''], B: ['', 'world', '', ''], @@ -557,68 +555,73 @@ function testDocApi() { D: [null, null, null, null], E: ['HELLO', '', '', ''], manualSort: [1, 2, 3, 4] - }); + }; + const respWithTableId = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/data`, chimpy); + assert.equal(respWithTableId.status, 200); + assert.deepEqual(respWithTableId.data, data); + const respWithTableRef = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/1/data`, chimpy); + assert.equal(respWithTableRef.status, 200); + assert.deepEqual(respWithTableRef.data, data); }); it("GET /docs/{did}/tables/{tid}/records retrieves data in records format", async function () { - const resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/records`, chimpy); - assert.equal(resp.status, 200); - assert.deepEqual(resp.data, - { - records: - [ - { - id: 1, - fields: { - A: 'hello', - B: '', - C: '', - D: null, - E: 'HELLO', - }, + const data = { + records: + [ + { + id: 1, + fields: { + A: 'hello', + B: '', + C: '', + D: null, + E: 'HELLO', }, - { - id: 2, - fields: { - A: '', - B: 'world', - C: '', - D: null, - E: '', - }, + }, + { + id: 2, + fields: { + A: '', + B: 'world', + C: '', + D: null, + E: '', }, - { - id: 3, - fields: { - A: '', - B: '', - C: '', - D: null, - E: '', - }, + }, + { + id: 3, + fields: { + A: '', + B: '', + C: '', + D: null, + E: '', }, - { - id: 4, - fields: { - A: '', - B: '', - C: '', - D: null, - E: '', - }, + }, + { + id: 4, + fields: { + A: '', + B: '', + C: '', + D: null, + E: '', }, - ] - }); + }, + ] + }; + const respWithTableId = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/records`, chimpy); + assert.equal(respWithTableId.status, 200); + assert.deepEqual(respWithTableId.data, data); + const respWithTableRef = await axios.get( + `${serverUrl}/api/docs/${docIds.Timesheets}/tables/1/records`, chimpy); + assert.equal(respWithTableRef.status, 200); + assert.deepEqual(respWithTableRef.data, data); }); it('GET /docs/{did}/tables/{tid}/records honors the "hidden" param', async function () { const params = { hidden: true }; - const resp = await axios.get( - `${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/records`, - {...chimpy, params } - ); - assert.equal(resp.status, 200); - assert.deepEqual(resp.data.records[0], { + const data = { id: 1, fields: { manualSort: 1, @@ -628,7 +631,19 @@ function testDocApi() { D: null, E: 'HELLO', }, - }); + }; + const respWithTableId = await axios.get( + `${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/records`, + {...chimpy, params } + ); + assert.equal(respWithTableId.status, 200); + assert.deepEqual(respWithTableId.data.records[0], data); + const respWithTableRef = await axios.get( + `${serverUrl}/api/docs/${docIds.Timesheets}/tables/1/records`, + {...chimpy, params } + ); + assert.equal(respWithTableRef.status, 200); + assert.deepEqual(respWithTableRef.data.records[0], data); }); it("GET /docs/{did}/tables/{tid}/records handles errors and hidden columns", async function () { @@ -683,119 +698,121 @@ function testDocApi() { }); it("GET /docs/{did}/tables/{tid}/columns retrieves columns", async function () { - const resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/columns`, chimpy); - assert.equal(resp.status, 200); - assert.deepEqual(resp.data, - { - columns: [ - { - id: 'A', - fields: { - colRef: 2, - parentId: 1, - parentPos: 1, - type: 'Text', - widgetOptions: '', - isFormula: false, - formula: '', - label: 'A', - description: '', - untieColIdFromLabel: false, - summarySourceCol: 0, - displayCol: 0, - visibleCol: 0, - rules: null, - recalcWhen: 0, - recalcDeps: null - } - }, - { - id: 'B', - fields: { - colRef: 3, - parentId: 1, - parentPos: 2, - type: 'Text', - widgetOptions: '', - isFormula: false, - formula: '', - label: 'B', - description: '', - untieColIdFromLabel: false, - summarySourceCol: 0, - displayCol: 0, - visibleCol: 0, - rules: null, - recalcWhen: 0, - recalcDeps: null - } - }, - { - id: 'C', - fields: { - colRef: 4, - parentId: 1, - parentPos: 3, - type: 'Text', - widgetOptions: '', - isFormula: false, - formula: '', - label: 'C', - description: '', - untieColIdFromLabel: false, - summarySourceCol: 0, - displayCol: 0, - visibleCol: 0, - rules: null, - recalcWhen: 0, - recalcDeps: null - } - }, - { - id: 'D', - fields: { - colRef: 5, - parentId: 1, - parentPos: 3, - type: 'Any', - widgetOptions: '', - isFormula: true, - formula: '', - label: 'D', - description: '', - untieColIdFromLabel: false, - summarySourceCol: 0, - displayCol: 0, - visibleCol: 0, - rules: null, - recalcWhen: 0, - recalcDeps: null - } - }, - { - id: 'E', - fields: { - colRef: 6, - parentId: 1, - parentPos: 4, - type: 'Any', - widgetOptions: '', - isFormula: true, - formula: '$A.upper()', - label: 'E', - description: '', - untieColIdFromLabel: false, - summarySourceCol: 0, - displayCol: 0, - visibleCol: 0, - rules: null, - recalcWhen: 0, - recalcDeps: null - } + const data = { + columns: [ + { + id: 'A', + fields: { + colRef: 2, + parentId: 1, + parentPos: 1, + type: 'Text', + widgetOptions: '', + isFormula: false, + formula: '', + label: 'A', + description: '', + untieColIdFromLabel: false, + summarySourceCol: 0, + displayCol: 0, + visibleCol: 0, + rules: null, + recalcWhen: 0, + recalcDeps: null } - ] - } - ); + }, + { + id: 'B', + fields: { + colRef: 3, + parentId: 1, + parentPos: 2, + type: 'Text', + widgetOptions: '', + isFormula: false, + formula: '', + label: 'B', + description: '', + untieColIdFromLabel: false, + summarySourceCol: 0, + displayCol: 0, + visibleCol: 0, + rules: null, + recalcWhen: 0, + recalcDeps: null + } + }, + { + id: 'C', + fields: { + colRef: 4, + parentId: 1, + parentPos: 3, + type: 'Text', + widgetOptions: '', + isFormula: false, + formula: '', + label: 'C', + description: '', + untieColIdFromLabel: false, + summarySourceCol: 0, + displayCol: 0, + visibleCol: 0, + rules: null, + recalcWhen: 0, + recalcDeps: null + } + }, + { + id: 'D', + fields: { + colRef: 5, + parentId: 1, + parentPos: 3, + type: 'Any', + widgetOptions: '', + isFormula: true, + formula: '', + label: 'D', + description: '', + untieColIdFromLabel: false, + summarySourceCol: 0, + displayCol: 0, + visibleCol: 0, + rules: null, + recalcWhen: 0, + recalcDeps: null + } + }, + { + id: 'E', + fields: { + colRef: 6, + parentId: 1, + parentPos: 4, + type: 'Any', + widgetOptions: '', + isFormula: true, + formula: '$A.upper()', + label: 'E', + description: '', + untieColIdFromLabel: false, + summarySourceCol: 0, + displayCol: 0, + visibleCol: 0, + rules: null, + recalcWhen: 0, + recalcDeps: null + } + } + ] + }; + const respWithTableId = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/columns`, chimpy); + assert.equal(respWithTableId.status, 200); + assert.deepEqual(respWithTableId.data, data); + const respWithTableRef = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/1/columns`, chimpy); + assert.equal(respWithTableRef.status, 200); + assert.deepEqual(respWithTableRef.data, data); }); it('GET /docs/{did}/tables/{tid}/columns retrieves hidden columns when "hidden" is set', async function () { @@ -869,6 +886,13 @@ function testDocApi() { ] }); + // POST /columns: Create new columns using tableRef in URL + resp = await axios.post(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/5/columns`, { + columns: [{id: "NewCol6", fields: {}}], + }, chimpy); + assert.equal(resp.status, 200); + assert.deepEqual(resp.data, {columns: [{id: "NewCol6"}]}); + // POST /columns to invalid table ID resp = await axios.post(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/NoSuchTable/columns`, {columns: [{}]}, chimpy); @@ -1032,6 +1056,7 @@ function testDocApi() { {colId: "NewCol4", label: 'NewCol4'}, {colId: "NewCol4_2", label: 'NewCol4_2'}, // NewCol5 is hidden by ACL + {colId: "NewCol6", label: 'NewCol6'}, ]); resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/tables/NewTable2_2/columns`, chimpy); From 90ce8825d9e9ca6a42a2021854a9d3afbfe60ed0 Mon Sep 17 00:00:00 2001 From: Dmitry Sagalovskiy Date: Fri, 13 Oct 2023 06:01:28 +0000 Subject: [PATCH 12/13] Translated using Weblate (Ukrainian) Currently translated at 84.6% (824 of 973 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/uk/ --- static/locales/uk.client.json | 106 ++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 24 deletions(-) diff --git a/static/locales/uk.client.json b/static/locales/uk.client.json index 15105db5..f4a3340d 100644 --- a/static/locales/uk.client.json +++ b/static/locales/uk.client.json @@ -61,7 +61,9 @@ "Save Document": "Зберегти документ", "Send to Google Drive": "Надіслати на Google Drive", "Show in folder": "Показати в папці", - "Work on a Copy": "Працювати над копією" + "Work on a Copy": "Працювати над копією", + "Download...": "Завантажити...", + "Share": "Поділитися" }, "MakeCopyMenu": { "Be careful, the original has changes not in this document. Those changes will be overwritten.": "Зверніть увагу! В оригіналі є зміни, яких немає в цьому документі. Ці зміни будуть перезаписані.", @@ -86,7 +88,10 @@ "Workspace": "Робочий простір", "You do not have write access to the selected workspace": "Ви не маєте права на запис у вибраному робочому просторі", "You do not have write access to this site": "Ви не маєте права на запис на цьому сайті", - "However, it appears to be already identical.": "Втім, схоже, що вони вже ідентичні." + "However, it appears to be already identical.": "Втім, схоже, що вони вже ідентичні.", + "Remove all data but keep the structure to use as a template": "Видалити всі дані, але зберегти структуру, щоб використовувати як шаблон", + "Remove document history (can significantly reduce file size)": "Видалити історію документа (може значно зменшити розмір файлу)", + "Download full document and history": "Завантажити повний документ та історію" }, "SortConfig": { "Add Column": "Додати стовпець", @@ -134,7 +139,7 @@ "Freeze {{count}} columns_other": "Закріпити {{count}} стовпців", "Hide {{count}} columns_other": "Сховати {{count}} стовпців", "Insert column to the {{to}}": "Вставити стовпець у {{to}}", - "More sort options ...": "Більше варіантів сортування…", + "More sort options ...": "Більше опцій сортування…", "Rename column": "Перейменувати стовпець", "Reset {{count}} columns_one": "Скинути стовпець", "Reset {{count}} entire columns_one": "Скинути весь стовпець", @@ -144,7 +149,9 @@ "Sorted (#{{count}})_other": "Відсортовано (#{{count}})", "Unfreeze all columns": "Відкріпити всі стовпці", "Unfreeze {{count}} columns_one": "Відкріпити цей стовпець", - "Unfreeze {{count}} columns_other": "Відкріпити {{count}} стовпців" + "Unfreeze {{count}} columns_other": "Відкріпити {{count}} стовпців", + "Insert column to the right": "Вставити стовпець праворуч", + "Insert column to the left": "Вставити стовпець ліворуч" }, "ThemeConfig": { "Switch appearance automatically to match system": "Автоматично змінювати оформлення відповідно до системи", @@ -170,7 +177,12 @@ "Welcome to {{orgName}}": "Ласкаво просимо до {{orgName}}", "You have read-only access to this site. Currently there are no documents.": "Ви маєте доступ до цього сайту лише для читання. Наразі документів немає.", "personal site": "особистий сайт", - "{{signUp}} to save your work. ": "{{signUp}}, щоб зберегти вашу роботу. " + "{{signUp}} to save your work. ": "{{signUp}}, щоб зберегти вашу роботу. ", + "Welcome to Grist, {{- name}}!": "Ласкаво просимо до Grist, {{- name}}!", + "Visit our {{link}} to learn more about Grist.": "Відвідайте наш {{link}}, щоб дізнатися більше про Grist.", + "Welcome to {{- orgName}}": "Ласкаво просимо до {{- orgName}}", + "Sign in": "Увійти в систему", + "To use Grist, please either sign up or sign in.": "Щоб користуватися Grist, будь ласка, зареєструйтеся або увійдіть." }, "NotifyUI": { "Go to your free personal site": "Перейдіть на свій безкоштовний персональний сайт", @@ -181,7 +193,8 @@ "Notifications": "Повідомлення", "Renew": "Поновити", "Report a problem": "Повідомити про проблему", - "Upgrade Plan": "Змінити тарифний план" + "Upgrade Plan": "Змінити тарифний план", + "Manage billing": "Управління рахунком" }, "ViewSectionMenu": { "Revert": "Повернутися", @@ -268,7 +281,9 @@ "{{wrongTypeCount}} non-{{columnType}} columns are not shown_one": "{{wrongTypeCount}} не-{{columnType}} стовпець не відображається", "{{wrongTypeCount}} non-{{columnType}} columns are not shown_other": "{{wrongTypeCount}} не-{{columnType}} стовпці не відображаються", "Widget does not require any permissions.": "Віджет не вимагає ніяких дозволів.", - "Widget needs to {{read}} the current table.": "Віджету необхідно {{read}} поточну таблицю." + "Widget needs to {{read}} the current table.": "Віджету необхідно {{read}} поточну таблицю.", + "No {{columnType}} columns in table.": "У таблиці немає стовпців типу {{columnType}}.", + "Clear selection": "Очистити вибір" }, "ACUserManager": { "Invite new member": "Запросити нового користувача", @@ -310,7 +325,9 @@ "Remove column {{- colId }} from {{- tableId }} rules": "Видалити стовпець {{- colId }} з правил {{- tableId }}", "When adding table rules, automatically add a rule to grant OWNER full access.": "При додаванні правил таблиці, автоматично додавати правило для надання ВЛАСНИКУ повного доступу.", "Seed rules": "Успадковані правила", - "Allow everyone to copy the entire document, or view it in full in fiddle mode.\nUseful for examples and templates, but not for sensitive data.": "Дозволити кожному скопіювати весь документ або переглянути його повністю в режимі створення нових копій.\nКорисно для прикладів і шаблонів, але не для конфіденційних даних." + "Allow everyone to copy the entire document, or view it in full in fiddle mode.\nUseful for examples and templates, but not for sensitive data.": "Дозволити кожному скопіювати весь документ або переглянути його повністю в режимі створення нових копій.\nКорисно для прикладів і шаблонів, але не для конфіденційних даних.", + "Allow editors to edit structure (e.g. modify and delete tables, columns, layouts), and to write formulas, which give access to all data regardless of read restrictions.": "Дозволити редакторам редагувати структуру (наприклад, змінювати та видаляти таблиці, стовпці, макети), а також писати формули, які надають доступ до всіх даних незалежно від обмежень на читання.", + "This default should be changed if editors' access is to be limited. ": "Цей параметр слід змінити, якщо ви хочете обмежити доступ редакторів. " }, "AccountPage": { "API": "API", @@ -341,7 +358,14 @@ "Sign in": "Увійти в систему", "Toggle Mobile Mode": "Переключити в мобільний режим", "Document Settings": "Параметри документа", - "Switch Accounts": "Змінити обліковий запис" + "Switch Accounts": "Змінити обліковий запис", + "Activation": "Активація", + "Support Grist": "Підтримати Grist", + "Upgrade Plan": "Оновити План", + "Use This Template": "Використати цей шаблон", + "Billing Account": "Рахунок оплати", + "Sign In": "Увійти в систему", + "Sign Up": "Зареєструватися" }, "ViewAsDropdown": { "View As": "Переглянути як", @@ -352,7 +376,8 @@ "Action Log failed to load": "Не вдалося завантажити журнал дій", "Table {{tableId}} was subsequently removed in action #{{actionNum}}": "Таблиця {{tableId}} згодом була видалена під час події #{{actionNum}}", "This row was subsequently removed in action {{action.actionNum}}": "Ця строка згодом була видалена під час події {{action.actionNum}}", - "Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "Колонка {{colId}} згодом була видалена під час події #{{action.actionNum}}" + "Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "Колонка {{colId}} згодом була видалена під час події #{{action.actionNum}}", + "All tables": "Всі таблиці" }, "ApiKey": { "Click to show": "Натисніть, щоб показати", @@ -377,7 +402,8 @@ "Home Page": "Домашня сторінка", "Legacy": "Застаріла версія", "Personal Site": "Особистий сайт", - "Team Site": "Сайт команди" + "Team Site": "Сайт команди", + "Grist Templates": "Шаблони від Grist" }, "CellContextMenu": { "Clear cell": "Очистити клітинку", @@ -398,7 +424,11 @@ "Reset {{count}} columns_one": "Скинути стовпець", "Reset {{count}} columns_other": "Скинути {{count}} стовпці", "Reset {{count}} entire columns_one": "Скинути весь стовпець", - "Reset {{count}} entire columns_other": "Скинути {{count}} всі стовпці" + "Reset {{count}} entire columns_other": "Скинути {{count}} всі стовпці", + "Copy": "Копіювати", + "Comment": "Коментар", + "Cut": "Вирізати", + "Paste": "Вставити" }, "ColorSelect": { "Apply": "Застосовувати", @@ -487,7 +517,9 @@ "API": "API", "Document ID copied to clipboard": "Ідентифікатор документа скопійований у буфер", "Ok": "ОК", - "Engine (experimental {{span}} change at own risk):": "Обчислювальна система (експериментальна версія {{span}} змінюйте на власний ризик):" + "Engine (experimental {{span}} change at own risk):": "Обчислювальна система (експериментальна версія {{span}} змінюйте на власний ризик):", + "Manage Webhooks": "Керування веб-хуками", + "Webhooks": "Веб-хуки" }, "DocTour": { "No valid document tour": "Немає дійсного огляду документа", @@ -516,7 +548,8 @@ "GristDoc": { "Added new linked section to view {{viewName}}": "Додано новий пов’язаний розділ для перегляду {{viewName}}", "Import from file": "Імпортувати з файлу", - "Saved linked section {{title}} in view {{name}}": "Збережений зв'язаний розділ {{title}}, що відображається в {{name}}" + "Saved linked section {{title}} in view {{name}}": "Збережений зв'язаний розділ {{title}}, що відображається в {{name}}", + "go to webhook settings": "перейти до налаштувань веб-хуків" }, "HomeLeftPane": { "Delete": "Видалити", @@ -526,17 +559,33 @@ "Manage Users": "Керувати користувачами", "Delete {{workspace}} and all included documents?": "Видалити {{workspace}} та всі включені документи?", "Create Workspace": "Створити робочій простір", - "Examples & Templates": "Приклади та шаблони", + "Examples & Templates": "Шаблони", "Import Document": "Імпортувати документ", "Trash": "Кошик", "Workspace will be moved to Trash.": "Робочий простір буде переміщено до кошика.", "Rename": "Перейменувати", - "Workspaces": "Робочій простір" + "Workspaces": "Робочій простір", + "Tutorial": "Туторіал" }, "Importer": { "Merge rows that match these fields:": "Об'єднати рядки, які відповідають цим полям:", "Update existing records": "Оновити існуючі записи", - "Select fields to match on": "Оберіть поля для зіставлення" + "Select fields to match on": "Оберіть поля для зіставлення", + "Column mapping": "Співставлення колонок", + "Grist column": "Стовпець у Grist", + "{{count}} unmatched field_one": "{{count}} невідповідне поле", + "{{count}} unmatched field in import_one": "{{count}} невідповідне поле при імпорті", + "Revert": "Повернути", + "Skip Import": "Пропустити імпорт", + "{{count}} unmatched field_other": "{{count}} невідповідних полів", + "New Table": "Нова таблиця", + "Skip": "Пропустити", + "Column Mapping": "Співставлення колонок", + "Destination table": "Таблиця призначення", + "Skip Table on Import": "Пропустити таблицю при імпорті", + "Import from file": "Імпорт з файлу", + "{{count}} unmatched field in import_other": "{{count}} невідповідних полів при імпорті", + "Source column": "Вихідний стовпець" }, "LeftPanelCommon": { "Help Center": "Центр допомоги" @@ -609,12 +658,13 @@ "Select Widget": "Виберіть віджет", "Series_one": "Серії", "Series_other": "Серії", - "Sort & Filter": "Сортування та фільтрація", + "Sort & Filter": "Порядок та фільтр", "TRANSFORM": "ПЕРЕТВОРИТИ", "Theme": "Тема", "WIDGET TITLE": "НАЗВА ВІДЖЕТА", "Widget": "Віджет", - "You do not have edit access to this document": "Ви не маєте права на редагування цього документа" + "You do not have edit access to this document": "Ви не маєте права на редагування цього документа", + "Add referenced columns": "Додати стовпець за посиланням" }, "RowContextMenu": { "Copy anchor link": "Скопіювати якірне посилання", @@ -679,7 +729,7 @@ "Make On-Demand": "Встановити статус \"на вимогу\"" }, "ViewLayoutMenu": { - "Advanced Sort & Filter": "Розширене сортування та фільтр", + "Advanced Sort & Filter": "Розширені порядок та фільтр", "Copy anchor link": "Скопіювати якірне посилання", "Data selection": "Вибір даних", "Delete record": "Видалити запис", @@ -718,7 +768,8 @@ "Override widget title": "Перевизначити назву віджета", "Provide a table name": "Вкажіть ім'я таблиці", "Save": "Зберегти", - "WIDGET TITLE": "НАЗВА ВІДЖЕТА" + "WIDGET TITLE": "НАЗВА ВІДЖЕТА", + "WIDGET DESCRIPTION": "ОПИС ВІДЖЕТУ" }, "breadcrumbs": { "fiddle": "fiddle", @@ -749,7 +800,10 @@ "You do not have access to this organization's documents.": "У вас немає доступу до документів цієї організації.", "Sign in to access this organization's documents.": "Увійдіть, щоб отримати доступ до документів цієї організації.", "The requested page could not be found.{{separator}}Please check the URL and try again.": "Не вдалося знайти запитувану сторінку.{{separator}}Будь ласка, перевірте URL-адресу і спробуйте ще раз.", - "You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Ви увійшли як {{email}}. Ви можете увійти під іншим обліковим записом або попросити доступ у адміністратора." + "You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.": "Ви увійшли як {{email}}. Ви можете увійти під іншим обліковим записом або попросити доступ у адміністратора.", + "Account deleted{{suffix}}": "Обліковий запис видалено{{suffix}}", + "Your account has been deleted.": "Ваш обліковий запис видалено.", + "Sign up": "Зареєструватися" }, "menus": { "* Workspaces are available on team plans. ": "* Робочі простори доступні в командних тарифах. ", @@ -783,7 +837,8 @@ "Find Next ": "Знайти наступне ", "Find Previous ": "Знайти попереднє ", "Search in document": "Пошук у документі", - "No results": "Немає результатів" + "No results": "Немає результатів", + "Search": "Пошук" }, "sendToDrive": { "Sending file to Google Drive": "Надсилання файлу на Google Диск" @@ -809,7 +864,10 @@ "Default cell style": "Стиль клітинки за замовчуванням", "Mixed style": "Змішаний стиль", "Open row styles": "Відкрити стилі рядків", - "Cell Style": "Стиль клітинки" + "Cell Style": "Стиль клітинки", + "HEADER STYLE": "СТИЛЬ ЗАГОЛОВКА", + "Header Style": "Стиль Заголовка", + "Default header style": "За замовчуванням" }, "ChoiceTextBox": { "CHOICES": "ВАРІАНТИ" From eb55afcbc4749cbcae80488dcac23ee169e31218 Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 16 Oct 2023 02:17:43 +0200 Subject: [PATCH 13/13] Option to export colId as header in CSV / XSLX instead of label (#688) (#692) --- app/server/lib/DocApi.ts | 3 +- app/server/lib/Export.ts | 7 +++- app/server/lib/ExportCSV.ts | 57 ++++++++++++++++++---------- app/server/lib/workerExporter.ts | 64 ++++++++++++++++++++------------ test/server/lib/DocApi.ts | 53 +++++++++++++++++++------- 5 files changed, 126 insertions(+), 58 deletions(-) diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index 095cd307..03fed713 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -1202,12 +1202,13 @@ export class DocWorkerApi { this._app.get('/api/docs/:docId/download/xlsx', canView, withDoc(async (activeDoc, req, res) => { // Query DB for doc metadata to get the doc title (to use as the filename). const {name: docTitle} = await this._dbManager.getDoc(req); - const options = !_.isEmpty(req.query) ? this._getDownloadOptions(req, docTitle) : { + const options: DownloadOptions = !_.isEmpty(req.query) ? this._getDownloadOptions(req, docTitle) : { filename: docTitle, tableId: '', viewSectionId: undefined, filters: [], sortOrder: [], + header: 'label' }; await downloadXLSX(activeDoc, req, res, options); })); diff --git a/app/server/lib/Export.ts b/app/server/lib/Export.ts index 0d590f20..ab446989 100644 --- a/app/server/lib/Export.ts +++ b/app/server/lib/Export.ts @@ -17,7 +17,7 @@ import {BaseFormatter, createFullFormatterFromDocData} from 'app/common/ValueFor import {ActiveDoc} from 'app/server/lib/ActiveDoc'; import {RequestWithLogin} from 'app/server/lib/Authorizer'; import {docSessionFromRequest} from 'app/server/lib/DocSession'; -import {optIntegerParam, optJsonParam, stringParam} from 'app/server/lib/requestUtils'; +import {optIntegerParam, optJsonParam, optStringParam, stringParam} from 'app/server/lib/requestUtils'; import {ServerColumnGetters} from 'app/server/lib/ServerColumnGetters'; import * as express from 'express'; import * as _ from 'underscore'; @@ -90,6 +90,8 @@ export interface ExportData { docSettings: DocumentSettings; } +export type ExportHeader = 'colId' | 'label'; + /** * Export parameters that identifies a section, filters, sort order. */ @@ -99,6 +101,7 @@ export interface ExportParameters { sortOrder?: number[]; filters?: Filter[]; linkingFilter?: FilterColValues; + header?: ExportHeader; } /** @@ -117,6 +120,7 @@ export function parseExportParameters(req: express.Request): ExportParameters { const sortOrder = optJsonParam(req.query.activeSortSpec, []) as number[]; const filters: Filter[] = optJsonParam(req.query.filters, []); const linkingFilter: FilterColValues = optJsonParam(req.query.linkingFilter, null); + const header = optStringParam(req.query.header, 'header', {allowed: ['label', 'colId']}) as ExportHeader | undefined; return { tableId, @@ -124,6 +128,7 @@ export function parseExportParameters(req: express.Request): ExportParameters { sortOrder, filters, linkingFilter, + header, }; } diff --git a/app/server/lib/ExportCSV.ts b/app/server/lib/ExportCSV.ts index 8f3949d5..a0ff027d 100644 --- a/app/server/lib/ExportCSV.ts +++ b/app/server/lib/ExportCSV.ts @@ -1,7 +1,7 @@ import {ApiError} from 'app/common/ApiError'; import {ActiveDoc} from 'app/server/lib/ActiveDoc'; import {FilterColValues} from "app/common/ActiveDocAPI"; -import {DownloadOptions, ExportData, exportSection, exportTable, Filter} from 'app/server/lib/Export'; +import {DownloadOptions, ExportData, ExportHeader, exportSection, exportTable, Filter} from 'app/server/lib/Export'; import log from 'app/server/lib/log'; import * as bluebird from 'bluebird'; import contentDisposition from 'content-disposition'; @@ -17,11 +17,13 @@ bluebird.promisifyAll(csv); export async function downloadCSV(activeDoc: ActiveDoc, req: express.Request, res: express.Response, options: DownloadOptions) { log.info('Generating .csv file...'); - const {filename, tableId, viewSectionId, filters, sortOrder, linkingFilter} = options; + const {filename, tableId, viewSectionId, filters, sortOrder, linkingFilter, header} = options; const data = viewSectionId ? - await makeCSVFromViewSection( - activeDoc, viewSectionId, sortOrder || null, filters || null, linkingFilter || null, req) : - await makeCSVFromTable(activeDoc, tableId, req); + await makeCSVFromViewSection({ + activeDoc, viewSectionId, sortOrder: sortOrder || null, filters: filters || null, + linkingFilter: linkingFilter || null, header, req + }) : + await makeCSVFromTable({activeDoc, tableId, header, req}); res.set('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', contentDisposition(filename + '.csv')); res.send(data); @@ -32,36 +34,51 @@ export async function downloadCSV(activeDoc: ActiveDoc, req: express.Request, * * See https://github.com/wdavidw/node-csv for API details. * - * @param {Object} activeDoc - the activeDoc that the table being converted belongs to. - * @param {Integer} viewSectionId - id of the viewsection to export. - * @param {Integer[]} activeSortOrder (optional) - overriding sort order. - * @param {Filter[]} filters (optional) - filters defined from ui. + * @param {Object} options - options for the export. + * @param {Object} options.activeDoc - the activeDoc that the table being converted belongs to. + * @param {Integer} options.viewSectionId - id of the viewsection to export. + * @param {Integer[]} options.activeSortOrder (optional) - overriding sort order. + * @param {Filter[]} options.filters (optional) - filters defined from ui. + * @param {FilterColValues} options.linkingFilter (optional) - linking filter defined from ui. + * @param {string} options.header (optional) - which field of the column to use as header + * @param {express.Request} options.req - the request object. + * * @return {Promise} Promise for the resulting CSV. */ -export async function makeCSVFromViewSection( +export async function makeCSVFromViewSection({ + activeDoc, viewSectionId, sortOrder = null, filters = null, linkingFilter = null, header, req +}: { activeDoc: ActiveDoc, viewSectionId: number, sortOrder: number[] | null, filters: Filter[] | null, linkingFilter: FilterColValues | null, - req: express.Request) { + header?: ExportHeader, + req: express.Request +}) { const data = await exportSection(activeDoc, viewSectionId, sortOrder, filters, linkingFilter, req); - const file = convertToCsv(data); + const file = convertToCsv(data, { header }); return file; } /** * Returns a csv stream of a table that can be transformed or parsed. * - * @param {Object} activeDoc - the activeDoc that the table being converted belongs to. - * @param {Integer} tableId - id of the table to export. + * @param {Object} options - options for the export. + * @param {Object} options.activeDoc - the activeDoc that the table being converted belongs to. + * @param {Integer} options.tableId - id of the table to export. + * @param {string} options.header (optional) - which field of the column to use as header + * @param {express.Request} options.req - the request object. + * * @return {Promise} Promise for the resulting CSV. */ -export async function makeCSVFromTable( +export async function makeCSVFromTable({ activeDoc, tableId, header, req }: { activeDoc: ActiveDoc, tableId: string, - req: express.Request) { + header?: ExportHeader, + req: express.Request +}) { if (!activeDoc.docData) { throw new Error('No docData in active document'); @@ -76,7 +93,7 @@ export async function makeCSVFromTable( } const data = await exportTable(activeDoc, tableRef, req); - const file = convertToCsv(data); + const file = convertToCsv(data, { header }); return file; } @@ -84,13 +101,13 @@ function convertToCsv({ rowIds, access, columns: viewColumns, - docSettings -}: ExportData) { +}: ExportData, options: { header?: ExportHeader }) { // create formatters for columns const formatters = viewColumns.map(col => col.formatter); // Arrange the data into a row-indexed matrix, starting with column headers. - const csvMatrix = [viewColumns.map(col => col.label)]; + const colPropertyAsHeader = options.header ?? 'label'; + const csvMatrix = [viewColumns.map(col => col[colPropertyAsHeader])]; // populate all the rows with values as strings rowIds.forEach(row => { csvMatrix.push(access.map((getter, c) => formatters[c].formatAny(getter(row)))); diff --git a/app/server/lib/workerExporter.ts b/app/server/lib/workerExporter.ts index c6992c20..212ed599 100644 --- a/app/server/lib/workerExporter.ts +++ b/app/server/lib/workerExporter.ts @@ -1,7 +1,7 @@ import {PassThrough} from 'stream'; import {FilterColValues} from "app/common/ActiveDocAPI"; import {ActiveDocSource, doExportDoc, doExportSection, doExportTable, - ExportData, ExportParameters, Filter} from 'app/server/lib/Export'; + ExportData, ExportHeader, ExportParameters, Filter} from 'app/server/lib/Export'; import {createExcelFormatter} from 'app/server/lib/ExcelFormatter'; import * as log from 'app/server/lib/log'; import {Alignment, Border, Buffer as ExcelBuffer, stream as ExcelWriteStream, @@ -79,26 +79,34 @@ export async function doMakeXLSXFromOptions( stream: Stream, options: ExportParameters ) { - const {tableId, viewSectionId, filters, sortOrder, linkingFilter} = options; + const {tableId, viewSectionId, filters, sortOrder, linkingFilter, header} = options; if (viewSectionId) { - return doMakeXLSXFromViewSection(activeDocSource, testDates, stream, viewSectionId, - sortOrder || null, filters || null, linkingFilter || null); + return doMakeXLSXFromViewSection({activeDocSource, testDates, stream, viewSectionId, header, + sortOrder: sortOrder || null, filters: filters || null, linkingFilter: linkingFilter || null}); } else if (tableId) { - return doMakeXLSXFromTable(activeDocSource, testDates, stream, tableId); + return doMakeXLSXFromTable({activeDocSource, testDates, stream, tableId, header}); } else { - return doMakeXLSX(activeDocSource, testDates, stream); + return doMakeXLSX({activeDocSource, testDates, stream, header}); } } /** + * @async * Returns a XLSX stream of a view section that can be transformed or parsed. * - * @param {Object} activeDoc - the activeDoc that the table being converted belongs to. - * @param {Integer} viewSectionId - id of the viewsection to export. - * @param {Integer[]} activeSortOrder (optional) - overriding sort order. - * @param {Filter[]} filters (optional) - filters defined from ui. + * @param {Object} options - options for the export. + * @param {Object} options.activeDocSource - the activeDoc that the table being converted belongs to. + * @param {Integer} options.viewSectionId - id of the viewsection to export. + * @param {Integer[]} options.activeSortOrder (optional) - overriding sort order. + * @param {Filter[]} options.filters (optional) - filters defined from ui. + * @param {FilterColValues} options.linkingFilter (optional) + * @param {Stream} options.stream - the stream to write to. + * @param {boolean} options.testDates - whether to use static dates for testing. + * @param {string} options.header (optional) - which field of the column to use as header */ -async function doMakeXLSXFromViewSection( +async function doMakeXLSXFromViewSection({ + activeDocSource, testDates, stream, viewSectionId, sortOrder, filters, linkingFilter, header +}: { activeDocSource: ActiveDocSource, testDates: boolean, stream: Stream, @@ -106,27 +114,35 @@ async function doMakeXLSXFromViewSection( sortOrder: number[] | null, filters: Filter[] | null, linkingFilter: FilterColValues | null, -) { + header?: ExportHeader, +}) { const data = await doExportSection(activeDocSource, viewSectionId, sortOrder, filters, linkingFilter); - const {exportTable, end} = convertToExcel(stream, testDates); + const {exportTable, end} = convertToExcel(stream, testDates, {header}); exportTable(data); return end(); } /** + * @async * Returns a XLSX stream of a table that can be transformed or parsed. * - * @param {Object} activeDoc - the activeDoc that the table being converted belongs to. - * @param {Integer} tableId - id of the table to export. + * @param {Object} options - options for the export. + * @param {Object} options.activeDocSource - the activeDoc that the table being converted belongs to. + * @param {Integer} options.tableId - id of the table to export. + * @param {Stream} options.stream - the stream to write to. + * @param {boolean} options.testDates - whether to use static dates for testing. + * @param {string} options.header (optional) - which field of the column to use as header + * */ -async function doMakeXLSXFromTable( +async function doMakeXLSXFromTable({activeDocSource, testDates, stream, tableId, header}: { activeDocSource: ActiveDocSource, testDates: boolean, stream: Stream, tableId: string, -) { + header?: ExportHeader, +}) { const data = await doExportTable(activeDocSource, {tableId}); - const {exportTable, end} = convertToExcel(stream, testDates); + const {exportTable, end} = convertToExcel(stream, testDates, {header}); exportTable(data); return end(); } @@ -134,12 +150,13 @@ async function doMakeXLSXFromTable( /** * Creates excel document with all tables from an active Grist document. */ -async function doMakeXLSX( +async function doMakeXLSX({activeDocSource, testDates, stream, header}: { activeDocSource: ActiveDocSource, testDates: boolean, stream: Stream, -): Promise { - const {exportTable, end} = convertToExcel(stream, testDates); + header?: ExportHeader, +}): Promise { + const {exportTable, end} = convertToExcel(stream, testDates, {header}); await doExportDoc(activeDocSource, async (table: ExportData) => exportTable(table)); return end(); } @@ -152,7 +169,7 @@ async function doMakeXLSX( * (The second option is for grist-static; at the time of writing * WorkbookWriter doesn't appear to be available in a browser context). */ -function convertToExcel(stream: Stream|undefined, testDates: boolean): { +function convertToExcel(stream: Stream|undefined, testDates: boolean, options: { header?: ExportHeader }): { exportTable: (table: ExportData) => void, end: () => Promise, } { @@ -206,7 +223,8 @@ function convertToExcel(stream: Stream|undefined, testDates: boolean): { const formatters = columns.map(col => createExcelFormatter(col.formatter.type, col.formatter.widgetOpts)); // Generate headers for all columns with correct styles for whole column. // Actual header style for a first row will be overwritten later. - ws.columns = columns.map((col, c) => ({ header: col.label, style: formatters[c].style() })); + const colHeader = options.header ?? 'label'; + ws.columns = columns.map((col, c) => ({ header: col[colHeader], style: formatters[c].style() })); // style up the header row for (let i = 1; i <= columns.length; i++) { // apply to all rows (including header) diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index dabb2fe0..845b019c 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -230,6 +230,14 @@ describe('DocApi', function () { // Contains the tests. This is where you want to add more test. function testDocApi() { + async function generateDocAndUrl(docName: string = "Dummy") { + const wid = (await userApi.getOrgWorkspaces('current')).find((w) => w.name === 'Private')!.id; + const docId = await userApi.newDoc({name: docName}, wid); + const docUrl = `${serverUrl}/api/docs/${docId}`; + const tableUrl = `${serverUrl}/api/docs/${docId}/tables/Table1`; + return { docUrl, tableUrl, docId }; + } + it("creator should be owner of a created ws", async () => { const kiwiEmail = 'kiwi@getgrist.com'; const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id; @@ -1080,13 +1088,13 @@ function testDocApi() { }); describe("/docs/{did}/tables/{tid}/columns", function () { - async function generateDocAndUrl(docName: string = "Dummy") { - const wid = (await userApi.getOrgWorkspaces('current')).find((w) => w.name === 'Private')!.id; - const docId = await userApi.newDoc({name: docName}, wid); - const url = `${serverUrl}/api/docs/${docId}/tables/Table1/columns`; - return { url, docId }; + async function generateDocAndUrlForColumns(name: string) { + const { tableUrl, docId } = await generateDocAndUrl(name); + return { + docId, + url: `${tableUrl}/columns`, + }; } - describe("PUT /docs/{did}/tables/{tid}/columns", function () { async function getColumnFieldsMapById(url: string, params: any) { const result = await axios.get(url, {...chimpy, params}); @@ -1104,7 +1112,7 @@ function testDocApi() { expectedFieldsByColId: Record, opts?: { getParams?: any } ) { - const {url} = await generateDocAndUrl('ColumnsPut'); + const {url} = await generateDocAndUrlForColumns('ColumnsPut'); const body: ColumnsPut = { columns }; const resp = await axios.put(url, body, {...chimpy, params}); assert.equal(resp.status, 200); @@ -1175,7 +1183,7 @@ function testDocApi() { it('should forbid update by viewers', async function () { // given - const { url, docId } = await generateDocAndUrl('ColumnsPut'); + const { url, docId } = await generateDocAndUrlForColumns('ColumnsPut'); await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}}); // when @@ -1187,7 +1195,7 @@ function testDocApi() { it("should return 404 when table is not found", async function() { // given - const { url } = await generateDocAndUrl('ColumnsPut'); + const { url } = await generateDocAndUrlForColumns('ColumnsPut'); const notFoundUrl = url.replace("Table1", "NonExistingTable"); // when @@ -1201,7 +1209,7 @@ function testDocApi() { describe("DELETE /docs/{did}/tables/{tid}/columns/{colId}", function () { it('should delete some column', async function() { - const {url} = await generateDocAndUrl('ColumnDelete'); + const {url} = await generateDocAndUrlForColumns('ColumnDelete'); const deleteUrl = url + '/A'; const resp = await axios.delete(deleteUrl, chimpy); @@ -1215,7 +1223,7 @@ function testDocApi() { }); it('should return 404 if table not found', async function() { - const {url} = await generateDocAndUrl('ColumnDelete'); + const {url} = await generateDocAndUrlForColumns('ColumnDelete'); const deleteUrl = url.replace("Table1", "NonExistingTable") + '/A'; const resp = await axios.delete(deleteUrl, chimpy); @@ -1224,7 +1232,7 @@ function testDocApi() { }); it('should return 404 if column not found', async function() { - const {url} = await generateDocAndUrl('ColumnDelete'); + const {url} = await generateDocAndUrlForColumns('ColumnDelete'); const deleteUrl = url + '/NonExistingColId'; const resp = await axios.delete(deleteUrl, chimpy); @@ -1233,7 +1241,7 @@ function testDocApi() { }); it('should forbid column deletion by viewers', async function() { - const {url, docId} = await generateDocAndUrl('ColumnDelete'); + const {url, docId} = await generateDocAndUrlForColumns('ColumnDelete'); await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}}); const deleteUrl = url + '/A'; const resp = await axios.delete(deleteUrl, kiwi); @@ -2609,6 +2617,25 @@ function testDocApi() { assert.equal(resp2.data, 'A,B\nSanta,1\nBob,11\nAlice,2\nFelix,22\n'); }); + it('GET /docs/{did}/download/csv with header=colId shows columns id in the header instead of their name', + async function () { + const { docUrl } = await generateDocAndUrl('csvWithColIdAsHeader'); + const AColRef = 2; + const userActions = [ + ['AddRecord', 'Table1', null, {A: 'a1', B: 'b1'}], + ['UpdateRecord', '_grist_Tables_column', AColRef, { untieColIdFromLabel: true }], + ['UpdateRecord', '_grist_Tables_column', AColRef, { + label: 'Column label for A', + colId: 'AColId' + }] + ]; + const resp = await axios.post(`${docUrl}/apply`, userActions, chimpy); + assert.equal(resp.status, 200); + const csvResp = await axios.get(`${docUrl}/download/csv?tableId=Table1&header=colId`, chimpy); + assert.equal(csvResp.status, 200); + assert.equal(csvResp.data, 'AColId,B,C\na1,b1,\n'); + }); + it("GET /docs/{did}/download/csv respects permissions", async function () { // kiwi has no access to TestDoc const resp = await axios.get(`${serverUrl}/api/docs/${docIds.TestDoc}/download/csv?tableId=Table1`, kiwi);