From a019c406abc66209e50d1ad2b3e00262d9f15b93 Mon Sep 17 00:00:00 2001 From: Camille L Date: Wed, 10 May 2023 16:44:50 +0000 Subject: [PATCH 01/10] Translated using Weblate (French) Currently translated at 98.0% (769 of 784 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/fr/ --- static/locales/fr.client.json | 124 +++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 41 deletions(-) diff --git a/static/locales/fr.client.json b/static/locales/fr.client.json index f645f0fc..70adb462 100644 --- a/static/locales/fr.client.json +++ b/static/locales/fr.client.json @@ -133,7 +133,7 @@ "Each Y series is followed by two series, for top and bottom error bars.": "Each Y series is followed by two series, for top and bottom error bars.", "Create separate series for each value of the selected column.": "Créer une série séparée pour chaque valeur de la colonne sélectionnée.", "Pick a column": "Choisir une colonne", - "selected new group data columns": "selected new group data columns", + "selected new group data columns": "nouveau groupe de colonnes sélectionné", "Toggle chart aggregation": "Activer/désactiver l'agrégation des graphiques" }, "CodeEditorPanel": { @@ -263,12 +263,12 @@ "Locale:": "Langue :", "Currency:": "Devise :", "Local currency ({{currency}})": "Devise locale ({{currency}})", - "Engine (experimental {{span}} change at own risk):": "Moteur (expérimental {{span}} changez à vos risques et périls):", + "Engine (experimental {{span}} change at own risk):": "Moteur (expérimental {{span}} changez à vos risques et périls) :", "Save": "Enregistrer", "Save and Reload": "Enregistrer et recharger", "Document ID copied to clipboard": "Identifiant de document copié", "API": "API", - "Ok": "Ok" + "Ok": "OK" }, "DocumentUsage": { "Usage statistics are only available to users with full access to the document data.": "Les statistiques d'utilisation ne sont disponibles qu'aux utilisateurs ayant un accès complet aux données du document.", @@ -316,19 +316,20 @@ "Mixed Behavior": "Comportement mixte", "Clear and make into formula": "Effacer et transformer en formule", "Convert column to data": "Convertir la colonne en données", - "Convert to trigger formula": "Convert to trigger formula", + "Convert to trigger formula": "Convertir en formule", "Clear and reset": "Effacer et réinitialiser", "Enter formula": "Saisir la formule", "COLUMN BEHAVIOR": "NATURE DE COLONNE", "Set formula": "Définir la formule", "Set trigger formula": "Définir une formule d’initialisation", "Make into data column": "Transformer en colonne de données", - "TRIGGER FORMULA": "TRIGGER FORMULA" + "TRIGGER FORMULA": "TRIGGER FORMULA", + "DESCRIPTION": "DESCRIPTION" }, "FieldMenus": { - "Using common settings": "Using common settings", - "Using separate settings": "Using separate settings", - "Use separate settings": "Use separate settings", + "Using common settings": "Utilisation des paramètres communs", + "Using separate settings": "Utilisation de paramètres distincts", + "Use separate settings": "Utiliser des paramètres distincts", "Save as common settings": "Save common settings", "Revert to common settings": "Revert common settings" }, @@ -374,12 +375,14 @@ "Unfreeze all columns": "Libérer toutes les colonnes", "Add to sort": "Ajouter au tri", "Sorted (#{{count}})_one": "Trié (#{{count}})", - "Sorted (#{{count}})_other": "Triés (#{{count}})" + "Sorted (#{{count}})_other": "Triés (#{{count}})", + "Insert column to the right": "Insérer une colonne à droite", + "Insert column to the left": "Insérer une colonne à gauche" }, "GristDoc": { "Import from file": "Importer depuis un fichier", - "Added new linked section to view {{viewName}}": "Added new linked section to view {{viewName}}", - "Saved linked section {{title}} in view {{name}}": "Saved linked section {{title}} in view {{name}}" + "Added new linked section to view {{viewName}}": "Ajout d'une nouvelle section à la page {{viewName}}", + "Saved linked section {{title}} in view {{name}}": "Sauvegarder la section {{title}} dans la page {{name}}" }, "HomeIntro": { "Sign up": "S'inscrire", @@ -393,7 +396,7 @@ "Welcome to Grist, {{name}}!": "Bienvenue sur Grist, {{name}} !", "Get started by inviting your team and creating your first Grist document.": "Pour commencer, inviter votre équipe et créer votre premier document Grist.", "Get started by creating your first Grist document.": "Commencez en créant votre premier document Grist.", - "Get started by exploring templates, or creating your first Grist document.": "Get started by exploring templates, or creating your first Grist document.", + "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", @@ -401,11 +404,13 @@ "Create Empty Document": "Créer un document vide", "Import Document": "Importer un Fichier", "Visit our {{link}} to learn more.": "Consulter le {{link}} pour en savoir plus.", - "{{signUp}} to save your work. ": "{{signUp}} pour enregistrer votre travail. " + "{{signUp}} to save your work. ": "{{signUp}} pour enregistrer votre travail. ", + "Welcome to Grist, {{- name}}!": "Bienvenue sur Grist, {{- name}} !", + "Welcome to {{- orgName}}": "Bienvenue sur {{- orgName}}" }, "HomeLeftPane": { "All Documents": "Tous les documents", - "Examples & Templates": "Exemples & Templates", + "Examples & Templates": "Modèles", "Create Empty Document": "Créer un document vide", "Import Document": "Importer un Fichier", "Create Workspace": "Créer un nouveau dossier", @@ -416,18 +421,19 @@ "Delete {{workspace}} and all included documents?": "Supprimer le dossier {{workspace}} et tous les documents qu'il contient ?", "Workspace will be moved to Trash.": "Le dossier va être mis à la corbeille.", "Manage Users": "Gérer les utilisateurs", - "Access Details": "Access Details" + "Access Details": "Détails d'accès", + "Tutorial": "Tutoriel" }, "Importer": { - "Update existing records": "Update existing records", - "Merge rows that match these fields:": "Fusionner les lignes si ces champs correspondent:", + "Update existing records": "Mettre à jour les enregistrements existants", + "Merge rows that match these fields:": "Fusionner les lignes si ces champs correspondent :", "Select fields to match on": "Sélectionner les champs pour l'appairage" }, "LeftPanelCommon": { "Help Center": "Centre d'aide" }, "MakeCopyMenu": { - "Replacing the original requires editing rights on the original document.": "Replacing the original requires editing rights on the original document.", + "Replacing the original requires editing rights on the original document.": "Le remplacement de l'original nécessite des droits d'édition sur le document d'origine.", "Cancel": "Annuler", "Update Original": "Mettre à jour l'original", "Update": "Mettre à jour", @@ -435,10 +441,10 @@ "Original Has Modifications": "L'original a été modifié", "Overwrite": "Remplacer", "Be careful, the original has changes not in this document. Those changes will be overwritten.": "Attention, l'original a des modifications qui ne sont pas dans ce document. Ces modifications seront écrasées.", - "Original Looks Unrelated": "Original Looks Unrelated", - "It will be overwritten, losing any content not in this document.": "It will be overwritten, losing any content not in this document.", - "Original Looks Identical": "Original Looks Identical", - "However, it appears to be already identical.": "However, it appears to be already identical.", + "Original Looks Unrelated": "L'original ne semble pas relié", + "It will be overwritten, losing any content not in this document.": "Il sera écrasé, perdant tout contenu ne figurant pas dans ce document.", + "Original Looks Identical": "L'original semble identique", + "However, it appears to be already identical.": "Cependant, il semble être déjà identique.", "Sign up": "Inscription", "To save your changes, please sign up, then reload this page.": "Pour enregistrer vos modifications, veuillez vous inscrire, puis recharger cette page.", "No destination workspace": "Aucun dossier destination", @@ -452,7 +458,7 @@ "You do not have write access to the selected workspace": "Vous n’avez pas accès en écriture à ce dossier" }, "NotifyUI": { - "Upgrade Plan": "Upgrade Plan", + "Upgrade Plan": "Améliorer votre abonnement", "Renew": "Renouveler", "Go to your free personal site": "Accéder à votre espace personnel", "Cannot find personal site, sorry!": "Espace personnel introuvable, désolé !", @@ -490,10 +496,10 @@ "Read Only": "Lecture seule" }, "PluginScreen": { - "Import failed: ": "Échec de l'importation: " + "Import failed: ": "Échec de l'importation : " }, "RecordLayout": { - "Updating record layout.": "Updating record layout." + "Updating record layout.": "Mise à jour de la disposition." }, "RecordLayoutEditor": { "Add Field": "Ajouter un champ", @@ -530,7 +536,7 @@ "SOURCE DATA": "DONNÉES SOURCE", "GROUPED BY": "GROUPER PAR", "Edit Data Selection": "Données source", - "Detach": "Detach", + "Detach": "Détacher", "SELECT BY": "SÉLECTIONNER PAR", "Select Widget": "Choisir la vue", "SELECTOR FOR": "SÉLECTEUR", @@ -578,7 +584,7 @@ "Add Column": "Ajouter une colonne", "Update Data": "Mettre à jour les données", "Use choice position": "Use choice position", - "Natural sort": "Natural sort", + "Natural sort": "Tri naturel", "Empty values last": "Valeurs vides en dernier", "Search Columns": "Rechercher" }, @@ -650,8 +656,8 @@ "Compact": "Compact", "Blocks": "Blocs", "Edit Card Layout": "Disposition de la carte", - "Plugin: ": "Plugin: ", - "Section: ": "Section: " + "Plugin: ": "Plugin : ", + "Section: ": "Section : " }, "ViewLayoutMenu": { "Delete record": "Supprimer la ligne", @@ -665,7 +671,9 @@ "Advanced Sort & Filter": "Tri et filtre avancés", "Data selection": "Sélection des données", "Open configuration": "Ouvrir la configuration", - "Delete widget": "Supprimer la vue" + "Delete widget": "Supprimer la vue", + "Collapse widget": "Réduire la vue", + "Add to page": "Ajouter à la page" }, "ViewSectionMenu": { "Update Sort&Filter settings": "Mettre à jour le tri et le filtre", @@ -682,19 +690,23 @@ "Hidden Fields cannot be reordered": "Les champs masqués ne peuvent pas être réordonnés", "Cannot drop items into Hidden Fields": "Impossible de mettre des éléments dans les champs cachés", "Select All": "Sélectionner tout", - "Clear": "Effacer" + "Clear": "Effacer", + "Visible {{label}}": "{{label}} visible", + "Hide {{label}}": "Cacher {{label}}", + "Show {{label}}": "Montrer {{label}}", + "Hidden {{label}}": "{{label}} caché" }, "WelcomeQuestions": { "Welcome to Grist!": "Bienvenue sur Grist !", "Product Development": "Développement de produit", - "Finance & Accounting": "Finance & comptabilité", + "Finance & Accounting": "Finance et comptabilité", "Media Production": "Production de média", "IT & Technology": "Technologie informatique", "Marketing": "Marketing", "Research": "Recherche", "Sales": "Ventes", "Education": "Éducation", - "HR & Management": "RH & Gestion", + "HR & Management": "RH et Gestion", "Other": "Autres", "What brings you to Grist? Please help us serve you better.": "Pourquoi utilisez-vous Grist ? Aidez-nous à l’améliorer.", "Type here": "Écrire ici" @@ -708,7 +720,7 @@ "Cancel": "Annuler" }, "breadcrumbs": { - "You may make edits, but they will create a new copy and will\nnot affect the original document.": "Vous pouvez faire des modifications, mais une nouvelle copie sera créée et ces modifications n’affecteront pas le document original.", + "You may make edits, but they will create a new copy and will\nnot affect the original document.": "Vous pouvez faire des modifications, mais une nouvelle copie\n sera créée et ces modifications n’affecteront pas le document original.", "snapshot": "instantané", "unsaved": "non enregistré", "recovery mode": "mode récupération", @@ -716,7 +728,7 @@ "fiddle": "bac à sable" }, "duplicatePage": { - "Note that this does not copy data, but creates another view of the same data.": "Note that this does not copy data, but creates another view of the same data.", + "Note that this does not copy data, but creates another view of the same data.": "Notez que cette opération ne duplique pas les données, mais crée une autre page avec les mêmes données.", "Duplicate page {{pageName}}": "Dupliquer la page {{pageName}}" }, "errorPages": { @@ -741,12 +753,23 @@ "menus": { "Select fields": "Sélectionner les champs", "* Workspaces are available on team plans. ": "* Les dossiers sont disponibles avec une offre équipe. ", - "Upgrade now": "Mettre à jour maintenant" + "Upgrade now": "Mettre à jour maintenant", + "Numeric": "Numérique", + "Reference List": "Référence multiple", + "Attachment": "Pièce jointe", + "Text": "Texte", + "Date": "Date", + "DateTime": "Date et Heure", + "Choice": "Choix unique", + "Integer": "Entier", + "Choice List": "Choix multiple", + "Toggle": "Booléen", + "Reference": "Référence" }, "modals": { "Save": "Enregistrer", "Cancel": "Annuler", - "Ok": "Ok" + "Ok": "OK" }, "pages": { "Rename": "Renommer", @@ -820,7 +843,7 @@ "Configuring your document": "Configuration de votre document", "Double-click or hit {{enter}} on a cell to edit it. ": "Double-cliquer ou appuyer sur {{enter}} sur une cellule pour la modifier ", "Editing Data": "Modification des données", - "Welcome to Grist!": "Bienvenue sur Grist!", + "Welcome to Grist!": "Bienvenue sur Grist !", "Start with {{equal}} to enter a formula.": "Commencer par {{equal}} pour ajouter une formule.", "Sharing": "Partager", "Reference": "Référence", @@ -868,7 +891,7 @@ }, "ColumnInfo": { "COLUMN DESCRIPTION": "DESCRIPTION", - "COLUMN ID: ": "ID: ", + "COLUMN ID: ": "Identifiant de la colonne : ", "COLUMN LABEL": "LIBELLÉ", "Cancel": "Annuler", "Save": "Enregistrer" @@ -902,7 +925,7 @@ "SHOW COLUMN": "MONTRER LA COLONNE" }, "HyperLinkEditor": { - "[link label] url": "[label du lien] url" + "[link label] url": "[label du lien] URL" }, "GristTooltips": { "Apply conditional formatting to cells in this column when formula conditions are met.": "Appliquez un formatage conditionnel aux cellules de cette colonne lorsque les conditions de la formule sont remplies.", @@ -942,6 +965,25 @@ "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.", - "Unpin to hide the the button while keeping the filter.": "Détachez pour cacher le bouton tout en conservant le filtre." + "Unpin to hide the the button while keeping the filter.": "Détachez pour cacher le bouton tout en conservant le filtre.", + "Anchor Links": "Ancres", + "Custom Widgets": "Vues personnalisées" + }, + "ColumnTitle": { + "Add description": "Ajouter une description", + "Cancel": "Annuler", + "Column ID copied to clipboard": "Identifiant de la column copié", + "COLUMN ID: ": "Identifiant de la column : ", + "Column description": "Description de la colonne", + "Column label": "Libellé de la colonne", + "Provide a column label": "Fournir un libellé pour la colonne", + "Save": "Sauvegarder" + }, + "DescriptionConfig": { + "DESCRIPTION": "DESCRIPTION" + }, + "PagePanels": { + "Open Creator Panel": "Ouvrir le menu latéral", + "Close Creator Panel": "Fermer le menu latéral" } } From c16204f8ad8232e20bb887f0900916f52bdbf1c7 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Fri, 12 May 2023 15:08:28 +0200 Subject: [PATCH 02/10] feature widget description (#483) Add description to widget title popup and right panel --- app/client/components/DetailView.js | 6 +- app/client/components/GridView.js | 4 +- app/client/models/entities/ViewSectionRec.ts | 5 + app/client/ui/ColumnTitle.ts | 75 +------- app/client/ui/DescriptionConfig.ts | 21 +- app/client/ui/RenamePopupStyles.ts | 75 ++++++++ app/client/ui/RightPanel.ts | 13 +- app/client/ui/WidgetTitle.ts | 190 ++++++++++++------- app/client/ui/tooltips.ts | 18 +- app/common/schema.ts | 4 +- app/server/lib/initialDocSql.ts | 12 +- sandbox/grist/migrations.py | 8 + sandbox/grist/schema.py | 3 +- static/locales/en.client.json | 3 +- test/nbrowser/DescriptionWidget.ts | 96 ++++++++++ 15 files changed, 359 insertions(+), 174 deletions(-) create mode 100644 app/client/ui/RenamePopupStyles.ts create mode 100644 test/nbrowser/DescriptionWidget.ts diff --git a/app/client/components/DetailView.js b/app/client/components/DetailView.js index a869c6bb..2d69c816 100644 --- a/app/client/components/DetailView.js +++ b/app/client/components/DetailView.js @@ -16,7 +16,7 @@ const RecordLayout = require('./RecordLayout'); const commands = require('./commands'); const {RowContextMenu} = require('../ui/RowContextMenu'); const {parsePasteForView} = require("./BaseView2"); -const {columnInfoTooltip} = require("../ui/tooltips"); +const {descriptionInfoTooltip} = require("../ui/tooltips"); /** @@ -247,7 +247,7 @@ DetailView.prototype.buildFieldDom = function(field, row) { kd.cssClass(function() { return 'detail_theme_field_' + self.viewSection.themeDef(); }), dom('div.g_record_detail_label_container', dom('div.g_record_detail_label', kd.text(field.label)), - kd.scope(field.description, desc => desc ? columnInfoTooltip(kd.text(field.description)) : null) + kd.scope(field.description, desc => desc ? descriptionInfoTooltip(kd.text(field.description), "colmun") : null) ), dom('div.g_record_detail_value'), ); @@ -280,7 +280,7 @@ DetailView.prototype.buildFieldDom = function(field, row) { kd.cssClass(function() { return 'detail_theme_field_' + self.viewSection.themeDef(); }), dom('div.g_record_detail_label_container', dom('div.g_record_detail_label', kd.text(field.displayLabel)), - kd.scope(field.description, desc => desc ? columnInfoTooltip(kd.text(field.description)) : null) + kd.scope(field.description, desc => desc ? descriptionInfoTooltip(kd.text(field.description), "column") : null) ), dom('div.g_record_detail_value', kd.toggleClass('scissors', isCopyActive), diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index b35089cf..ecbb3f5b 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -44,7 +44,7 @@ const {testId, isNarrowScreen} = require('app/client/ui2018/cssVars'); const {contextMenu} = require('app/client/ui/contextMenu'); const {mouseDragMatchElem} = require('app/client/ui/mouseDrag'); const {menuToggle} = require('app/client/ui/MenuToggle'); -const {columnInfoTooltip, showTooltip} = require('app/client/ui/tooltips'); +const {descriptionInfoTooltip, showTooltip} = require('app/client/ui/tooltips'); const {parsePasteForView} = require("./BaseView2"); const {NEW_FILTER_JSON} = require('app/client/models/ColumnFilter'); const {CombinedStyle} = require("app/client/models/Styles"); @@ -1087,7 +1087,7 @@ GridView.prototype.buildDom = function() { if (btn) { btn.click(); } }), dom('div.g-column-label', - kd.scope(field.description, desc => desc ? columnInfoTooltip(kd.text(field.description)) : null), + kd.scope(field.description, desc => desc ? descriptionInfoTooltip(kd.text(field.description), "column") : null), dom.on('mousedown', ev => isEditingLabel() ? ev.stopPropagation() : true), // We are using editableLabel here, but we don't use it for editing. kf.editableLabel(self.isPreview ? field.label : field.displayLabel, ko.observable(false)), diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index 168d0c0d..4ad353df 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -53,6 +53,8 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO // Default widget title (the one that is used in titleDef). defaultWidgetTitle: ko.PureComputed; + description: modelUtil.KoSaveableObservable; + // true if this record is its table's rawViewSection, i.e. a 'raw data view' // in which case the UI prevents various things like hiding columns or changing the widget type. isRaw: ko.Computed; @@ -363,6 +365,9 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): // Widget title. this.titleDef = modelUtil.fieldWithDefault(this.title, this.defaultWidgetTitle); + // Widget description + this.description = modelUtil.fieldWithDefault(this.description, this.description()); + // true if this record is its table's rawViewSection, i.e. a 'raw data view' // in which case the UI prevents various things like hiding columns or changing the widget type. this.isRaw = this.autoDispose(ko.pureComputed(() => this.table().rawViewSectionRef() === this.getRowId())); diff --git a/app/client/ui/ColumnTitle.ts b/app/client/ui/ColumnTitle.ts index 8626b6d7..7c9c33ab 100644 --- a/app/client/ui/ColumnTitle.ts +++ b/app/client/ui/ColumnTitle.ts @@ -6,17 +6,16 @@ import {makeT} from 'app/client/lib/localization'; import {setTestState} from 'app/client/lib/testState'; import {ViewFieldRec} from 'app/client/models/DocModel'; import {autoGrow} from 'app/client/ui/forms'; -import {textarea} from 'app/client/ui/inputs'; import {showTransientTooltip} from 'app/client/ui/tooltips'; import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons'; import {theme, vars} from 'app/client/ui2018/cssVars'; -import {cssTextInput} from 'app/client/ui2018/editableLabel'; import {icon} from 'app/client/ui2018/icons'; import {menuCssClass} from 'app/client/ui2018/menus'; -import {Computed, dom, IInputOptions, input, makeTestId, Observable, styled} from 'grainjs'; +import {Computed, dom, makeTestId, Observable, styled} from 'grainjs'; import * as ko from 'knockout'; import {IOpenController, PopupControl, setPopupToCreateDom} from 'popweasel'; +import { cssInput, cssLabel, cssRenamePopup, cssTextArea } from 'app/client/ui/RenamePopupStyles'; const testId = makeTestId('test-column-title-'); @@ -281,16 +280,6 @@ const cssAddDescription = styled('div', ` } `); -const cssRenamePopup = styled('div', ` - display: flex; - flex-direction: column; - min-width: 280px; - padding: 16px; - background-color: ${theme.popupBg}; - border-radius: 2px; - outline: none; -`); - const cssColLabelBlock = styled('div', ` display: flex; flex-direction: column; @@ -298,17 +287,6 @@ const cssColLabelBlock = styled('div', ` min-width: 80px; `); -const cssLabel = styled('label', ` - color: ${theme.text}; - font-size: ${vars.xsmallFontSize}; - font-weight: ${vars.bigControlTextWeight}; - text-transform: uppercase; - margin: 0 0 8px 0; - &:not(:first-child) { - margin-top: 16px; - } -`); - const cssColId = styled('div', ` font-size: ${vars.xsmallFontSize}; font-weight: ${vars.bigControlTextWeight}; @@ -321,29 +299,6 @@ const cssColId = styled('div', ` align-self: start; `); -const cssTextArea = styled(textarea, ` - color: ${theme.inputFg}; - background-color: ${theme.mainPanelBg}; - border: 1px solid ${theme.inputBorder}; - width: 100%; - padding: 3px 7px; - outline: none; - max-width: 100%; - min-width: calc(280px - 16px*2); - max-height: 500px; - min-height: calc(3em * 1.5); - resize: none; - border-radius: 3px; - &::placeholder { - color: ${theme.inputPlaceholderFg}; - } - - &[readonly] { - background-color: ${theme.inputDisabledBg}; - color: ${theme.inputDisabledFg}; - } -`); - const cssButtons = styled('div', ` display: flex; margin-top: 16px; @@ -352,29 +307,3 @@ const cssButtons = styled('div', ` min-width: calc(50 / 13 * 1em); /* Min 50px for 13px font size, to make Save and Close buttons equal width */ } `); - -const cssInputWithIcon = styled('div', ` - position: relative; - display: flex; - flex-direction: column; -`); - -const cssInput = styled(( - obs: Observable, - opts: IInputOptions, - ...args) => input(obs, opts, cssTextInput.cls(''), ...args), ` - text-overflow: ellipsis; - color: ${theme.inputFg}; - background-color: transparent; - &:disabled { - color: ${theme.inputDisabledFg}; - background-color: ${theme.inputDisabledBg}; - pointer-events: none; - } - &::placeholder { - color: ${theme.inputPlaceholderFg}; - } - .${cssInputWithIcon.className} > &:disabled { - padding-right: 28px; - } -`); diff --git a/app/client/ui/DescriptionConfig.ts b/app/client/ui/DescriptionConfig.ts index ae67fa1b..65f03042 100644 --- a/app/client/ui/DescriptionConfig.ts +++ b/app/client/ui/DescriptionConfig.ts @@ -1,18 +1,21 @@ import {CursorPos} from 'app/client/components/Cursor'; import {makeT} from 'app/client/lib/localization'; -import {ColumnRec} from 'app/client/models/DocModel'; +import { KoSaveableObservable } from 'app/client/models/modelUtil'; import {autoGrow} from 'app/client/ui/forms'; import {textarea} from 'app/client/ui/inputs'; import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles'; import {testId, theme} from 'app/client/ui2018/cssVars'; import {dom, fromKo, MultiHolder, styled} from 'grainjs'; -const t = makeT('FieldConfig'); +const t = makeT('DescriptionConfig'); export function buildDescriptionConfig( owner: MultiHolder, - origColumn: ColumnRec, - cursor: ko.Computed, + description: KoSaveableObservable, + options: { + cursor: ko.Computed, + testPrefix: string, + }, ) { // We will listen to cursor position and force a blur event on @@ -22,7 +25,7 @@ export function buildDescriptionConfig( // update a different column. let editor: HTMLTextAreaElement | undefined; owner.autoDispose( - cursor.subscribe(() => { + options.cursor.subscribe(() => { editor?.blur(); }) ); @@ -30,14 +33,14 @@ export function buildDescriptionConfig( return [ cssLabel(t("DESCRIPTION")), cssRow( - editor = cssTextArea(fromKo(origColumn.description), + editor = cssTextArea(fromKo(description), { onInput: false }, { rows: '3' }, dom.on('blur', async (e, elem) => { - await origColumn.description.setAndSave(elem.value.trim()); + await description.saveOnly(elem.value); }), - testId('column-description'), - autoGrow(fromKo(origColumn.description)) + testId(`${options.testPrefix}-description`), + autoGrow(fromKo(description)) ) ), ]; diff --git a/app/client/ui/RenamePopupStyles.ts b/app/client/ui/RenamePopupStyles.ts new file mode 100644 index 00000000..e658b786 --- /dev/null +++ b/app/client/ui/RenamePopupStyles.ts @@ -0,0 +1,75 @@ +import { theme, vars } from 'app/client/ui2018/cssVars'; +import {textarea} from 'app/client/ui/inputs'; +import {cssTextInput} from 'app/client/ui2018/editableLabel'; +import {IInputOptions, input, Observable, styled} from 'grainjs'; + + +export const cssRenamePopup = styled('div', ` + display: flex; + flex-direction: column; + min-width: 280px; + padding: 16px; + background-color: ${theme.popupBg}; + border-radius: 2px; + outline: none; +`); + +export const cssLabel = styled('label', ` + color: ${theme.text}; + font-size: ${vars.xsmallFontSize}; + font-weight: ${vars.bigControlTextWeight}; + text-transform: uppercase; + margin: 0 0 8px 0; + &:not(:first-child) { + margin-top: 16px; + } +`); + +const cssInputWithIcon = styled('div', ` + position: relative; + display: flex; + flex-direction: column; +`); + +export const cssInput = styled(( + obs: Observable, + opts: IInputOptions, + ...args) => input(obs, opts, cssTextInput.cls(''), ...args), ` + text-overflow: ellipsis; + color: ${theme.inputFg}; + background-color: transparent; + &:disabled { + color: ${theme.inputDisabledFg}; + background-color: ${theme.inputDisabledBg}; + pointer-events: none; + } + &::placeholder { + color: ${theme.inputPlaceholderFg}; + } + .${cssInputWithIcon.className} > &:disabled { + padding-right: 28px; + } +`); + +export const cssTextArea = styled(textarea, ` + color: ${theme.inputFg}; + background-color: ${theme.mainPanelBg}; + border: 1px solid ${theme.inputBorder}; + width: 100%; + padding: 3px 6px; + outline: none; + max-width: 100%; + min-width: calc(280px - 16px*2); + max-height: 500px; + min-height: calc(3em * 1.5); + resize: none; + border-radius: 3px; + &::placeholder { + color: ${theme.inputPlaceholderFg}; + } + + &[readonly] { + background-color: ${theme.inputDisabledBg}; + color: ${theme.inputDisabledFg}; + } +`); diff --git a/app/client/ui/RightPanel.ts b/app/client/ui/RightPanel.ts index 6c60b0e5..3524ea45 100644 --- a/app/client/ui/RightPanel.ts +++ b/app/client/ui/RightPanel.ts @@ -251,7 +251,7 @@ export class RightPanel extends Disposable { dom.create(buildNameConfig, origColumn, cursor, isMultiSelect), ), cssSection( - dom.create(buildDescriptionConfig, origColumn, cursor), + dom.create(buildDescriptionConfig, origColumn.description, { cursor, "testPrefix": "column" }), ), cssSeparator(), cssSection( @@ -361,6 +361,13 @@ export class RightPanel extends Disposable { const hasColumnMapping = use(activeSection.columnsToMap); return Boolean(isCustom && hasColumnMapping); }); + + // build cursor position observable + const cursor = owner.autoDispose(ko.computed(() => { + const vsi = this._gristDoc.viewModel.activeSection?.().viewInstance(); + return vsi?.cursor.currentPosition() ?? {}; + })); + return dom.maybe(viewConfigTab, (vct) => [ this._disableIfReadonly(), cssLabel(dom.text(use => use(activeSection.isRaw) ? t("DATA TABLE NAME") : t("WIDGET TITLE")), @@ -377,6 +384,10 @@ export class RightPanel extends Disposable { testId('right-widget-title') )), + cssSection( + dom.create(buildDescriptionConfig, activeSection.description, { cursor, "testPrefix": "right-widget" }), + ), + dom.maybe( (use) => !use(activeSection.isRaw), () => cssRow( diff --git a/app/client/ui/WidgetTitle.ts b/app/client/ui/WidgetTitle.ts index a47425ab..fb13bf3f 100644 --- a/app/client/ui/WidgetTitle.ts +++ b/app/client/ui/WidgetTitle.ts @@ -1,13 +1,16 @@ +import * as commands from 'app/client/components/commands'; import {makeT} from 'app/client/lib/localization'; -import {FocusLayer} from 'app/client/lib/FocusLayer'; +import { FocusLayer } from 'app/client/lib/FocusLayer'; import {ViewSectionRec} from 'app/client/models/entities/ViewSectionRec'; import {basicButton, cssButton, primaryButton} from 'app/client/ui2018/buttons'; -import {theme, vars} from 'app/client/ui2018/cssVars'; -import {cssTextInput} from 'app/client/ui2018/editableLabel'; +import { theme } from 'app/client/ui2018/cssVars'; import {menuCssClass} from 'app/client/ui2018/menus'; import {ModalControl} from 'app/client/ui2018/modals'; -import {Computed, dom, DomElementArg, IInputOptions, input, makeTestId, Observable, styled} from 'grainjs'; +import { Computed, dom, DomElementArg, makeTestId, Observable, styled } from 'grainjs'; import {IOpenController, setPopupToCreateDom} from 'popweasel'; +import { descriptionInfoTooltip } from './tooltips'; +import { autoGrow } from './forms'; +import { cssInput, cssLabel, cssRenamePopup, cssTextArea } from 'app/client/ui/RenamePopupStyles'; const testId = makeTestId('test-widget-title-'); const t = makeT('WidgetTitle'); @@ -19,17 +22,20 @@ interface WidgetTitleOptions { export function buildWidgetTitle(vs: ViewSectionRec, options: WidgetTitleOptions, ...args: DomElementArg[]) { const title = Computed.create(null, use => use(vs.titleDef)); - return buildRenameWidget(vs, title, options, dom.autoDispose(title), ...args); + const description = Computed.create(null, use => use(vs.description)); + return buildRenameWidget(vs, title, description, options, dom.autoDispose(title), ...args); } export function buildTableName(vs: ViewSectionRec, ...args: DomElementArg[]) { const title = Computed.create(null, use => use(use(vs.table).tableNameDef)); - return buildRenameWidget(vs, title, { widgetNameHidden: true }, dom.autoDispose(title), ...args); + const description = Computed.create(null, use => use(vs.description)); + return buildRenameWidget(vs, title, description, { widgetNameHidden: true }, dom.autoDispose(title), ...args); } export function buildRenameWidget( vs: ViewSectionRec, title: Observable, + description: Observable, options: WidgetTitleOptions, ...args: DomElementArg[]) { return cssTitleContainer( @@ -48,6 +54,9 @@ export function buildRenameWidget( }, dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }), ), + dom.maybe(description, () => [ + descriptionInfoTooltip(description.get(), "widget") + ]), ...args ); } @@ -69,11 +78,19 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio // - when widget title is set, shows just a text to override it. const inputWidgetPlaceholder = !vs.title.peek() ? t("Override widget title") : vs.defaultWidgetTitle.peek(); + // User input for widget description + const inputWidgetDesc = Observable.create(ctrl, vs.description.peek() ?? ''); + const disableSave = Computed.create(ctrl, (use) => { const newTableName = use(inputTableName)?.trim() ?? ''; const newWidgetTitle = use(inputWidgetTitle)?.trim() ?? ''; + const newWidgetDesc = use(inputWidgetDesc)?.trim() ?? ''; // Can't save when table name is empty or there wasn't any change. - return !newTableName || (newTableName === tableName && newWidgetTitle === use(vs.title)); + return !newTableName || ( + newTableName === tableName + && newWidgetTitle === use(vs.title) + && newWidgetDesc === use(vs.description) + ); }); const modalCtl = ModalControl.create(ctrl, () => ctrl.close()); @@ -99,10 +116,20 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio await vs.title.saveOnly(newTitle); } }; - const doSave = modalCtl.doWork(() => Promise.all([ + + const saveWidgetDesc = async () => { + const newWidgetDesc = inputWidgetDesc.get().trim() ?? ''; + // If value was changed. + if (newWidgetDesc !== vs.description.peek()) { + await vs.description.saveOnly(newWidgetDesc); + } + }; + + const save = () => Promise.all([ saveTableName(), - saveWidgetTitle() - ]), {close: true}); + saveWidgetTitle(), + saveWidgetDesc() + ]); function initialFocus() { const isRawView = !widgetInput; @@ -122,18 +149,72 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio } } - // Build actual dom that looks like: - // DATA TABLE NAME - // [input] - // WIDGET TITLE - // [input] - // [Save] [Cancel] + // When the popup is closing we will save everything, unless the user has pressed the cancel button. + let cancelled = false; + + // Function to close the popup with saving. + const close = () => ctrl.close(); + + // Function to close the popup without saving. + const cancel = () => { cancelled = true; close(); }; + + // Function that is called when popup is closed. + const onClose = () => { + if (!cancelled) { + save().catch(reportError); + } + }; + + // User interface for the popup. + const myCommands = { + // Escape key: just close the popup. + cancel, + // Enter key: save and close the popup, unless the description input is focused. + // There is also a variant for Ctrl+Enter which will always save. + accept: () => { + // Enters are ignored in the description input (unless ctrl is pressed) + if (document.activeElement === descInput) { return true; } + close(); + }, + // ArrowUp + cursorUp: () => { + // moves focus to the widget title input if it is already at the top of widget description + if (document.activeElement === descInput && descInput?.selectionStart === 0) { + widgetInput?.focus(); + widgetInput?.select(); + } else if (document.activeElement === widgetInput) { + tableInput?.focus(); + tableInput?.select(); + } else { + return true; + } + }, + // ArrowDown + cursorDown: () => { + if (document.activeElement === tableInput) { + widgetInput?.focus(); + widgetInput?.select(); + } else if (document.activeElement === widgetInput) { + descInput?.focus(); + descInput?.select(); + } else { + return true; + } + } + }; + + // Create this group and attach it to the popup and all inputs. + const commandGroup = commands.createGroup({ ...myCommands }, ctrl, true); + let tableInput: HTMLInputElement|undefined; let widgetInput: HTMLInputElement|undefined; + let descInput: HTMLTextAreaElement | undefined; return cssRenamePopup( // Create a FocusLayer to keep focus in this popup while it's active, and prevent keyboard // shortcuts from being seen by the view underneath. - elem => { FocusLayer.create(ctrl, {defaultFocusElem: elem, pauseMousetrap: true}); }, + elem => { FocusLayer.create(ctrl, { defaultFocusElem: elem, pauseMousetrap: false }); }, + dom.onDispose(onClose), + dom.autoDispose(commandGroup), testId('popup'), dom.cls(menuCssClass), dom.maybe(!options.tableNameHidden, () => [ @@ -144,30 +225,41 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio inputTableName, updateOnKey, {disabled: isSummary, placeholder: t("Provide a table name")}, - testId('table-name-input') + testId('table-name-input'), + commandGroup.attach(), ), ]), dom.maybe(!options.widgetNameHidden, () => [ cssLabel(t("WIDGET TITLE")), widgetInput = cssInput(inputWidgetTitle, updateOnKey, {placeholder: inputWidgetPlaceholder}, - testId('section-name-input') + testId('section-name-input'), + commandGroup.attach(), ), ]), + cssLabel(t("WIDGET DESCRIPTION")), + descInput = cssTextArea(inputWidgetDesc, updateOnKey, + testId('section-description-input'), + commandGroup.attach(), + autoGrow(inputWidgetDesc), + ), cssButtons( primaryButton(t("Save"), - dom.on('click', doSave), + dom.on('click', close), dom.boolAttr('disabled', use => use(disableSave) || use(modalCtl.workInProgress)), testId('save'), ), basicButton(t("Cancel"), testId('cancel'), - dom.on('click', () => modalCtl.close()) + dom.on('click', cancel) ), ), dom.onKeyDown({ - Escape: () => modalCtl.close(), - // On enter save or cancel - depending on the change. - Enter: () => disableSave.get() ? modalCtl.close() : doSave(), + Enter$: e => { + if (e.ctrlKey || e.metaKey) { + close(); + return false; + } + } }), elem => { setTimeout(initialFocus, 0); }, ); @@ -180,6 +272,10 @@ const cssTitleContainer = styled('div', ` flex: 1 1 0px; min-width: 0px; display: flex; + .info_toggle_icon { + width: 13px; + height: 13px; + } `); const cssTitle = styled('div', ` @@ -199,26 +295,6 @@ const cssTitle = styled('div', ` } `); -const cssRenamePopup = styled('div', ` - display: flex; - flex-direction: column; - min-width: 280px; - padding: 16px; - background-color: ${theme.popupBg}; - border-radius: 2px; - outline: none; -`); - -const cssLabel = styled('label', ` - color: ${theme.text}; - font-size: ${vars.xsmallFontSize}; - font-weight: ${vars.bigControlTextWeight}; - margin: 0 0 8px 0; - &:not(:first-child) { - margin-top: 16px; - } -`); - const cssButtons = styled('div', ` display: flex; margin-top: 16px; @@ -226,29 +302,3 @@ const cssButtons = styled('div', ` margin-left: 8px; } `); - -const cssInputWithIcon = styled('div', ` - position: relative; - display: flex; - flex-direction: column; -`); - -const cssInput = styled(( - obs: Observable, - opts: IInputOptions, - ...args) => input(obs, opts, cssTextInput.cls(''), ...args), ` - text-overflow: ellipsis; - color: ${theme.inputFg}; - background-color: transparent; - &:disabled { - color: ${theme.inputDisabledFg}; - background-color: ${theme.inputDisabledBg}; - pointer-events: none; - } - &::placeholder { - color: ${theme.inputPlaceholderFg}; - } - .${cssInputWithIcon.className} > &:disabled { - padding-right: 28px; - } -`); diff --git a/app/client/ui/tooltips.ts b/app/client/ui/tooltips.ts index 1dddda4a..2b1ff90d 100644 --- a/app/client/ui/tooltips.ts +++ b/app/client/ui/tooltips.ts @@ -347,15 +347,18 @@ export function withInfoTooltip( } /** - * Renders an column info icon that shows a tooltip with the specified `content` on click. + * Renders an description info icon that shows a tooltip with the specified `content` on click. */ - export function columnInfoTooltip(content: DomContents, menuOptions?: IMenuOptions, ...domArgs: DomElementArg[]) { - return cssColumnInfoTooltipButton( +export function descriptionInfoTooltip( + content: DomContents, + testPrefix: string, + ...domArgs: DomElementArg[]) { + return cssDescriptionInfoTooltipButton( icon('Info', dom.cls("info_toggle_icon")), - testId('column-info-tooltip'), + testId(`${testPrefix}-info-tooltip`), dom.on('mousedown', (e) => e.stopPropagation()), dom.on('click', (e) => e.stopPropagation()), - hoverTooltip(() => cssColumnInfoTooltip(content, testId('column-info-tooltip-popup')), { + hoverTooltip(() => cssDescriptionInfoTooltip(content, testId(`${testPrefix}-info-tooltip-popup`)), { closeDelay: 200, key: 'columnDescription', openOnClick: true, @@ -365,7 +368,8 @@ export function withInfoTooltip( ); } -const cssColumnInfoTooltip = styled('div', ` + +const cssDescriptionInfoTooltip = styled('div', ` white-space: pre-wrap; text-align: left; text-overflow: ellipsis; @@ -373,7 +377,7 @@ const cssColumnInfoTooltip = styled('div', ` max-width: min(500px, calc(100vw - 80px)); /* can't use 100%, 500px and 80px are picked by hand */ `); -const cssColumnInfoTooltipButton = styled('div', ` +const cssDescriptionInfoTooltipButton = styled('div', ` cursor: pointer; --icon-color: ${theme.infoButtonFg}; border-radius: 50%; diff --git a/app/common/schema.ts b/app/common/schema.ts index 58dd93f4..1c7fd3a6 100644 --- a/app/common/schema.ts +++ b/app/common/schema.ts @@ -4,7 +4,7 @@ import { GristObjCode } from "app/plugin/GristData"; // tslint:disable:object-literal-key-quotes -export const SCHEMA_VERSION = 37; +export const SCHEMA_VERSION = 38; export const schema = { @@ -104,6 +104,7 @@ export const schema = { parentId : "Ref:_grist_Views", parentKey : "Text", title : "Text", + description : "Text", defaultWidth : "Int", borderWidth : "Int", theme : "Text", @@ -311,6 +312,7 @@ export interface SchemaTypes { parentId: number; parentKey: string; title: string; + description: string; defaultWidth: number; borderWidth: number; theme: string; diff --git a/app/server/lib/initialDocSql.ts b/app/server/lib/initialDocSql.ts index f06c7bba..b7271835 100644 --- a/app/server/lib/initialDocSql.ts +++ b/app/server/lib/initialDocSql.ts @@ -6,7 +6,7 @@ export const GRIST_DOC_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',37,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',38,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_Imports" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "origFileName" TEXT DEFAULT '', "parseFormula" TEXT DEFAULT '', "delimiter" TEXT DEFAULT '', "doublequote" BOOLEAN DEFAULT 0, "escapechar" TEXT DEFAULT '', "quotechar" TEXT DEFAULT '', "skipinitialspace" BOOLEAN DEFAULT 0, "encoding" TEXT DEFAULT '', "hasHeaders" BOOLEAN DEFAULT 0); @@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS "_grist_TabItems" (id INTEGER PRIMARY KEY, "tableRef" CREATE TABLE IF NOT EXISTS "_grist_TabBar" (id INTEGER PRIMARY KEY, "viewRef" INTEGER DEFAULT 0, "tabPos" NUMERIC DEFAULT 1e999); CREATE TABLE IF NOT EXISTS "_grist_Pages" (id INTEGER PRIMARY KEY, "viewRef" INTEGER DEFAULT 0, "indentation" INTEGER DEFAULT 0, "pagePos" NUMERIC DEFAULT 1e999); CREATE TABLE IF NOT EXISTS "_grist_Views" (id INTEGER PRIMARY KEY, "name" TEXT DEFAULT '', "type" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT ''); -CREATE TABLE IF NOT EXISTS "_grist_Views_section" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "parentId" INTEGER DEFAULT 0, "parentKey" TEXT DEFAULT '', "title" TEXT DEFAULT '', "defaultWidth" INTEGER DEFAULT 0, "borderWidth" INTEGER DEFAULT 0, "theme" TEXT DEFAULT '', "options" TEXT DEFAULT '', "chartType" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT '', "filterSpec" TEXT DEFAULT '', "sortColRefs" TEXT DEFAULT '', "linkSrcSectionRef" INTEGER DEFAULT 0, "linkSrcColRef" INTEGER DEFAULT 0, "linkTargetColRef" INTEGER DEFAULT 0, "embedId" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL); +CREATE TABLE IF NOT EXISTS "_grist_Views_section" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "parentId" INTEGER DEFAULT 0, "parentKey" TEXT DEFAULT '', "title" TEXT DEFAULT '', "description" TEXT DEFAULT '', "defaultWidth" INTEGER DEFAULT 0, "borderWidth" INTEGER DEFAULT 0, "theme" TEXT DEFAULT '', "options" TEXT DEFAULT '', "chartType" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT '', "filterSpec" TEXT DEFAULT '', "sortColRefs" TEXT DEFAULT '', "linkSrcSectionRef" INTEGER DEFAULT 0, "linkSrcColRef" INTEGER DEFAULT 0, "linkTargetColRef" INTEGER DEFAULT 0, "embedId" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_Views_section_field" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colRef" INTEGER DEFAULT 0, "width" INTEGER DEFAULT 0, "widgetOptions" TEXT DEFAULT '', "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "filter" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_Validations" (id INTEGER PRIMARY KEY, "formula" TEXT DEFAULT '', "name" TEXT DEFAULT '', "tableRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_REPL_Hist" (id INTEGER PRIMARY KEY, "code" TEXT DEFAULT '', "outputText" TEXT DEFAULT '', "errorText" TEXT DEFAULT ''); @@ -43,7 +43,7 @@ export const GRIST_DOC_WITH_TABLE1_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',37,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',38,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0); INSERT INTO _grist_Tables VALUES(1,'Table1',1,0,0,2); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); @@ -62,9 +62,9 @@ CREATE TABLE IF NOT EXISTS "_grist_Pages" (id INTEGER PRIMARY KEY, "viewRef" INT INSERT INTO _grist_Pages VALUES(1,1,0,1); CREATE TABLE IF NOT EXISTS "_grist_Views" (id INTEGER PRIMARY KEY, "name" TEXT DEFAULT '', "type" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT ''); INSERT INTO _grist_Views VALUES(1,'Table1','raw_data',''); -CREATE TABLE IF NOT EXISTS "_grist_Views_section" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "parentId" INTEGER DEFAULT 0, "parentKey" TEXT DEFAULT '', "title" TEXT DEFAULT '', "defaultWidth" INTEGER DEFAULT 0, "borderWidth" INTEGER DEFAULT 0, "theme" TEXT DEFAULT '', "options" TEXT DEFAULT '', "chartType" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT '', "filterSpec" TEXT DEFAULT '', "sortColRefs" TEXT DEFAULT '', "linkSrcSectionRef" INTEGER DEFAULT 0, "linkSrcColRef" INTEGER DEFAULT 0, "linkTargetColRef" INTEGER DEFAULT 0, "embedId" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL); -INSERT INTO _grist_Views_section VALUES(1,1,1,'record','',100,1,'','','','','','[]',0,0,0,'',NULL); -INSERT INTO _grist_Views_section VALUES(2,1,0,'record','',100,1,'','','','','','',0,0,0,'',NULL); +CREATE TABLE IF NOT EXISTS "_grist_Views_section" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "parentId" INTEGER DEFAULT 0, "parentKey" TEXT DEFAULT '', "title" TEXT DEFAULT '', "description" TEXT DEFAULT '', "defaultWidth" INTEGER DEFAULT 0, "borderWidth" INTEGER DEFAULT 0, "theme" TEXT DEFAULT '', "options" TEXT DEFAULT '', "chartType" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT '', "filterSpec" TEXT DEFAULT '', "sortColRefs" TEXT DEFAULT '', "linkSrcSectionRef" INTEGER DEFAULT 0, "linkSrcColRef" INTEGER DEFAULT 0, "linkTargetColRef" INTEGER DEFAULT 0, "embedId" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL); +INSERT INTO _grist_Views_section VALUES(1,1,1,'record','','',100,1,'','','','','','[]',0,0,0,'',NULL); +INSERT INTO _grist_Views_section VALUES(2,1,0,'record','','',100,1,'','','','','','',0,0,0,'',NULL); CREATE TABLE IF NOT EXISTS "_grist_Views_section_field" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colRef" INTEGER DEFAULT 0, "width" INTEGER DEFAULT 0, "widgetOptions" TEXT DEFAULT '', "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "filter" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL); INSERT INTO _grist_Views_section_field VALUES(1,1,1,2,0,'',0,0,'',NULL); INSERT INTO _grist_Views_section_field VALUES(2,1,2,3,0,'',0,0,'',NULL); diff --git a/sandbox/grist/migrations.py b/sandbox/grist/migrations.py index 24756589..80020f32 100644 --- a/sandbox/grist/migrations.py +++ b/sandbox/grist/migrations.py @@ -1204,3 +1204,11 @@ def migration37(tdset): Add fileExt column to _grist_Attachments. """ return tdset.apply_doc_actions([add_column('_grist_Attachments', 'fileExt', 'Text')]) + +@migration(schema_version=38) +def migration38(tdset): + """ + Add description to widget + """ + return tdset.apply_doc_actions([add_column('_grist_Views_section', 'description', 'Text')]) + \ No newline at end of file diff --git a/sandbox/grist/schema.py b/sandbox/grist/schema.py index 0c516965..650e5090 100644 --- a/sandbox/grist/schema.py +++ b/sandbox/grist/schema.py @@ -15,7 +15,7 @@ import six import actions -SCHEMA_VERSION = 37 +SCHEMA_VERSION = 38 def make_column(col_id, col_type, formula='', isFormula=False): return { @@ -181,6 +181,7 @@ def schema_create_actions(): # TODO: rename this (e.g. to "sectionType"). make_column("parentKey", "Text"), make_column("title", "Text"), + make_column("description", "Text"), make_column("defaultWidth", "Int", formula="100"), make_column("borderWidth", "Int", formula="1"), make_column("theme", "Text"), diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 72a5a6a2..4f69f902 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -724,7 +724,8 @@ "Override widget title": "Override widget title", "Provide a table name": "Provide a table name", "Save": "Save", - "WIDGET TITLE": "WIDGET TITLE" + "WIDGET TITLE": "WIDGET TITLE", + "WIDGET DESCRIPTION": "WIDGET DESCRIPTION" }, "breadcrumbs": { "You may make edits, but they will create a new copy and will\nnot affect the original document.": "You may make edits, but they will create a new copy and will\nnot affect the original document.", diff --git a/test/nbrowser/DescriptionWidget.ts b/test/nbrowser/DescriptionWidget.ts new file mode 100644 index 00000000..09fc11bb --- /dev/null +++ b/test/nbrowser/DescriptionWidget.ts @@ -0,0 +1,96 @@ +import { assert, driver, Key } from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; +import { setupTestSuite } from 'test/nbrowser/testUtils'; + + +describe('DescriptionWidget', function() { + this.timeout(20000); + const cleanup = setupTestSuite(); + + it('should support basic edition in right panel', async () => { + const mainSession = await gu.session().teamSite.login(); + await mainSession.tempDoc(cleanup, "CardView.grist", { load: true }); + + const newWidgetDesc = "This is the widget description\nIt is in two lines"; + await driver.find('.test-right-opener').click(); + // Sleep 100ms to let open the right panel and make the description input clickable + await driver.sleep(100); + const rightPanelDescriptionInput = await driver.find('.test-right-panel .test-right-widget-description'); + await rightPanelDescriptionInput.click(); + await rightPanelDescriptionInput.sendKeys(newWidgetDesc); + // Click on other input to unselect descriptionInput + await driver.find('.test-right-panel .test-right-widget-title').click(); + await checkDescValueInWidgetTooltip("Table", newWidgetDesc); + }); + + it('should support basic edition in widget popup', async () => { + const mainSession = await gu.session().teamSite.login(); + await mainSession.tempDoc(cleanup, "CardView.grist", { load: true }); + + const widgetName = "Table"; + const newWidgetDescFirstLine = "First line of the description"; + const newWidgetDescSecondLine = "Second line of the description"; + + await addWidgetDescription(widgetName, newWidgetDescFirstLine, newWidgetDescSecondLine); + await checkDescValueInWidgetTooltip(widgetName, `${newWidgetDescFirstLine}\n${newWidgetDescSecondLine}`); + }); + + it('should show info tooltip only if there is a description', async () => { + const mainSession = await gu.session().teamSite.login(); + await mainSession.tempDoc(cleanup, "CardView.grist", { load: true }); + + const newWidgetDesc = "New description for widget Table"; + + await addWidgetDescription("Table", newWidgetDesc); + + assert.isFalse(await getWidgetTooltip("Single card").isPresent()); + assert.isTrue(await getWidgetTooltip("Table").isPresent()); + + await checkDescValueInWidgetTooltip("Table", newWidgetDesc); + }); +}); + +async function waitForEditPopup() { + await gu.waitToPass(async () => { + assert.isTrue(await driver.find(".test-widget-title-popup").isDisplayed()); + }); +} + +async function waitForTooltip() { + await gu.waitToPass(async () => { + assert.isTrue(await driver.find(".test-widget-info-tooltip-popup").isDisplayed()); + }); +} + +function getWidgetTitle(widgetName: string) { + return driver.findContent('.test-widget-title-text', `${widgetName}`); +} + +function getWidgetTooltip(widgetName: string) { + return getWidgetTitle(widgetName).findClosest(".test-viewsection-title").find(".test-widget-info-tooltip"); +} + +async function addWidgetDescription(widgetName: string, desc: string, descSecondLine: string = "") { + // Click on the title and open the edition popup + await getWidgetTitle(widgetName).click(); + await waitForEditPopup(); + const widgetEditPopup = await driver.find('.test-widget-title-popup'); + const widgetDescInput = await widgetEditPopup.find('.test-widget-title-section-description-input'); + + // Edit the description of the widget inside the popup + await widgetDescInput.click(); + await widgetDescInput.sendKeys(desc); + if (descSecondLine !== "") { + await widgetDescInput.sendKeys(Key.ENTER, descSecondLine); + } + await widgetDescInput.sendKeys(Key.CONTROL, Key.ENTER); + await gu.waitForServer(); +} + +async function checkDescValueInWidgetTooltip(widgetName: string, desc: string) { + await getWidgetTooltip(widgetName).click(); + await waitForTooltip(); + const descriptionTooltip = await driver + .find('.test-widget-info-tooltip-popup'); + assert.equal(await descriptionTooltip.getText(), desc); +} From 72730461ebcd887e82915d4771f70c810df2de46 Mon Sep 17 00:00:00 2001 From: softwareguru90 <131297623+softwareguru90@users.noreply.github.com> Date: Fri, 12 May 2023 18:38:29 +0300 Subject: [PATCH 03/10] Track saved version per hour, day, week, month, year, and number of times a version with parameter (#509) Determining the number of snapshots to be kept with a parameter --- README.md | 2 ++ app/server/lib/DocSnapshots.ts | 27 +++++++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 60c49f98..67adb5a9 100644 --- a/README.md +++ b/README.md @@ -291,6 +291,8 @@ COOKIE_MAX_AGE | session cookie max age, defaults to 90 days; can be set to HOME_PORT | port number to listen on for REST API server; if set to "share", add API endpoints to regular grist port. PORT | port number to listen on for Grist server REDIS_URL | optional redis server for browser sessions and db query caching +GRIST_SNAPSHOT_TIME_CAP | optional. Define the caps for tracking buckets. Usage: {"hour": 25, "day": 32, "isoWeek": 12, "month": 96, "year": 1000} +GRIST_SNAPSHOT_KEEP | optional. Number of recent snapshots to retain unconditionally for a document, regardless of when they were made Sandbox related variables: diff --git a/app/server/lib/DocSnapshots.ts b/app/server/lib/DocSnapshots.ts index a353502f..270d129a 100644 --- a/app/server/lib/DocSnapshots.ts +++ b/app/server/lib/DocSnapshots.ts @@ -1,3 +1,4 @@ +import {integerParam} from 'app/server/lib/requestUtils'; import {ObjSnapshotWithMetadata} from 'app/common/DocSnapshot'; import {SnapshotWindow} from 'app/common/Features'; import {KeyedMutex} from 'app/common/KeyedMutex'; @@ -350,16 +351,28 @@ export function shouldKeepSnapshots(snapshots: ObjSnapshotWithMetadata[], snapsh // Get time of current version const start = moment.tz(current.lastModified, tz); + const capObjectString = process.env.GRIST_SNAPSHOT_TIME_CAP + || '{"hour": 25, "day": 32, "isoWeek": 12, "month": 96, "year": 1000}'; + // Parse the stringified JSON object into an actual object + const caps = JSON.parse(capObjectString); + + // Extract the cap values for each bucket range and convert them to integers + const capHour = integerParam(caps.hour, "GRIST_SNAPSHOT_TIMEBUCKET_CAP.hour"); + const capDay = integerParam(caps.day, "GRIST_SNAPSHOT_TIMEBUCKET_CAP.day"); + const capIsoWeek = integerParam(caps.isoWeek, "GRIST_SNAPSHOT_TIMEBUCKET_CAP.isoWeek"); + const capMonth = integerParam(caps.month, "GRIST_SNAPSHOT_TIMEBUCKET_CAP.month"); + const capYear = integerParam(caps.year, "GRIST_SNAPSHOT_TIMEBUCKET_CAP.year"); // Track saved version per hour, day, week, month, year, and number of times a version // has been saved based on a corresponding rule. const buckets: TimeBucket[] = [ - {range: 'hour', prev: start, usage: 0, cap: 25}, - {range: 'day', prev: start, usage: 0, cap: 32}, - {range: 'isoWeek', prev: start, usage: 0, cap: 12}, - {range: 'month', prev: start, usage: 0, cap: 96}, - {range: 'year', prev: start, usage: 0, cap: 1000} + {range: 'hour', prev: start, usage: 0, cap: capHour}, + {range: 'day', prev: start, usage: 0, cap: capDay}, + {range: 'isoWeek', prev: start, usage: 0, cap: capIsoWeek}, + {range: 'month', prev: start, usage: 0, cap: capMonth}, + {range: 'year', prev: start, usage: 0, cap: capYear} ]; + // For each snapshot starting with newest, check if it is worth saving by comparing // it with the last saved snapshot based on hour, day, week, month, year return snapshots.map((snapshot, index) => { @@ -375,7 +388,9 @@ export function shouldKeepSnapshots(snapshots: ObjSnapshotWithMetadata[], snapsh return false; } - let keep = index < 5; // Keep 5 most recent versions + // Keep 5 most recent versions if NUM_SNAPSHOT_KEEP not exist + let keep = index < integerParam(process.env.GRIST_SNAPSHOT_KEEP || 5, "GRIST_SNAPSHOT_KEEP"); + for (const bucket of buckets) { if (updateAndCheckRange(date, bucket)) { keep = true; } } From b611526fde3e3ed59fe6e1c7897a50cdb70c5190 Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Fri, 12 May 2023 01:09:32 +0000 Subject: [PATCH 04/10] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (786 of 786 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/pt_BR/ --- static/locales/pt_BR.client.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/locales/pt_BR.client.json b/static/locales/pt_BR.client.json index d254f093..09029747 100644 --- a/static/locales/pt_BR.client.json +++ b/static/locales/pt_BR.client.json @@ -589,7 +589,8 @@ "Columns_one": "Coluna", "Columns_other": "Colunas", "Fields_one": "Campo", - "Fields_other": "Campos" + "Fields_other": "Campos", + "Add referenced columns": "Adicionar colunas referenciadas" }, "RowContextMenu": { "Copy anchor link": "Copiar o link de ancoragem", @@ -1047,6 +1048,7 @@ "Add description": "Adicionar descrição", "Column ID copied to clipboard": "ID da coluna copiada para a área de transferência", "COLUMN ID: ": "ID DA COLUNA: ", - "Provide a column label": "Forneça um rótulo de coluna" + "Provide a column label": "Forneça um rótulo de coluna", + "Close": "Fechar" } } From e0e29eccb1894fd301a4140e9d2bbf8d29111e08 Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Fri, 12 May 2023 01:09:56 +0000 Subject: [PATCH 05/10] Translated using Weblate (Spanish) Currently translated at 100.0% (786 of 786 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/es/ --- static/locales/es.client.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/locales/es.client.json b/static/locales/es.client.json index 3351488a..4d9712f6 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -486,7 +486,8 @@ "Columns_one": "Columna", "Columns_other": "Columnas", "Fields_one": "Campo", - "Fields_other": "Campos" + "Fields_other": "Campos", + "Add referenced columns": "Añadir columnas referenciadas" }, "RowContextMenu": { "Copy anchor link": "Copiar enlace de anclaje", @@ -1037,6 +1038,7 @@ "Add description": "Agregar una descripción", "Column ID copied to clipboard": "ID de la columna copiada al portapapeles", "Column description": "Descripción de la Columna", - "Provide a column label": "Proporciona una etiqueta a la columna" + "Provide a column label": "Proporciona una etiqueta a la columna", + "Close": "Cerrar" } } From 01931bfe0146219ded88300c265a5e0e226cdc7c Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Fri, 12 May 2023 00:41:26 +0000 Subject: [PATCH 06/10] Translated using Weblate (German) Currently translated at 100.0% (786 of 786 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/de/ --- static/locales/de.client.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/locales/de.client.json b/static/locales/de.client.json index 43cdfc81..23113099 100644 --- a/static/locales/de.client.json +++ b/static/locales/de.client.json @@ -589,7 +589,8 @@ "Columns_one": "Spalte", "Columns_other": "Spalten", "Fields_one": "Feld", - "Fields_other": "Felder" + "Fields_other": "Felder", + "Add referenced columns": "Referenzspalten hinzufügen" }, "RowContextMenu": { "Copy anchor link": "Ankerlink kopieren", @@ -1047,6 +1048,7 @@ "Provide a column label": "Geben Sie eine Spaltenbeschriftung an", "Save": "Speichern", "Column label": "Spaltenbeschriftung", - "Column ID copied to clipboard": "Spalten-ID in die Zwischenablage kopiert" + "Column ID copied to clipboard": "Spalten-ID in die Zwischenablage kopiert", + "Close": "Schließen" } } From a1fbb663044eb1ad0c30a72707d7199ab66af459 Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Sat, 13 May 2023 16:18:03 +0000 Subject: [PATCH 07/10] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (787 of 787 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/pt_BR/ --- static/locales/pt_BR.client.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/locales/pt_BR.client.json b/static/locales/pt_BR.client.json index 09029747..bed4a1eb 100644 --- a/static/locales/pt_BR.client.json +++ b/static/locales/pt_BR.client.json @@ -780,7 +780,8 @@ "Override widget title": "Substituir o título do Widget", "Provide a table name": "Forneça um nome de tabela", "Save": "Salvar", - "WIDGET TITLE": "TÍTULO DO WIDGET" + "WIDGET TITLE": "TÍTULO DO WIDGET", + "WIDGET DESCRIPTION": "DESCRIÇÃO DO WIDGET" }, "breadcrumbs": { "You may make edits, but they will create a new copy and will\nnot affect the original document.": "Você pode fazer edições, mas elas criarão uma nova cópia e\nnão afetarão o documento original.", From a6409776e7ffeb39061bededea846d5a32120108 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 13 May 2023 09:41:10 +0000 Subject: [PATCH 08/10] Translated using Weblate (Spanish) Currently translated at 100.0% (787 of 787 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/es/ --- static/locales/es.client.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/locales/es.client.json b/static/locales/es.client.json index 4d9712f6..c14ea705 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -637,7 +637,8 @@ "Override widget title": "Sobrescribir título del Widget", "Provide a table name": "Proporcionar un nombre de tabla", "Save": "Guardar", - "WIDGET TITLE": "TÍTULO DEL WIDGET" + "WIDGET TITLE": "TÍTULO DEL WIDGET", + "WIDGET DESCRIPTION": "DESCRIPCIÓN DEL WIDGET" }, "errorPages": { "Access denied{{suffix}}": "Acceso negado{{suffix}}", From cf8558e6b424d96784d082edb7e9cc31d5c530c0 Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Sat, 13 May 2023 15:53:49 +0000 Subject: [PATCH 09/10] Translated using Weblate (German) Currently translated at 100.0% (787 of 787 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/de/ --- static/locales/de.client.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/locales/de.client.json b/static/locales/de.client.json index 23113099..6a4cd98d 100644 --- a/static/locales/de.client.json +++ b/static/locales/de.client.json @@ -780,7 +780,8 @@ "Override widget title": "Widget-Titel überschreiben", "Provide a table name": "Geben Sie einen Tabellennamen an", "Save": "Speichern", - "WIDGET TITLE": "WIDGET TITEL" + "WIDGET TITLE": "WIDGET TITEL", + "WIDGET DESCRIPTION": "WIDGET-BESCHREIBUNG" }, "breadcrumbs": { "You may make edits, but they will create a new copy and will\nnot affect the original document.": "Sie können Änderungen vornehmen, die jedoch eine neue Kopie erstellen und\ndas Originaldokument nicht beeinflussen.", From 5c36ae698841a5e1c5ed38555755a18a225716c3 Mon Sep 17 00:00:00 2001 From: Riccardo Polignieri Date: Sat, 13 May 2023 14:28:30 +0000 Subject: [PATCH 10/10] Translated using Weblate (Italian) Currently translated at 100.0% (787 of 787 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/it/ --- static/locales/it.client.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/static/locales/it.client.json b/static/locales/it.client.json index a1cd1280..910d8294 100644 --- a/static/locales/it.client.json +++ b/static/locales/it.client.json @@ -122,7 +122,8 @@ "SELECTOR FOR": "SELETTORE PER", "Series_one": "Serie", "Series_other": "Serie", - "Sort & Filter": "Ordina e filtra" + "Sort & Filter": "Ordina e filtra", + "Add referenced columns": "Aggiungi colonne referenziate" }, "RowContextMenu": { "Copy anchor link": "Copia link", @@ -853,7 +854,8 @@ "Override widget title": "Sovrascrivi titolo widget", "Provide a table name": "Inserisci un nome per la tabella", "Save": "Salva", - "WIDGET TITLE": "TITOLO WIDGET" + "WIDGET TITLE": "TITOLO WIDGET", + "WIDGET DESCRIPTION": "DESCRIZIONE WIDGET" }, "breadcrumbs": { "You may make edits, but they will create a new copy and will\nnot affect the original document.": "Puoi fare delle modifiche, ma queste generano una nuova copia\ne l'originale resta immutato.", @@ -983,6 +985,7 @@ "COLUMN ID: ": "ID COLONNA: ", "Column label": "Etichetta colonna", "Provide a column label": "Dare un'etichetta alla colonna", - "Cancel": "Annulla" + "Cancel": "Annulla", + "Close": "Chiudi" } }