Merge branch 'MM-50966-in-product-expansion' into MM-51456-update-overage-notices
Этот коммит содержится в:
@@ -1,3 +1,2 @@
|
||||
save-exact=true
|
||||
legacy-peer-deps=true
|
||||
global-style=true
|
||||
|
||||
28
webapp/README.md
Обычный файл
28
webapp/README.md
Обычный файл
@@ -0,0 +1,28 @@
|
||||
# Mattermost Web App
|
||||
|
||||
This folder contains the client code for the Mattermost web app. It's broken up into multiple packages each of which either contains an area of the app (such as `playbooks` or `boards`) or shared logic used across other packages (such as the packages located in the `platform` directory). For anyone who's used to working in [the mattermost/mattermost-webapp repo](https://github.com/mattermost/mattermost-webapp), most of that is now located in `channels`.
|
||||
|
||||
## npm Workspaces
|
||||
|
||||
To interact with a workspace using npm, such as to add a dependency or run a script, use the `--workspace` (or `--workspaces`) flag. This can be done when using built-in npm commands such as `npm add` or when running scripts. Those commands should be run from this directory.
|
||||
|
||||
```sh
|
||||
# Add a dependency to a single package
|
||||
npm add react --workspace=boards
|
||||
|
||||
# Build multiple packages
|
||||
npm run build --workspace=packages/client --workspace=packages/components
|
||||
|
||||
# Test all workspaces
|
||||
npm test --workspaces
|
||||
|
||||
# Clean all workspaces that have a clean script defined
|
||||
npm run clean --workspaces --if-present
|
||||
```
|
||||
|
||||
To install dependencies for a workspace, simply run `npm install` from this folder as you would do normally. Most packages' dependencies will be included in the root `node_modules`, and all packages' dependencies will appear in the `package-lock.json`. A `node_modules` will only be created inside a package if one of its dependencies conflicts with that of another package.
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [Developer setup](https://developers.mattermost.com/contribute/developer-setup/), now included with the Mattermost server developer setup
|
||||
- [Web app developer documentation](https://developers.mattermost.com/contribute/more-info/webapp/)
|
||||
@@ -91,9 +91,6 @@
|
||||
"error",
|
||||
{ "allowSameFolder": true, "rootDir": "webapp/boards"}
|
||||
],
|
||||
/* "no-restricted-imports": ["error", {
|
||||
"patterns": ["..*"]
|
||||
}], */
|
||||
"import-newlines/enforce": [
|
||||
2,
|
||||
3
|
||||
@@ -101,7 +98,8 @@
|
||||
"object-curly-spacing": [
|
||||
2,
|
||||
"never"
|
||||
]
|
||||
],
|
||||
"formatjs/no-multiple-whitespaces": 2
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
@@ -2982,32 +2982,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
-----
|
||||
|
||||
The following software may be included in this product: mini-create-react-context. A copy of the source code may be downloaded from https://github.com/StringEpsilon/mini-create-react-context. This software contains the following license and notice below:
|
||||
|
||||
Copyright (c) 2019-present StringEpsilon <StringEpsilon@gmail.com>
|
||||
|
||||
Copyright (c) 2017-2019 James Kyle <me@thejameskyle.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-----
|
||||
|
||||
The following software may be included in this product: mkdirp. A copy of the source code may be downloaded from https://github.com/substack/node-mkdirp.git. This software contains the following license and notice below:
|
||||
|
||||
Copyright 2010 James Halliday (mail@substack.net)
|
||||
|
||||
@@ -453,4 +453,4 @@
|
||||
"tutorial_tip.ok": "Next",
|
||||
"tutorial_tip.out": "Opt out of these tips.",
|
||||
"tutorial_tip.seen": "Seen this before?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"AppBar.Tooltip": "تغییر وضعیت تختههای مرتبط",
|
||||
"AppBar.Tooltip": "تغییر وضعیت تابلوهای مرتبط",
|
||||
"Attachment.Attachment-title": "ضمیمه",
|
||||
"AttachmentBlock.DeleteAction": "حذف",
|
||||
"AttachmentBlock.addElement": "افزودن {type}",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"AdminBadge.SystemAdmin": "Administrateur",
|
||||
"AdminBadge.TeamAdmin": "Administrateur d'équipe",
|
||||
"AppBar.Tooltip": "Activer les panneaux liés",
|
||||
"Attachment.Attachment-title": "Pièce jointe",
|
||||
"AttachmentBlock.DeleteAction": "supprimer",
|
||||
"AttachmentBlock.addElement": "ajouter {type}",
|
||||
"AttachmentBlock.delete": "Pièce jointe supprimée.",
|
||||
"AttachmentBlock.failed": "Impossible de télécharger le fichier. Limite de taille de fichier atteinte.",
|
||||
"AttachmentElement.delete-confirmation-dialog-button-text": "Supprimer",
|
||||
"AttachmentElement.download": "Télécharger",
|
||||
"BoardComponent.add-a-group": "+ Ajouter un groupe",
|
||||
"BoardComponent.delete": "Supprimer",
|
||||
"BoardComponent.hidden-columns": "Colonnes cachées",
|
||||
@@ -71,6 +80,7 @@
|
||||
"CardBadges.title-checkboxes": "Cases à cocher",
|
||||
"CardBadges.title-comments": "Commentaires",
|
||||
"CardBadges.title-description": "Cette carte a une description",
|
||||
"CardDetail.Attach": "Joindre",
|
||||
"CardDetail.Follow": "Suivre",
|
||||
"CardDetail.Following": "Suivi",
|
||||
"CardDetail.add-content": "Ajouter du contenu",
|
||||
@@ -92,6 +102,7 @@
|
||||
"CardDetailProperty.property-deleted": "{propertyName} supprimé avec succès !",
|
||||
"CardDetailProperty.property-name-change-subtext": "de \"{oldPropType}\" à \"{newPropType}\"",
|
||||
"CardDetial.limited-link": "En savoir plus sur nos offres.",
|
||||
"CardDialog.delete-confirmation-dialog-attachment": "Confirmer la suppression de la pièce jointe",
|
||||
"CardDialog.delete-confirmation-dialog-button-text": "Supprimer",
|
||||
"CardDialog.delete-confirmation-dialog-heading": "Confirmer la suppression de la carte !",
|
||||
"CardDialog.editing-template": "Vous éditez un modèle.",
|
||||
@@ -102,9 +113,12 @@
|
||||
"Categories.CreateCategoryDialog.UpdateText": "Mettre à jour",
|
||||
"CenterPanel.Login": "Connexion",
|
||||
"CenterPanel.Share": "Partager",
|
||||
"ChannelIntro.CreateBoard": "Créer un tableau",
|
||||
"ColorOption.selectColor": "Choisir la couleur {color}",
|
||||
"Comment.delete": "Supprimer",
|
||||
"CommentsList.send": "Envoyer",
|
||||
"ConfirmPerson.empty": "Vide",
|
||||
"ConfirmPerson.search": "Recherche en cours…",
|
||||
"ConfirmationDialog.cancel-action": "Annuler",
|
||||
"ConfirmationDialog.confirm-action": "Confirmer",
|
||||
"ContentBlock.Delete": "Supprimer",
|
||||
@@ -118,9 +132,11 @@
|
||||
"ContentBlock.editText": "Éditer le texte...",
|
||||
"ContentBlock.image": "image",
|
||||
"ContentBlock.insertAbove": "Insérer au-dessus",
|
||||
"ContentBlock.moveBlock": "Déplacer le contenu de la carte",
|
||||
"ContentBlock.moveDown": "Déplacer vers le bas",
|
||||
"ContentBlock.moveUp": "Déplacer vers le haut",
|
||||
"ContentBlock.text": "texte",
|
||||
"DateFilter.empty": "Vide",
|
||||
"DateRange.clear": "Supprimer",
|
||||
"DateRange.empty": "Vide",
|
||||
"DateRange.endDate": "Date de fin",
|
||||
@@ -139,10 +155,14 @@
|
||||
"Filter.ends-with": "se termine par",
|
||||
"Filter.includes": "inclus",
|
||||
"Filter.is": "est",
|
||||
"Filter.is-after": "est après",
|
||||
"Filter.is-before": "est avant",
|
||||
"Filter.is-empty": "est vide",
|
||||
"Filter.is-not-empty": "n'est pas vide",
|
||||
"Filter.is-not-set": "n'est pas renseigné",
|
||||
"Filter.is-set": "est renseigné",
|
||||
"Filter.isafter": "est après",
|
||||
"Filter.isbefore": "est avant",
|
||||
"Filter.not-contains": "ne contient pas",
|
||||
"Filter.not-ends-with": "ne se termine pas par",
|
||||
"Filter.not-includes": "n'inclut pas",
|
||||
@@ -151,6 +171,7 @@
|
||||
"FilterByText.placeholder": "filtre de texte",
|
||||
"FilterComponent.add-filter": "+ Ajouter un filtre",
|
||||
"FilterComponent.delete": "Supprimer",
|
||||
"FilterValue.empty": "(vide)",
|
||||
"FindBoardsDialog.IntroText": "Rechercher des tableaux",
|
||||
"FindBoardsDialog.NoResultsFor": "Pas de résultats pour \"{searchQuery}\"",
|
||||
"FindBoardsDialog.NoResultsSubtext": "Vérifiez l'orthographe ou essayez une autre recherche.",
|
||||
@@ -161,6 +182,7 @@
|
||||
"GroupBy.ungroup": "Dégrouper",
|
||||
"HideBoard.MenuOption": "Cacher le tableau",
|
||||
"KanbanCard.untitled": "Sans titre",
|
||||
"MentionSuggestion.is-not-board-member": "(non membre du tableau)",
|
||||
"Mutator.new-board-from-template": "nouveau tableau à partir du modèle",
|
||||
"Mutator.new-card-from-template": "nouvelle carte depuis un modèle",
|
||||
"Mutator.new-template-from-card": "nouveau modèle depuis une carte",
|
||||
@@ -178,6 +200,9 @@
|
||||
"OnboardingTour.OpenACard.Title": "Ouvrir une carte",
|
||||
"OnboardingTour.ShareBoard.Body": "Vous pouvez partager votre tableau en interne, au sein de votre équipe ou le publier publiquement pour une visibilité en dehors de votre organisation.",
|
||||
"OnboardingTour.ShareBoard.Title": "Partager un tableau",
|
||||
"PersonProperty.board-members": "Membres du tableau",
|
||||
"PersonProperty.me": "Moi",
|
||||
"PersonProperty.non-board-members": "Non membres du tableau",
|
||||
"PropertyMenu.Delete": "Supprimer",
|
||||
"PropertyMenu.changeType": "Changer le type de la propriété",
|
||||
"PropertyMenu.selectType": "Sélectionner le type de propriété",
|
||||
@@ -187,6 +212,7 @@
|
||||
"PropertyType.CreatedTime": "Date de création",
|
||||
"PropertyType.Date": "Date",
|
||||
"PropertyType.Email": "Adresse e-mail",
|
||||
"PropertyType.MultiPerson": "Personne multiple",
|
||||
"PropertyType.MultiSelect": "Sélection multiple",
|
||||
"PropertyType.Number": "Nombre",
|
||||
"PropertyType.Person": "Personne",
|
||||
@@ -230,6 +256,8 @@
|
||||
"Sidebar.import-archive": "Importer une archive",
|
||||
"Sidebar.invite-users": "Inviter des utilisateurs",
|
||||
"Sidebar.logout": "Se déconnecter",
|
||||
"Sidebar.new-category.badge": "Nouveau",
|
||||
"Sidebar.new-category.drag-boards-cta": "Déplacer les tableaux ici...",
|
||||
"Sidebar.no-boards-in-category": "Aucun tableaux",
|
||||
"Sidebar.product-tour": "Visite guidée",
|
||||
"Sidebar.random-icons": "Icônes aléatoires",
|
||||
@@ -252,6 +280,8 @@
|
||||
"SidebarTour.SidebarCategories.Body": "Tous vos tableaux sont maintenant organisés sous votre nouvelle barre latérale. Plus besoin de basculer entre les espaces de travail. Des catégories personnalisées uniques basées sur vos espaces de travail précédents peuvent avoir été automatiquement créées pour vous dans le cadre de la mise à jour 7.2. Ceux-ci peuvent être supprimés ou modifiés selon vos préférences.",
|
||||
"SidebarTour.SidebarCategories.Link": "En savoir plus",
|
||||
"SidebarTour.SidebarCategories.Title": "Catégories de la barre latérale",
|
||||
"SiteStats.total_boards": "Total des tableaux",
|
||||
"SiteStats.total_cards": "Total des cartes",
|
||||
"TableComponent.add-icon": "Ajouter une icône",
|
||||
"TableComponent.name": "Nom",
|
||||
"TableComponent.plus-new": "+ Nouveau",
|
||||
@@ -262,6 +292,8 @@
|
||||
"TableHeaderMenu.insert-right": "Insérer à droite",
|
||||
"TableHeaderMenu.sort-ascending": "Tri ascendant",
|
||||
"TableHeaderMenu.sort-descending": "Tri descendant",
|
||||
"TableRow.DuplicateCard": "Dupliquer une carte",
|
||||
"TableRow.MoreOption": "Plus d'actions",
|
||||
"TableRow.open": "Ouvrir",
|
||||
"TopBar.give-feedback": "Donner un avis",
|
||||
"URLProperty.copiedLink": "Copié !",
|
||||
@@ -277,6 +309,7 @@
|
||||
"ValueSelector.valueSelector": "Sélecteur de value",
|
||||
"ValueSelectorLabel.openMenu": "Ouvrir le menu",
|
||||
"VersionMessage.help": "Découvrez les nouveautés de cette version.",
|
||||
"VersionMessage.learn-more": "En savoir plus",
|
||||
"View.AddView": "Ajouter une vue",
|
||||
"View.Board": "Tableau",
|
||||
"View.DeleteView": "Supprimer la vue",
|
||||
@@ -331,6 +364,9 @@
|
||||
"WelcomePage.StartUsingIt.Text": "Commencez à l'utiliser",
|
||||
"Workspace.editing-board-template": "Vous éditez un modèle de tableau.",
|
||||
"badge.guest": "Invité",
|
||||
"boardPage.confirm-join-button": "Rejoindre",
|
||||
"boardPage.confirm-join-text": "Vous êtes sur le point de rejoindre un forum privé sans avoir été explicitement ajouté par l'administrateur du forum. Êtes-vous sûr de vouloir rejoindre ce forum privé ?",
|
||||
"boardPage.confirm-join-title": "Rejoindre un tableau privé",
|
||||
"boardSelector.confirm-link-board": "Lier la carte au canal",
|
||||
"boardSelector.confirm-link-board-button": "Oui, lier ce tableau",
|
||||
"boardSelector.confirm-link-board-subtext": "Lorsque vous liez \"{boardName}\" au canal, tous les membres du canal (existants et nouveaux) pourront le modifier. Vous pouvez dissocier un tableau d'un canal à tout moment.",
|
||||
@@ -343,6 +379,8 @@
|
||||
"calendar.month": "Mois",
|
||||
"calendar.today": "AUJOURD'HUI",
|
||||
"calendar.week": "Semaine",
|
||||
"centerPanel.undefined": "Pas de {propertyName}",
|
||||
"centerPanel.unknown-user": "Utilisateur inconnu",
|
||||
"cloudMessage.learn-more": "En savoir plus",
|
||||
"createImageBlock.failed": "Impossible de télécharger le fichier. Limite de taille de fichier atteinte.",
|
||||
"default-properties.badges": "Commentaires et description",
|
||||
@@ -364,6 +402,10 @@
|
||||
"login.log-in-button": "Connexion",
|
||||
"login.log-in-title": "Connexion",
|
||||
"login.register-button": "ou créez un compte si vous n'en avez pas",
|
||||
"new_channel_modal.create_board.empty_board_description": "Créer un nouveau tableau vide",
|
||||
"new_channel_modal.create_board.empty_board_title": "Tableau vide",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "Sélectionner un modèle",
|
||||
"new_channel_modal.create_board.title": "Créer un tableau pour ce canal",
|
||||
"notification-box-card-limit-reached.close-tooltip": "Oublier pendant 10 jours",
|
||||
"notification-box-card-limit-reached.contact-link": "informez votre administrateur",
|
||||
"notification-box-card-limit-reached.link": "Passer à une offre payante",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"AppBar.Tooltip": "リンク先Boardの切替え",
|
||||
"AdminBadge.SystemAdmin": "管理者",
|
||||
"AdminBadge.TeamAdmin": "チーム管理者",
|
||||
"AppBar.Tooltip": "リンク先Boardsの切替え",
|
||||
"Attachment.Attachment-title": "添付する",
|
||||
"AttachmentBlock.DeleteAction": "削除",
|
||||
"AttachmentBlock.addElement": "{type} を追加",
|
||||
@@ -137,6 +139,7 @@
|
||||
"ContentBlock.moveDown": "下へ移動する",
|
||||
"ContentBlock.moveUp": "上へ移動する",
|
||||
"ContentBlock.text": "テキスト",
|
||||
"DateFilter.empty": "空",
|
||||
"DateRange.clear": "クリア",
|
||||
"DateRange.empty": "空",
|
||||
"DateRange.endDate": "終了日",
|
||||
@@ -155,10 +158,14 @@
|
||||
"Filter.ends-with": "で終わる",
|
||||
"Filter.includes": "を含む",
|
||||
"Filter.is": "と一致する",
|
||||
"Filter.is-after": "が次の日付以降",
|
||||
"Filter.is-before": "が次の日付以前",
|
||||
"Filter.is-empty": "が空である",
|
||||
"Filter.is-not-empty": "が空でない",
|
||||
"Filter.is-not-set": "が未設定",
|
||||
"Filter.is-set": "が設定済み",
|
||||
"Filter.isafter": "が次の日付以降",
|
||||
"Filter.isbefore": "が次の日付以前",
|
||||
"Filter.not-contains": "を含まない",
|
||||
"Filter.not-ends-with": "で終わらない",
|
||||
"Filter.not-includes": "を含まない",
|
||||
@@ -305,6 +312,7 @@
|
||||
"ValueSelector.valueSelector": "値選択",
|
||||
"ValueSelectorLabel.openMenu": "メニューを開く",
|
||||
"VersionMessage.help": "このバージョンの新機能を確認する。",
|
||||
"VersionMessage.learn-more": "詳しく",
|
||||
"View.AddView": "ビューを追加",
|
||||
"View.Board": "Board",
|
||||
"View.DeleteView": "ビューを削除",
|
||||
@@ -359,6 +367,9 @@
|
||||
"WelcomePage.StartUsingIt.Text": "利用を開始する",
|
||||
"Workspace.editing-board-template": "Boardのテンプレートを編集しています。",
|
||||
"badge.guest": "ゲスト",
|
||||
"boardPage.confirm-join-button": "参加",
|
||||
"boardPage.confirm-join-text": "あなたは、ボード管理者によって明示的に追加されることなく、非公開のボードに参加しようとしています。本当にこの非公開ボードに参加しますか?",
|
||||
"boardPage.confirm-join-title": "非公開ボードに参加",
|
||||
"boardSelector.confirm-link-board": "Boardをチャンネルへリンク",
|
||||
"boardSelector.confirm-link-board-button": "はい、Boardをリンクします",
|
||||
"boardSelector.confirm-link-board-subtext": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。Boardとチャンネルのリンク解除はいつでも可能です。",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"BoardComponent.hidden-columns": "დამალული სვეტები",
|
||||
"BoardComponent.hide": "დამალვა",
|
||||
"BoardComponent.new": "+ ახალი",
|
||||
"BoardComponent.no-property": "არ არის {Property}",
|
||||
"BoardComponent.no-property": "არ არის {property}",
|
||||
"BoardComponent.no-property-title": "ცარიელი {property} საკუთრების მქონე ელემენტები აქ წავა. ამ სვეტის წაშლა შეუძლებელია.",
|
||||
"BoardComponent.show": "ჩვენება",
|
||||
"BoardMember.schemeAdmin": "ადმინისტრატორი",
|
||||
|
||||
@@ -1 +1,456 @@
|
||||
{}
|
||||
{
|
||||
"AdminBadge.SystemAdmin": "Administratorius",
|
||||
"AdminBadge.TeamAdmin": "Komandos administratorius",
|
||||
"AppBar.Tooltip": "Perjungti susietas lentas",
|
||||
"Attachment.Attachment-title": "Priedas",
|
||||
"AttachmentBlock.DeleteAction": "Ištrinti",
|
||||
"AttachmentBlock.addElement": "pridėti {type}",
|
||||
"AttachmentBlock.delete": "Priedas ištrintas.",
|
||||
"AttachmentBlock.failed": "Šio failo nepavyko įkelti, nes pasiektas failo dydžio limitas.",
|
||||
"AttachmentBlock.upload": "Priedo įkėlimas.",
|
||||
"AttachmentBlock.uploadSuccess": "Priedas įkeltas.",
|
||||
"AttachmentElement.delete-confirmation-dialog-button-text": "Ištrinti",
|
||||
"AttachmentElement.download": "Parsisiųsti",
|
||||
"AttachmentElement.upload-percentage": "Įkeliama...({uploadPercent}%)",
|
||||
"BoardComponent.add-a-group": "+ Pridėti grupę",
|
||||
"BoardComponent.delete": "Ištrinti",
|
||||
"BoardComponent.hidden-columns": "Paslėpti stulpeliai",
|
||||
"BoardComponent.hide": "Slėpti",
|
||||
"BoardComponent.new": "+ Naujas",
|
||||
"BoardComponent.no-property": "Nėra {property}",
|
||||
"BoardComponent.no-property-title": "Elementai su tuščia nuosavybe {property} pateks čia. Šio stulpelio pašalinti negalima.",
|
||||
"BoardComponent.show": "Rodyti",
|
||||
"BoardMember.schemeAdmin": "Administratorius",
|
||||
"BoardMember.schemeCommenter": "Komentuotojas",
|
||||
"BoardMember.schemeEditor": "Redaktorius",
|
||||
"BoardMember.schemeNone": "Joks",
|
||||
"BoardMember.schemeViewer": "Žiūriklis",
|
||||
"BoardMember.unlinkChannel": "Atsieti",
|
||||
"BoardPage.newVersion": "Yra nauja Boards versija. Spustelėkite čia, kad įkeltumėte iš naujo.",
|
||||
"BoardPage.syncFailed": "Lenta gali būti ištrinta arba jos prieiga gali būti atšaukta.",
|
||||
"BoardTemplateSelector.add-template": "Sukurti naują šabloną",
|
||||
"BoardTemplateSelector.create-empty-board": "Sukurti tuščią lentą",
|
||||
"BoardTemplateSelector.delete-template": "Ištrinti",
|
||||
"BoardTemplateSelector.description": "Pridėkite lentą prie šoninės juostos naudodami bet kurį iš toliau nurodytų šablonų arba pradėkite nuo nulio.",
|
||||
"BoardTemplateSelector.edit-template": "Redaguoti",
|
||||
"BoardTemplateSelector.plugin.no-content-description": "Pridėkite lentą prie šoninės juostos naudodami bet kurį iš toliau nurodytų šablonų arba pradėkite nuo nulio.",
|
||||
"BoardTemplateSelector.plugin.no-content-title": "Sukurti lentą",
|
||||
"BoardTemplateSelector.title": "Sukurti lentą",
|
||||
"BoardTemplateSelector.use-this-template": "Naudoti šį šabloną",
|
||||
"BoardsSwitcher.Title": "Rasti lentas",
|
||||
"BoardsUnfurl.Limited": "Papildoma informacija yra paslėpta, nes kortelė yra archyvuojama",
|
||||
"BoardsUnfurl.Remainder": "+{remainder} dar",
|
||||
"BoardsUnfurl.Updated": "Atnaujinta {time}",
|
||||
"Calculations.Options.average.displayName": "Vidutinis",
|
||||
"Calculations.Options.average.label": "Vidutinis",
|
||||
"Calculations.Options.count.displayName": "Skaičius",
|
||||
"Calculations.Options.count.label": "Skaičius",
|
||||
"Calculations.Options.countChecked.displayName": "Pažymėta",
|
||||
"Calculations.Options.countChecked.label": "Skaičius patikrintas",
|
||||
"Calculations.Options.countUnchecked.displayName": "Nepažymėta",
|
||||
"Calculations.Options.countUnchecked.label": "Skaičiaus žymėjimas panaikintas",
|
||||
"Calculations.Options.countUniqueValue.displayName": "Unikalus",
|
||||
"Calculations.Options.countUniqueValue.label": "Suskaičiuoti unikalias reikšmes",
|
||||
"Calculations.Options.countValue.displayName": "Reikšmės",
|
||||
"Calculations.Options.countValue.label": "Skaičius",
|
||||
"Calculations.Options.dateRange.displayName": "Diapazonas",
|
||||
"Calculations.Options.dateRange.label": "Diapazonas",
|
||||
"Calculations.Options.earliest.displayName": "Anksčiausiai",
|
||||
"Calculations.Options.earliest.label": "Anksčiausiai",
|
||||
"Calculations.Options.latest.displayName": "Naujausias",
|
||||
"Calculations.Options.latest.label": "Naujausias",
|
||||
"Calculations.Options.max.displayName": "Maks.",
|
||||
"Calculations.Options.max.label": "Maks.",
|
||||
"Calculations.Options.median.displayName": "Mediana",
|
||||
"Calculations.Options.median.label": "Mediana",
|
||||
"Calculations.Options.min.displayName": "Min.",
|
||||
"Calculations.Options.min.label": "Min.",
|
||||
"Calculations.Options.none.displayName": "Apskaičiuoti",
|
||||
"Calculations.Options.none.label": "Joks",
|
||||
"Calculations.Options.percentChecked.displayName": "Pažymėta",
|
||||
"Calculations.Options.percentChecked.label": "Patikrinta procentais",
|
||||
"Calculations.Options.percentUnchecked.displayName": "Nepažymėta",
|
||||
"Calculations.Options.percentUnchecked.label": "Nepažymėtas procentas",
|
||||
"Calculations.Options.range.displayName": "Diapazonas",
|
||||
"Calculations.Options.range.label": "Diapazonas",
|
||||
"Calculations.Options.sum.displayName": "Iš viso",
|
||||
"Calculations.Options.sum.label": "Iš viso",
|
||||
"CalendarCard.untitled": "Be pavadinimo",
|
||||
"CardActionsMenu.copiedLink": "Nukopijuota!",
|
||||
"CardActionsMenu.copyLink": "Kopijuoti nuorodą",
|
||||
"CardActionsMenu.delete": "Ištrinti",
|
||||
"CardActionsMenu.duplicate": "Pasikartojantis",
|
||||
"CardBadges.title-checkboxes": "Žymimieji langeliai",
|
||||
"CardBadges.title-comments": "Komentarai",
|
||||
"CardBadges.title-description": "Ši kortelė turi aprašymą",
|
||||
"CardDetail.Attach": "Prisegti",
|
||||
"CardDetail.Follow": "Sekti",
|
||||
"CardDetail.Following": "Sekama",
|
||||
"CardDetail.add-content": "Pridėti turinį",
|
||||
"CardDetail.add-icon": "Pridėti piktogramą",
|
||||
"CardDetail.add-property": "+ Pridėti savybę",
|
||||
"CardDetail.addCardText": "pridėti kortelės tekstą",
|
||||
"CardDetail.limited-body": "Naujovinkite į mūsų Professional arba Enterprise planą.",
|
||||
"CardDetail.limited-button": "Patobulinti",
|
||||
"CardDetail.limited-title": "Ši kortelė paslėpta",
|
||||
"CardDetail.moveContent": "Perkelti kortelės turinį",
|
||||
"CardDetail.new-comment-placeholder": "Pridėti komentarą...",
|
||||
"CardDetailProperty.confirm-delete-heading": "Patvirtinkite nuosavybės ištrynimą",
|
||||
"CardDetailProperty.confirm-delete-subtext": "Ar tikrai norite ištrinti savybę „{propertyName}“? Ją ištrynus, savybė bus ištrinta iš visų šios lentos kortelių.",
|
||||
"CardDetailProperty.confirm-property-name-change-subtext": "Ar tikrai norite pakeisti savybę „{propertyName}“ {customText}? Tai turės įtakos {numOfCards} kortelės reikšmei (-ėms) šioje lentoje ir duomenys gali būti prarasti.",
|
||||
"CardDetailProperty.confirm-property-type-change": "Patvirtinkite nuosavybės tipo pakeitimą",
|
||||
"CardDetailProperty.delete-action-button": "Ištrinti",
|
||||
"CardDetailProperty.property-change-action-button": "Keisti savybę",
|
||||
"CardDetailProperty.property-changed": "Savybė sėkmingai pakeista!",
|
||||
"CardDetailProperty.property-deleted": "{propertyName} sėkmingai ištrinta!",
|
||||
"CardDetailProperty.property-name-change-subtext": "įveskite iš „{oldPropType}“ į „{newPropType}“",
|
||||
"CardDetial.limited-link": "Sužinokite daugiau apie mūsų planus.",
|
||||
"CardDialog.delete-confirmation-dialog-attachment": "Patvirtinkite priedo ištrynimą",
|
||||
"CardDialog.delete-confirmation-dialog-button-text": "Ištrinti",
|
||||
"CardDialog.delete-confirmation-dialog-heading": "Patvirtinkite kortelės ištrynimą",
|
||||
"CardDialog.editing-template": "Jūs redaguojate šabloną.",
|
||||
"CardDialog.nocard": "Šios kortelės nėra arba ji nepasiekiama.",
|
||||
"Categories.CreateCategoryDialog.CancelText": "Atšaukti",
|
||||
"Categories.CreateCategoryDialog.CreateText": "Sukurti",
|
||||
"Categories.CreateCategoryDialog.Placeholder": "Pavadinkite savo kategoriją",
|
||||
"Categories.CreateCategoryDialog.UpdateText": "Atnaujinti",
|
||||
"CenterPanel.Login": "Prisijungti",
|
||||
"CenterPanel.Share": "Bendrinti",
|
||||
"ChannelIntro.CreateBoard": "Sukurti lentą",
|
||||
"ColorOption.selectColor": "Pasirinkti {color} spalvą",
|
||||
"Comment.delete": "Ištrinti",
|
||||
"CommentsList.send": "Siųsti",
|
||||
"ConfirmPerson.empty": "Tuščias",
|
||||
"ConfirmPerson.search": "Ieškoti...",
|
||||
"ConfirmationDialog.cancel-action": "Atšaukti",
|
||||
"ConfirmationDialog.confirm-action": "Patvirtinti",
|
||||
"ContentBlock.Delete": "Ištrinti",
|
||||
"ContentBlock.DeleteAction": "Ištrinti",
|
||||
"ContentBlock.addElement": "pridėti {type}",
|
||||
"ContentBlock.checkbox": "žymimasis langelis",
|
||||
"ContentBlock.divider": "skirtukas",
|
||||
"ContentBlock.editCardCheckbox": "perjungtas žymimasis laukelis",
|
||||
"ContentBlock.editCardCheckboxText": "redaguoti kortelės tekstą",
|
||||
"ContentBlock.editCardText": "redaguoti kortelės tekstą",
|
||||
"ContentBlock.editText": "Keisti tekstą...",
|
||||
"ContentBlock.image": "Paveikslėlis",
|
||||
"ContentBlock.insertAbove": "Įdėti aukščiau",
|
||||
"ContentBlock.moveBlock": "perkelti kortelės turinį",
|
||||
"ContentBlock.moveDown": "Perkelti žemyn",
|
||||
"ContentBlock.moveUp": "Perkelti aukštyn",
|
||||
"ContentBlock.text": "tekstas",
|
||||
"DateFilter.empty": "Tuščias",
|
||||
"DateRange.clear": "Išvalyti",
|
||||
"DateRange.empty": "Tuščias",
|
||||
"DateRange.endDate": "Pabaigos data",
|
||||
"DateRange.today": "Šiandien",
|
||||
"DeleteBoardDialog.confirm-cancel": "Atšaukti",
|
||||
"DeleteBoardDialog.confirm-delete": "Ištrinti",
|
||||
"DeleteBoardDialog.confirm-info": "Ar tikrai norite ištrinti lentą „{boardTitle}“? Ją ištrynus, bus ištrintos visos lentos kortelės.",
|
||||
"DeleteBoardDialog.confirm-info-template": "Ar tikrai norite ištrinti lentos šabloną „{boardTitle}“?",
|
||||
"DeleteBoardDialog.confirm-tite": "Patvirtinkite lentos ištrynimą",
|
||||
"DeleteBoardDialog.confirm-tite-template": "Patvirtinkite lentos šablono ištrynimą",
|
||||
"Dialog.closeDialog": "Uždaryti dialogo langą",
|
||||
"EditableDayPicker.today": "Šiandien",
|
||||
"Error.mobileweb": "Mobiliojo žiniatinklio palaikymas šiuo metu yra ankstyvoje beta versijoje. Gali būti ne visos funkcijos.",
|
||||
"Error.websocket-closed": "Interneto lizdo ryšys uždarytas, ryšys nutrauktas. Jei tai išlieka, patikrinkite savo serverio arba žiniatinklio tarpinio serverio konfigūraciją.",
|
||||
"Filter.contains": "turi",
|
||||
"Filter.ends-with": "baigiasi",
|
||||
"Filter.includes": "apima",
|
||||
"Filter.is": "yra",
|
||||
"Filter.is-after": "yra po",
|
||||
"Filter.is-before": "yra prieš",
|
||||
"Filter.is-empty": "yra tuščias",
|
||||
"Filter.is-not-empty": "nėra tuščias",
|
||||
"Filter.is-not-set": "nėra nustatytas",
|
||||
"Filter.is-set": "yra nustatytas",
|
||||
"Filter.isafter": "yra po",
|
||||
"Filter.isbefore": "yra prieš",
|
||||
"Filter.not-contains": "nėra",
|
||||
"Filter.not-ends-with": "nesibaigia",
|
||||
"Filter.not-includes": "neapima",
|
||||
"Filter.not-starts-with": "neprasideda",
|
||||
"Filter.starts-with": "prasideda",
|
||||
"FilterByText.placeholder": "filtruoti tekstą",
|
||||
"FilterComponent.add-filter": "+ Pridėti filtrą",
|
||||
"FilterComponent.delete": "Ištrinti",
|
||||
"FilterValue.empty": "(tuščia)",
|
||||
"FindBoardsDialog.IntroText": "Ieškoti lentų",
|
||||
"FindBoardsDialog.NoResultsFor": "Nėra rezultatų pagal „{searchQuery}“",
|
||||
"FindBoardsDialog.NoResultsSubtext": "Patikrinkite rašybą arba pabandykite atlikti kitą paiešką.",
|
||||
"FindBoardsDialog.SubTitle": "Įveskite, kad rastumėte lentą. Norėdami naršyti, naudokite <b>AUKŠTYN / ŽEMYN</b>. <b>ENTER</b> , kad pasirinktumėte, <b>ESC</b> , kad atsisakytumėte",
|
||||
"FindBoardsDialog.Title": "Raskite lentas",
|
||||
"GroupBy.hideEmptyGroups": "Slėpti {count} tuščias grupes",
|
||||
"GroupBy.showHiddenGroups": "Rodyti paslėptas grupes: {count}",
|
||||
"GroupBy.ungroup": "Išgrupuoti",
|
||||
"HideBoard.MenuOption": "Paslėpti lentą",
|
||||
"KanbanCard.untitled": "Be pavadinimo",
|
||||
"MentionSuggestion.is-not-board-member": "(ne lentos narys)",
|
||||
"Mutator.new-board-from-template": "nauja lenta pagal šabloną",
|
||||
"Mutator.new-card-from-template": "nauja kortelė pagal šabloną",
|
||||
"Mutator.new-template-from-card": "naujas šablonas pagal kortelę",
|
||||
"OnboardingTour.AddComments.Body": "Galite komentuoti problemas ir net @paminėti kitus Mattermost naudotojus, kad atkreiptumėte jų dėmesį.",
|
||||
"OnboardingTour.AddComments.Title": "Pridėti komentarų",
|
||||
"OnboardingTour.AddDescription.Body": "Pridėkite aprašymą prie savo kortelės, kad komandos draugai žinotų, kam ji skirta.",
|
||||
"OnboardingTour.AddDescription.Title": "Pridėti aprašymą",
|
||||
"OnboardingTour.AddProperties.Body": "Pridėkite prie kortelių įvairių savybių, kad jos būtų dar galingesnės.",
|
||||
"OnboardingTour.AddProperties.Title": "Pridėti savybių",
|
||||
"OnboardingTour.AddView.Body": "Eikite čia, kad sukurtumėte naują rodinį ir galėtumėte tvarkyti lentą naudodami skirtingus išdėstymus.",
|
||||
"OnboardingTour.AddView.Title": "Pridėti naują rodinį",
|
||||
"OnboardingTour.CopyLink.Body": "Galite bendrinti savo korteles su komandos draugais nukopijuodami nuorodą ir įklijuodami ją į kanalą, tiesioginį pranešimą ar grupės pranešimą.",
|
||||
"OnboardingTour.CopyLink.Title": "Kopijuoti nuorodą",
|
||||
"OnboardingTour.OpenACard.Body": "Atidarykite kortelę, kad sužinotumėte, kokiais galingais būdais Boards gali padėti organizuoti darbą.",
|
||||
"OnboardingTour.OpenACard.Title": "Atidaryti kortelę",
|
||||
"OnboardingTour.ShareBoard.Body": "Galite bendrinti lentą viduje, savo komandoje arba paskelbti ją viešai, kad būtų matoma už organizacijos ribų.",
|
||||
"OnboardingTour.ShareBoard.Title": "Bendrinti lentą",
|
||||
"PersonProperty.board-members": "Lentos nariai",
|
||||
"PersonProperty.me": "aš",
|
||||
"PersonProperty.non-board-members": "Ne lentos nariai",
|
||||
"PropertyMenu.Delete": "Ištrinti",
|
||||
"PropertyMenu.changeType": "Pakeisti savybės tipą",
|
||||
"PropertyMenu.selectType": "Pasirinkti savybės tipą",
|
||||
"PropertyMenu.typeTitle": "Tipas",
|
||||
"PropertyType.Checkbox": "Žymimasis langelis",
|
||||
"PropertyType.CreatedBy": "Sukurta",
|
||||
"PropertyType.CreatedTime": "Sukūrimo laikas",
|
||||
"PropertyType.Date": "Data",
|
||||
"PropertyType.Email": "El. paštas",
|
||||
"PropertyType.MultiPerson": "Daugelio žmonių",
|
||||
"PropertyType.MultiSelect": "Keletas pasirinkimų",
|
||||
"PropertyType.Number": "Skaičius",
|
||||
"PropertyType.Person": "Asmuo",
|
||||
"PropertyType.Phone": "Telefonas",
|
||||
"PropertyType.Select": "Pasirinkite",
|
||||
"PropertyType.Text": "Tekstas",
|
||||
"PropertyType.Unknown": "Nežinoma",
|
||||
"PropertyType.UpdatedBy": "Paskutinį kartą atnaujino",
|
||||
"PropertyType.UpdatedTime": "Paskutinio atnaujinimo laikas",
|
||||
"PropertyType.Url": "URL",
|
||||
"PropertyValueElement.empty": "Tuščias",
|
||||
"RegistrationLink.confirmRegenerateToken": "Tai panaikins anksčiau bendrintas nuorodas. Tęsti?",
|
||||
"RegistrationLink.copiedLink": "Nukopijuota!",
|
||||
"RegistrationLink.copyLink": "Kopijuoti nuorodą",
|
||||
"RegistrationLink.description": "Pasidalykite šia nuoroda, kad kiti galėtų susikurti paskyras:",
|
||||
"RegistrationLink.regenerateToken": "Atkurti prieigos raktą",
|
||||
"RegistrationLink.tokenRegenerated": "Registracijos nuoroda sugeneruota iš naujo",
|
||||
"ShareBoard.PublishDescription": "Paskelbti ir bendrinti tik skaitomą nuorodą su visais žiniatinklio naudotojais.",
|
||||
"ShareBoard.PublishTitle": "Paskelbti žiniatinklyje",
|
||||
"ShareBoard.ShareInternal": "Bendrinti viduje",
|
||||
"ShareBoard.ShareInternalDescription": "Leidimus turintys naudotojai galės naudotis šia nuoroda.",
|
||||
"ShareBoard.Title": "Bendrinti lentą",
|
||||
"ShareBoard.confirmRegenerateToken": "Tai panaikins anksčiau bendrintas nuorodas. Tęsti?",
|
||||
"ShareBoard.copiedLink": "Nukopijuota!",
|
||||
"ShareBoard.copyLink": "Kopijuoti nuorodą",
|
||||
"ShareBoard.regenerate": "Atkurti prieigos raktą",
|
||||
"ShareBoard.searchPlaceholder": "Ieškoti žmonių ir kanalų",
|
||||
"ShareBoard.teamPermissionsText": "Visi {teamName} komandos nariai",
|
||||
"ShareBoard.tokenRegenrated": "Prieigos raktas atkurtas",
|
||||
"ShareBoard.userPermissionsRemoveMemberText": "Pašalinti narį",
|
||||
"ShareBoard.userPermissionsYouText": "(Jūs)",
|
||||
"ShareTemplate.Title": "Bendrinti šabloną",
|
||||
"ShareTemplate.searchPlaceholder": "Ieškoti žmonių",
|
||||
"Sidebar.delete-board": "Ištrinti lentą",
|
||||
"Sidebar.duplicate-board": "Pasikartojanti lenta",
|
||||
"Sidebar.export-archive": "Eksportuoti archyvą",
|
||||
"Sidebar.import": "Importuoti",
|
||||
"Sidebar.import-archive": "Importuoti archyvą",
|
||||
"Sidebar.new-category.badge": "Naujas",
|
||||
"Sidebar.new-category.drag-boards-cta": "Vilkite lentas čia...",
|
||||
"Sidebar.no-boards-in-category": "Viduje nėra lentų",
|
||||
"Sidebar.product-tour": "Produkto apžvalga",
|
||||
"Sidebar.random-icons": "Atsitiktinės piktogramos",
|
||||
"Sidebar.set-language": "Nustatyti kalbą",
|
||||
"Sidebar.set-theme": "Nustatyti temą",
|
||||
"Sidebar.settings": "Nustatymai",
|
||||
"Sidebar.template-from-board": "Naujas šablonas pagal lentą",
|
||||
"Sidebar.untitled-board": "(Lenta be pavadinimo)",
|
||||
"Sidebar.untitled-view": "(Rodinys be pavadinimo)",
|
||||
"SidebarCategories.BlocksMenu.Move": "Perkelti į...",
|
||||
"SidebarCategories.CategoryMenu.CreateNew": "Sukurti naują kategoriją",
|
||||
"SidebarCategories.CategoryMenu.Delete": "Ištrinti kategoriją",
|
||||
"SidebarCategories.CategoryMenu.DeleteModal.Body": "<b>{categoryName}</b> lentos bus perkeltos atgal į lentų kategorijas. Jūs nesate pašalintas iš jokių lentų.",
|
||||
"SidebarCategories.CategoryMenu.DeleteModal.Title": "Ištrinti šią kategoriją?",
|
||||
"SidebarCategories.CategoryMenu.Update": "Pervadinti kategoriją",
|
||||
"SidebarTour.ManageCategories.Body": "Kurkite ir tvarkykite pasirinktines kategorijas. Kategorijos priklauso nuo naudotojo, todėl lentos perkėlimas į kategoriją neturės įtakos kitiems nariams, naudojantiems tą pačią lentą.",
|
||||
"SidebarTour.ManageCategories.Title": "Tvarkyti kategorijas",
|
||||
"SidebarTour.SearchForBoards.Body": "Atidarykite lentos perjungiklį (Cmd / Ctrl + K), kad galėtumėte greitai ieškoti ir pridėti lentų šoninėje juostoje.",
|
||||
"SidebarTour.SearchForBoards.Title": "Ieškoti lentų",
|
||||
"SidebarTour.SidebarCategories.Body": "Visos Jūsų lentos dabar sutvarkytos naujoje šoninėje juostoje. Nebereikia perjungti darbo erdvių. Vienkartinės pasirinktinės kategorijos, pagrįstos ankstesnėmis darbo sritimis, gali būti automatiškai sukurtos Jums atnaujinant į 7.2 versiją. Jas galima pašalinti arba redaguoti pagal savo pageidavimus.",
|
||||
"SidebarTour.SidebarCategories.Link": "Sužinoti daugiau",
|
||||
"SidebarTour.SidebarCategories.Title": "Šoninės juostos kategorijos",
|
||||
"SiteStats.total_boards": "Iš viso lentų",
|
||||
"SiteStats.total_cards": "Iš viso kortelių",
|
||||
"TableComponent.add-icon": "Pridėti piktogramą",
|
||||
"TableComponent.name": "Pavadinimas",
|
||||
"TableComponent.plus-new": "+ Naujas",
|
||||
"TableHeaderMenu.delete": "Ištrinti",
|
||||
"TableHeaderMenu.duplicate": "Pasikartojantis",
|
||||
"TableHeaderMenu.hide": "Slėpti",
|
||||
"TableHeaderMenu.insert-left": "Įdėkite kairėje",
|
||||
"TableHeaderMenu.insert-right": "Įdėkite dešinėje",
|
||||
"TableHeaderMenu.sort-ascending": "Rūšiuoti didėjančia tvarka",
|
||||
"TableHeaderMenu.sort-descending": "Rūšiuoti mažėjančia tvarka",
|
||||
"TableRow.DuplicateCard": "kortelės dublikatas",
|
||||
"TableRow.MoreOption": "Daugiau veiksmų",
|
||||
"TableRow.open": "Atverti",
|
||||
"TopBar.give-feedback": "Palikti atsiliepimą",
|
||||
"URLProperty.copiedLink": "Nukopijuota!",
|
||||
"URLProperty.copy": "Kopijuoti",
|
||||
"URLProperty.edit": "Redaguoti",
|
||||
"UndoRedoHotKeys.canRedo": "Grąžinti",
|
||||
"UndoRedoHotKeys.canRedo-with-description": "Perdaryti {description}",
|
||||
"UndoRedoHotKeys.canUndo": "Anuliuoti",
|
||||
"UndoRedoHotKeys.canUndo-with-description": "Anuliuoti {description}",
|
||||
"UndoRedoHotKeys.cannotRedo": "Nėra ką perdaryti",
|
||||
"UndoRedoHotKeys.cannotUndo": "Nėra ką anuliuoti",
|
||||
"ValueSelector.noOptions": "Jokių parinkčių. Pradėkite rašyti, kad pridėtumėte pirmąją!",
|
||||
"ValueSelector.valueSelector": "Reikšmės parinkiklis",
|
||||
"ValueSelectorLabel.openMenu": "Atidaryti meniu",
|
||||
"VersionMessage.help": "Patikrinkite, kas naujo šioje versijoje.",
|
||||
"VersionMessage.learn-more": "Sužinoti daugiau",
|
||||
"View.AddView": "Pridėti rodinį",
|
||||
"View.Board": "Lenta",
|
||||
"View.DeleteView": "Ištrinti rodinį",
|
||||
"View.DuplicateView": "Pasikartojantis rodinys",
|
||||
"View.Gallery": "Galerija",
|
||||
"View.NewBoardTitle": "Lentos rodinys",
|
||||
"View.NewCalendarTitle": "Kalendoriaus rodinys",
|
||||
"View.NewGalleryTitle": "Galerijos vaizdas",
|
||||
"View.NewTableTitle": "Lentelės rodinys",
|
||||
"View.NewTemplateDefaultTitle": "Šablonas be pavadinimo",
|
||||
"View.NewTemplateTitle": "Be pavadinimo",
|
||||
"View.Table": "Lentelė",
|
||||
"ViewHeader.add-template": "Naujas šablonas",
|
||||
"ViewHeader.delete-template": "Ištrinti",
|
||||
"ViewHeader.display-by": "Pateikė: {property}",
|
||||
"ViewHeader.edit-template": "Redaguoti",
|
||||
"ViewHeader.empty-card": "Tuščia kortelė",
|
||||
"ViewHeader.export-board-archive": "Eksportuoti lentos archyvą",
|
||||
"ViewHeader.export-complete": "Eksportas baigtas!",
|
||||
"ViewHeader.export-csv": "Eksportuoti į CSV",
|
||||
"ViewHeader.export-failed": "Eksportuoti nepavyko!",
|
||||
"ViewHeader.filter": "Filtras",
|
||||
"ViewHeader.group-by": "Grupuoti pagal: {property}",
|
||||
"ViewHeader.new": "Naujas",
|
||||
"ViewHeader.properties": "Savybės",
|
||||
"ViewHeader.properties-menu": "Savybių meniu",
|
||||
"ViewHeader.search-text": "Ieškoti kortelių",
|
||||
"ViewHeader.select-a-template": "Pasirinkti šabloną",
|
||||
"ViewHeader.set-default-template": "Nustatyti kaip numatytąjį",
|
||||
"ViewHeader.sort": "Rūšiuoti",
|
||||
"ViewHeader.untitled": "Be pavadinimo",
|
||||
"ViewHeader.view-header-menu": "Žiūrėti antraštės meniu",
|
||||
"ViewHeader.view-menu": "Žiūrėti meniu",
|
||||
"ViewLimitDialog.Heading": "Pasiektas peržiūrų skaičius vienoje lentoje",
|
||||
"ViewLimitDialog.PrimaryButton.Title.Admin": "Patobulinti",
|
||||
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Pranešti administratoriui",
|
||||
"ViewLimitDialog.Subtext.Admin": "Naujovinkite į mūsų Professional arba Enterprise planą.",
|
||||
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Sužinokite daugiau apie mūsų planus.",
|
||||
"ViewLimitDialog.Subtext.RegularUser": "Praneškite savo administratoriui, kad jis naujovintų į mūsų Professional arba Enterprise planą.",
|
||||
"ViewLimitDialog.UpgradeImg.AltText": "atnaujinti vaizdą",
|
||||
"ViewLimitDialog.notifyAdmin.Success": "Jūsų administratoriui buvo pranešta",
|
||||
"ViewTitle.hide-description": "slėpti aprašymą",
|
||||
"ViewTitle.pick-icon": "Pasirinkti piktogramą",
|
||||
"ViewTitle.random-icon": "Atsitiktinis",
|
||||
"ViewTitle.remove-icon": "Pašalinti piktogramą",
|
||||
"ViewTitle.show-description": "rodyti aprašymą",
|
||||
"ViewTitle.untitled-board": "Lenta be pavadinimo",
|
||||
"WelcomePage.Description": "„Boards“ yra projektų valdymo įrankis, padedantis apibrėžti, organizuoti, sekti ir valdyti darbą įvairiose komandose, naudojant pažįstamą Kanban lentos vaizdą.",
|
||||
"WelcomePage.Explore.Button": "Apžiūrėti",
|
||||
"WelcomePage.Heading": "Sveiki atvykę į Boards",
|
||||
"WelcomePage.NoThanks.Text": "Ne, ačiū, išsiaiškinsiu pats",
|
||||
"WelcomePage.StartUsingIt.Text": "Pradėkite jį naudoti",
|
||||
"Workspace.editing-board-template": "Redaguojate lentos šabloną.",
|
||||
"badge.guest": "Svečias",
|
||||
"boardPage.confirm-join-button": "Prisijungti",
|
||||
"boardPage.confirm-join-text": "Jūs ketinate prisijungti prie privačios lentos, tačiau lentos administratorius Jūsų nepridėjo. Ar tikrai norite prisijungti prie šios privačios lentos?",
|
||||
"boardPage.confirm-join-title": "Prisijunti prie privačios lentos",
|
||||
"boardSelector.confirm-link-board": "Susieti lentą su kanalu",
|
||||
"boardSelector.confirm-link-board-button": "Taip, susieti lentą",
|
||||
"boardSelector.confirm-link-board-subtext": "Kai susiesite „{boardName}“ su kanalu, visi kanalo nariai (esami ir nauji) galės ją redaguoti. Tai neįtraukia narių, kurie yra svečiai. Galite bet kada atsieti lentą nuo kanalo.",
|
||||
"boardSelector.confirm-link-board-subtext-with-other-channel": "Kai susiesite „{boardName}“ su kanalu, visi kanalo nariai (esami ir nauji) galės ją redaguoti. Tai neįtraukia narių, kurie yra svečiai.{lineBreak} Ši lenta šiuo metu susieta su kitu kanalu. Jis bus atsietas, jei pasirinksite susieti jį čia.",
|
||||
"boardSelector.create-a-board": "Sukurti lentą",
|
||||
"boardSelector.link": "nuoroda",
|
||||
"boardSelector.search-for-boards": "Ieškoti lentų",
|
||||
"boardSelector.title": "Susieti lentas",
|
||||
"boardSelector.unlink": "Atsieti",
|
||||
"calendar.month": "Mėnuo",
|
||||
"calendar.today": "Šiandien",
|
||||
"calendar.week": "Savaitė",
|
||||
"centerPanel.undefined": "Nėra {propertyName}",
|
||||
"centerPanel.unknown-user": "Nežinomas naudotojas",
|
||||
"createImageBlock.failed": "Šio failo nepavyko įkelti, nes pasiektas failo dydžio limitas.",
|
||||
"default-properties.badges": "Komentarai ir aprašymas",
|
||||
"default-properties.title": "Pavadinimas",
|
||||
"error.back-to-home": "Grįžti namo",
|
||||
"error.back-to-team": "Atgal į komandą",
|
||||
"error.board-not-found": "Lenta nerasta.",
|
||||
"error.go-login": "Prisijungti",
|
||||
"error.invalid-read-only-board": "Neturite prieigos prie šios lentos. Prisijunkite, kad pasiektumėte lentas.",
|
||||
"error.not-logged-in": "Jūsų sesija gali būti pasibaigusi arba nesate prisijungę. Prisijunkite dar kartą, kad pasiektumėte lentas.",
|
||||
"error.page.title": "Atsiprašome, kažkas nutiko",
|
||||
"error.team-undefined": "Netinkama komanda.",
|
||||
"error.unknown": "Įvyko klaida.",
|
||||
"generic.previous": "Ankstesnis",
|
||||
"guest-no-board.subtitle": "Dar neturite prieigos prie jokios šios komandos lentos, palaukite, kol kas nors jus įtrauks į bet kurią lentą.",
|
||||
"guest-no-board.title": "Dar nėra jokių lentų",
|
||||
"imagePaste.upload-failed": "Kai kurie failai nebuvo įkelti, nes pasiektas failo dydžio limitas.",
|
||||
"limitedCard.title": "Paslėptos kortelės",
|
||||
"login.log-in-button": "Prisijungti",
|
||||
"login.log-in-title": "Prisijungti",
|
||||
"login.register-button": "arba susikurkite paskyrą, jei jos neturite",
|
||||
"new_channel_modal.create_board.empty_board_description": "Sukurti naują tuščią lentą",
|
||||
"new_channel_modal.create_board.empty_board_title": "Tuščia lenta",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "Pasirinkti šabloną",
|
||||
"new_channel_modal.create_board.title": "Sukurti šio kanalo lentą",
|
||||
"notification-box-card-limit-reached.close-tooltip": "Snausti 10 dienų",
|
||||
"notification-box-card-limit-reached.contact-link": "praneškite savo administratoriui",
|
||||
"notification-box-card-limit-reached.link": "Atnaujinti į mokamą planą",
|
||||
"notification-box-card-limit-reached.title": "{cards} kortelės paslėptos",
|
||||
"notification-box-cards-hidden.title": "Šis veiksmas paslėpė kitą kortelę",
|
||||
"notification-box.card-limit-reached.not-admin.text": "Norėdami pasiekti archyvuotas korteles, galite {contactLink} naujovinti į mokamą planą.",
|
||||
"notification-box.card-limit-reached.text": "Pasiektas kortelių limitas, jei norite peržiūrėti senesnes korteles, {link}",
|
||||
"person.add-user-to-board": "Pridėti {username} prie lentos",
|
||||
"person.add-user-to-board-confirm-button": "Pridėti prie lentos",
|
||||
"person.add-user-to-board-permissions": "Leidimai",
|
||||
"person.add-user-to-board-question": "Ar norite pridėti {username} prie lentos?",
|
||||
"person.add-user-to-board-warning": "{username} nėra lentos narys ir negaus apie tai jokių pranešimų.",
|
||||
"register.login-button": "arba prisijunkite, jei jau turite paskyrą",
|
||||
"register.signup-title": "Prisiregistruokite prie paskyros",
|
||||
"rhs-board-non-admin-msg": "Jūs nesate lentos administratorius",
|
||||
"rhs-boards.add": "Pridėti",
|
||||
"rhs-boards.dm": "AŽ",
|
||||
"rhs-boards.gm": "GŽ",
|
||||
"rhs-boards.header.dm": "ši tiesioginė žinutė",
|
||||
"rhs-boards.header.gm": "ši grupės žinutė",
|
||||
"rhs-boards.last-update-at": "Paskutinį kartą atnaujinta: {datetime}",
|
||||
"rhs-boards.link-boards-to-channel": "Susieti lentas su {channelName}",
|
||||
"rhs-boards.linked-boards": "Susietos lentos",
|
||||
"rhs-boards.no-boards-linked-to-channel": "Su {channelName} dar nėra susietų lentų",
|
||||
"rhs-boards.no-boards-linked-to-channel-description": "„Boards“ yra projektų valdymo įrankis, padedantis apibrėžti, organizuoti, sekti ir valdyti darbą įvairiose komandose, naudojant pažįstamą Kanban lentos vaizdą.",
|
||||
"rhs-boards.unlink-board": "Atsieti lentą",
|
||||
"rhs-boards.unlink-board1": "Atsieti lentą",
|
||||
"rhs-channel-boards-header.title": "Boards",
|
||||
"share-board.publish": "Paskelbti",
|
||||
"share-board.share": "Bendrinti",
|
||||
"shareBoard.channels-select-group": "Kanalai",
|
||||
"shareBoard.confirm-change-team-role.body": "Visi šioje lentoje esantys asmenys, turintys žemesnį nei „{role}“ vaidmenį <b>, dabar bus paaukštinti į {role}</b> . Ar tikrai norite pakeisti minimalų lentos vaidmenį?",
|
||||
"shareBoard.confirm-change-team-role.confirmBtnText": "Pakeiskite minimalų lentos vaidmenį",
|
||||
"shareBoard.confirm-change-team-role.title": "Pakeiskite minimalų lentos vaidmenį",
|
||||
"shareBoard.confirm-link-channel": "Susieti lentą su kanalu",
|
||||
"shareBoard.confirm-link-channel-button": "Susieti kanalą",
|
||||
"shareBoard.confirm-link-channel-button-with-other-channel": "Atsieti ir susieti čia",
|
||||
"shareBoard.confirm-link-channel-subtext": "Kai susiesite kanalą su lenta, visi kanalo nariai (esami ir nauji) galės ją redaguoti. Tai neįtraukia narių, kurie yra svečiai.",
|
||||
"shareBoard.confirm-link-channel-subtext-with-other-channel": "Kai susiesite kanalą su lenta, visi kanalo nariai (esami ir nauji) galės jį redaguoti. Tai neįtraukia narių, kurie yra svečiai.{lineBreak}Ši lenta šiuo metu susieta su kitu kanalu. Jis bus atsietas, jei pasirinksite susieti jį čia.",
|
||||
"shareBoard.confirm-unlink.body": "Kai atsiesite kanalą nuo lentos, visi kanalo nariai (esami ir nauji) praras prieigą prie jo, nebent jiems bus suteiktas atskiras leidimas.",
|
||||
"shareBoard.confirm-unlink.confirmBtnText": "Atsieti kanalą",
|
||||
"shareBoard.confirm-unlink.title": "Atsieti kanalą nuo lentos",
|
||||
"shareBoard.lastAdmin": "Lentose turi būti bent vienas administratorius",
|
||||
"shareBoard.members-select-group": "Nariai",
|
||||
"shareBoard.unknown-channel-display-name": "Nežinomas kanalas",
|
||||
"tutorial_tip.finish_tour": "Atlikta",
|
||||
"tutorial_tip.got_it": "Supratau",
|
||||
"tutorial_tip.ok": "Kitas",
|
||||
"tutorial_tip.out": "Atsisakyti šių patarimų.",
|
||||
"tutorial_tip.seen": "Matėte tai anksčiau?"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"AdminBadge.SystemAdmin": "Beheerder",
|
||||
"AdminBadge.TeamAdmin": "Teambeheerder",
|
||||
"AppBar.Tooltip": "Gekoppelde borden weergeven",
|
||||
"Attachment.Attachment-title": "Bijlage",
|
||||
"AttachmentBlock.DeleteAction": "verwijderen",
|
||||
@@ -137,6 +139,7 @@
|
||||
"ContentBlock.moveDown": "Naar beneden verplaatsen",
|
||||
"ContentBlock.moveUp": "Naar boven verplaatsen",
|
||||
"ContentBlock.text": "tekst",
|
||||
"DateFilter.empty": "Leeg",
|
||||
"DateRange.clear": "Wissen",
|
||||
"DateRange.empty": "Leeg",
|
||||
"DateRange.endDate": "Einddatum",
|
||||
@@ -155,10 +158,14 @@
|
||||
"Filter.ends-with": "eindigt met",
|
||||
"Filter.includes": "bevat",
|
||||
"Filter.is": "is",
|
||||
"Filter.is-after": "is na",
|
||||
"Filter.is-before": "is voor",
|
||||
"Filter.is-empty": "is leeg",
|
||||
"Filter.is-not-empty": "is niet leeg",
|
||||
"Filter.is-not-set": "is niet ingesteld",
|
||||
"Filter.is-set": "is ingesteld",
|
||||
"Filter.isafter": "is na",
|
||||
"Filter.isbefore": "is voor",
|
||||
"Filter.not-contains": "bevat niet",
|
||||
"Filter.not-ends-with": "eindigt niet met",
|
||||
"Filter.not-includes": "bevat niet",
|
||||
@@ -305,6 +312,7 @@
|
||||
"ValueSelector.valueSelector": "Waardekiezer",
|
||||
"ValueSelectorLabel.openMenu": "Menu openen",
|
||||
"VersionMessage.help": "Bekijk eens wat nieuw is in deze versie.",
|
||||
"VersionMessage.learn-more": "Meer info",
|
||||
"View.AddView": "Weergave toevoegen",
|
||||
"View.Board": "Bord",
|
||||
"View.DeleteView": "Weergave verwijderen",
|
||||
@@ -359,6 +367,9 @@
|
||||
"WelcomePage.StartUsingIt.Text": "Ga het gebruiken",
|
||||
"Workspace.editing-board-template": "Je bent een bordsjabloon aan het bewerken.",
|
||||
"badge.guest": "Gast",
|
||||
"boardPage.confirm-join-button": "Word lid",
|
||||
"boardPage.confirm-join-text": "Je staat op het punt lid te worden van een privé-bord zonder dat je expliciet bent toegevoegd door de bordbeheerder. Weet je zeker dat je lid wilt worden van dit privé-bord?",
|
||||
"boardPage.confirm-join-title": "Word lid van het privé-bord",
|
||||
"boardSelector.confirm-link-board": "Koppel bord aan kanaal",
|
||||
"boardSelector.confirm-link-board-button": "Ja, koppel het bord",
|
||||
"boardSelector.confirm-link-board-subtext": "Wanneer je \"{boardName}\" aan het kanaal koppelt, kunnen alle leden van het kanaal (bestaande en nieuwe) het bewerken. Dit sluit leden die gast zijn uit. Je kan de koppeling van een bord naar een kanaal op elk moment ongedaan maken.",
|
||||
|
||||
@@ -63,12 +63,15 @@
|
||||
"CardDetail.limited-button": "Atualizar",
|
||||
"CardDetail.new-comment-placeholder": "Adicionar um comentário...",
|
||||
"CardDetailProperty.delete-action-button": "Apagar",
|
||||
"CardDetailProperty.property-change-action-button": "Alterar propriedade",
|
||||
"CardDetailProperty.property-changed": "Propriedade alterada com sucesso!",
|
||||
"CardDialog.delete-confirmation-dialog-button-text": "Apagar",
|
||||
"Categories.CreateCategoryDialog.CancelText": "Cancelar",
|
||||
"Categories.CreateCategoryDialog.CreateText": "Criar",
|
||||
"Categories.CreateCategoryDialog.UpdateText": "Atualizar",
|
||||
"CenterPanel.Login": "Entrar",
|
||||
"CenterPanel.Share": "Compartilhar",
|
||||
"ChannelIntro.CreateBoard": "Criar um quadro",
|
||||
"Comment.delete": "Apagar",
|
||||
"CommentsList.send": "Enviar",
|
||||
"ConfirmPerson.empty": "Vazio",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"AdminBadge.SystemAdmin": "Администратор",
|
||||
"AdminBadge.TeamAdmin": "Администратор Команды",
|
||||
"AppBar.Tooltip": "Переключить связанные доски",
|
||||
"Attachment.Attachment-title": "Вложение",
|
||||
"AttachmentBlock.DeleteAction": "Удалить",
|
||||
@@ -137,6 +139,7 @@
|
||||
"ContentBlock.moveDown": "Опустить",
|
||||
"ContentBlock.moveUp": "Поднять",
|
||||
"ContentBlock.text": "текст",
|
||||
"DateFilter.empty": "Пусто",
|
||||
"DateRange.clear": "Очистить",
|
||||
"DateRange.empty": "Пусто",
|
||||
"DateRange.endDate": "Дата окончания",
|
||||
@@ -155,10 +158,14 @@
|
||||
"Filter.ends-with": "заканчивается",
|
||||
"Filter.includes": "содержит",
|
||||
"Filter.is": "является",
|
||||
"Filter.is-after": "после",
|
||||
"Filter.is-before": "раньше",
|
||||
"Filter.is-empty": "пусто",
|
||||
"Filter.is-not-empty": "не пусто",
|
||||
"Filter.is-not-set": "не установлен",
|
||||
"Filter.is-set": "установлен",
|
||||
"Filter.isafter": "после",
|
||||
"Filter.isbefore": "раньше",
|
||||
"Filter.not-contains": "не содержит",
|
||||
"Filter.not-ends-with": "не заканчивается",
|
||||
"Filter.not-includes": "не содержит",
|
||||
@@ -197,6 +204,7 @@
|
||||
"OnboardingTour.ShareBoard.Body": "Вы можете поделиться своей доской внутри своей команды или опубликовать ее для общего доступа за пределами Вашей организации.",
|
||||
"OnboardingTour.ShareBoard.Title": "Поделиться доской",
|
||||
"PersonProperty.board-members": "Совет директоров",
|
||||
"PersonProperty.me": "Я",
|
||||
"PersonProperty.non-board-members": "Не члены правления",
|
||||
"PropertyMenu.Delete": "Удалить",
|
||||
"PropertyMenu.changeType": "Изменить тип свойства",
|
||||
@@ -304,6 +312,7 @@
|
||||
"ValueSelector.valueSelector": "Выбор значения",
|
||||
"ValueSelectorLabel.openMenu": "Открыть меню",
|
||||
"VersionMessage.help": "Узнайте, что нового в этой версии.",
|
||||
"VersionMessage.learn-more": "Узнать больше",
|
||||
"View.AddView": "Добавить вид",
|
||||
"View.Board": "Доска",
|
||||
"View.DeleteView": "Удалить вид",
|
||||
@@ -358,6 +367,9 @@
|
||||
"WelcomePage.StartUsingIt.Text": "Начать пользоваться",
|
||||
"Workspace.editing-board-template": "Вы редактируете шаблон доски.",
|
||||
"badge.guest": "Гость",
|
||||
"boardPage.confirm-join-button": "Присоединиться",
|
||||
"boardPage.confirm-join-text": "Вы собираетесь присоединиться к закрытой доске без явного добавления администратором доски. Вы уверены, что хотите присоединиться к этой закрытой доске?",
|
||||
"boardPage.confirm-join-title": "Присоединиться к приватной доске",
|
||||
"boardSelector.confirm-link-board": "Привязать доску к каналу",
|
||||
"boardSelector.confirm-link-board-button": "Да, ссылка доски",
|
||||
"boardSelector.confirm-link-board-subtext": "Связывание доски \"{boardName}\" с каналом даст всем участникам канала доступ на редактирование доски. Вы можете в любое время отвязать доску о канала.",
|
||||
@@ -386,6 +398,8 @@
|
||||
"error.team-undefined": "Не корректная команда.",
|
||||
"error.unknown": "Произошла ошибка.",
|
||||
"generic.previous": "Предыдущий",
|
||||
"guest-no-board.subtitle": "У вас пока нет доступа ни к одной доске в этой команде, пожалуйста, подождите, пока кто-нибудь не добавит вас к любой из досок.",
|
||||
"guest-no-board.title": "Пока нет досок",
|
||||
"imagePaste.upload-failed": "Некоторые файлы не загружены из-за превышения квоты на размер файла.",
|
||||
"limitedCard.title": "Карточки скрыты",
|
||||
"login.log-in-button": "Вход в систему",
|
||||
@@ -406,22 +420,40 @@
|
||||
"person.add-user-to-board-confirm-button": "Добавить доску",
|
||||
"person.add-user-to-board-permissions": "Разрешения",
|
||||
"person.add-user-to-board-question": "Вы хотите добавить {username} на доску?",
|
||||
"person.add-user-to-board-warning": "{username} не является участником доски и не получает никаких уведомлений о ней.",
|
||||
"register.login-button": "или войти в систему, если у вас уже есть аккаунт",
|
||||
"register.signup-title": "Зарегистрируйте свой аккаунт",
|
||||
"rhs-board-non-admin-msg": "Вы не являетесь администратором этой доски",
|
||||
"rhs-boards.add": "Добавить",
|
||||
"rhs-boards.dm": "ЛС",
|
||||
"rhs-boards.gm": "GM",
|
||||
"rhs-boards.header.dm": "это личное сообщение",
|
||||
"rhs-boards.header.gm": "это групповое сообщение",
|
||||
"rhs-boards.last-update-at": "Последнее обновление: {datetime}",
|
||||
"rhs-boards.link-boards-to-channel": "Связать доски с {channelName}",
|
||||
"rhs-boards.linked-boards": "Связанные доски",
|
||||
"rhs-boards.no-boards-linked-to-channel": "К каналу {channelName} пока не подключены доски",
|
||||
"rhs-boards.no-boards-linked-to-channel-description": "Доски — это инструмент управления проектами, который помогает определять, организовывать, отслеживать и управлять работой между командами, используя знакомое представление доски Канбан.",
|
||||
"rhs-boards.unlink-board": "Отвязать доску",
|
||||
"rhs-boards.unlink-board1": "Отвязать доску",
|
||||
"rhs-channel-boards-header.title": "Доски",
|
||||
"share-board.publish": "Опубликовать",
|
||||
"share-board.share": "Поделиться",
|
||||
"shareBoard.channels-select-group": "Каналы",
|
||||
"shareBoard.confirm-change-team-role.body": "Все на этой доске с правами ниже, чем роль \"{role}\" <b>теперь будут повышены до {role}</b>. Вы уверены, что хотите изменить минимальную роль для доски?",
|
||||
"shareBoard.confirm-change-team-role.confirmBtnText": "Изменение минимальной роли доски",
|
||||
"shareBoard.confirm-change-team-role.title": "Изменение минимальной роли доски",
|
||||
"shareBoard.confirm-link-channel": "Привязать доску к каналу",
|
||||
"shareBoard.confirm-link-channel-button": "Привязать канал",
|
||||
"shareBoard.confirm-link-channel-button-with-other-channel": "Отвязать и привязать это",
|
||||
"shareBoard.confirm-link-channel-subtext": "Когда вы связываете канал с доской, все участники канала (существующие и новые) смогут редактировать его. За исключением пользователей, которые являются гостями.",
|
||||
"shareBoard.confirm-link-channel-subtext-with-other-channel": "Когда вы связываете канал с доской, все участники канала (существующие и новые) смогут редактировать его. За исключением пользователей, которые являются гостями.{lineBreak}Эта доска в настоящее время связана с другим каналом. Она будет удалена, если вы решите связать ее здесь.",
|
||||
"shareBoard.confirm-unlink.body": "Когда вы отвязываете канал от доски, все участники канала (как существующие, так и новые) теряют к нему доступ, если им не дано отдельное разрешение.",
|
||||
"shareBoard.confirm-unlink.confirmBtnText": "Отвязать канал",
|
||||
"shareBoard.confirm-unlink.title": "Отвязать канал от доски",
|
||||
"shareBoard.lastAdmin": "Доски должны иметь хотя бы одного администратора",
|
||||
"shareBoard.members-select-group": "Участники",
|
||||
"shareBoard.unknown-channel-display-name": "Неизвестный канал",
|
||||
"tutorial_tip.finish_tour": "Готово",
|
||||
"tutorial_tip.got_it": "Понятно",
|
||||
"tutorial_tip.ok": "Следующий",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"AppBar.Tooltip": "切换链接的板块",
|
||||
"AppBar.Tooltip": "切换已链接的板块",
|
||||
"Attachment.Attachment-title": "附件",
|
||||
"AttachmentBlock.DeleteAction": "删除",
|
||||
"AttachmentBlock.addElement": "添加 {type}",
|
||||
@@ -137,6 +137,7 @@
|
||||
"ContentBlock.moveDown": "下移",
|
||||
"ContentBlock.moveUp": "上移",
|
||||
"ContentBlock.text": "文字",
|
||||
"DateFilter.empty": "空",
|
||||
"DateRange.clear": "清除",
|
||||
"DateRange.empty": "空的",
|
||||
"DateRange.endDate": "结束日期",
|
||||
@@ -305,6 +306,7 @@
|
||||
"ValueSelector.valueSelector": "值选择器",
|
||||
"ValueSelectorLabel.openMenu": "打开菜单",
|
||||
"VersionMessage.help": "了解查看新版本有什么新特性。",
|
||||
"VersionMessage.learn-more": "了解更多",
|
||||
"View.AddView": "添加视图",
|
||||
"View.Board": "板块",
|
||||
"View.DeleteView": "删除视图",
|
||||
@@ -359,6 +361,7 @@
|
||||
"WelcomePage.StartUsingIt.Text": "开始使用",
|
||||
"Workspace.editing-board-template": "您正在编辑版面模板。",
|
||||
"badge.guest": "访客",
|
||||
"boardPage.confirm-join-button": "加入",
|
||||
"boardSelector.confirm-link-board": "连接板块到频道",
|
||||
"boardSelector.confirm-link-board-button": "是的,连接板块",
|
||||
"boardSelector.confirm-link-board-subtext": "当你连接“{boardName}”到频道时,所有频道的成员(现有的或新的)都可以编辑。这并不包括访客。你随时都可以取消板块与频道的连接。",
|
||||
|
||||
@@ -7,7 +7,7 @@ const config = {
|
||||
transform: {
|
||||
"^.+\\.(t|j)sx?$": ["@swc/jest"]
|
||||
},
|
||||
moduleFileExtensions: [
|
||||
moduleFileExtensions: [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
@@ -20,7 +20,6 @@ const config = {
|
||||
"/nanoevents/",
|
||||
"node_modules/(?!react-native|react-router|react-day-picker)"
|
||||
],
|
||||
maxWorkers: "80%",
|
||||
testEnvironment: "jsdom",
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "boards",
|
||||
"version": "7.10.0",
|
||||
"version": "7.9.1",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"i18n-extract": "formatjs extract \"src/**/*.{ts,tsx}\" --ignore \"**/*.d.ts\" \"../**/*.d.ts\" --out-file i18n/tmp.json && formatjs compile i18n/tmp.json --out-file i18n/en.json && npx rimraf i18n/tmp.json",
|
||||
"build": "webpack --mode=production",
|
||||
"build:watch": "webpack --mode=production --watch",
|
||||
"start:product": "webpack serve --mode=development",
|
||||
@@ -18,6 +17,7 @@
|
||||
"check-types": "tsc -b",
|
||||
"check-types:fix": "npm run check-types -- --noEmit --fix",
|
||||
"check": "npm run check-lint && npm run check-style",
|
||||
"i18n-extract": "formatjs extract \"src/**/*.{ts,tsx}\" --ignore \"**/*.d.ts\" --id-interpolation-pattern '[sha512:contenthash:base64:6]' --format simple --out-file i18n/en.json",
|
||||
"fix": "npm run check-lint:fix && npm run check-style:fix",
|
||||
"test": "cross-env TZ=Etc/UTC jest",
|
||||
"test:watch": "cross-env TZ=Etc/UTC jest --watch",
|
||||
@@ -46,7 +46,6 @@
|
||||
"glob-parent": "6.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "4.0.17",
|
||||
"mini-create-react-context": "^0.4.1",
|
||||
"moment": "^2.29.1",
|
||||
"nanoevents": "^5.1.13",
|
||||
"react": "17.0.2",
|
||||
@@ -59,7 +58,7 @@
|
||||
"react-dom": "17.0.2",
|
||||
"react-hot-keys": "^2.7.1",
|
||||
"react-hotkeys-hook": "^3.4.4",
|
||||
"react-intl": "^5.20.0",
|
||||
"react-intl": "*",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "5.5.9",
|
||||
@@ -96,7 +95,6 @@
|
||||
"@types/node": "16.11.7",
|
||||
"@types/react": "17.0.53",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-day-picker": "5.3.0",
|
||||
"@types/react-dom": "17.0.19",
|
||||
"@types/react-redux": "^7.1.23",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
@@ -105,11 +103,9 @@
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"babel-eslint": "10.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "6.7.1",
|
||||
"eslint-import-resolver-webpack": "0.13.2",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-formatjs": "4.9.0",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-import-newlines": "1.3.1",
|
||||
|
||||
@@ -39,7 +39,7 @@ const BoardSwitcherDialog = (props: Props): JSX.Element => {
|
||||
const team = useAppSelector(getCurrentTeam)
|
||||
const me = useAppSelector(getMe)
|
||||
const title = intl.formatMessage({id: 'FindBoardsDialog.Title', defaultMessage: 'Find Boards'})
|
||||
const subTitle = intl.formatMessage(
|
||||
const subTitle = intl.formatMessage<ReactNode>(
|
||||
{
|
||||
id: 'FindBoardsDialog.SubTitle',
|
||||
defaultMessage: 'Type to find a board. Use <b>UP/DOWN</b> to browse. <b>ENTER</b> to select, <b>ESC</b> to dismiss',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState} from 'react'
|
||||
import React, {ReactNode, useState} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import MenuWrapper from 'src/widgets/menuWrapper'
|
||||
@@ -77,7 +77,7 @@ const TeamPermissionsRow = (): JSX.Element => {
|
||||
id: 'shareBoard.confirm-change-team-role.title',
|
||||
defaultMessage: 'Change minimum board role',
|
||||
}),
|
||||
subText: intl.formatMessage({
|
||||
subText: intl.formatMessage<ReactNode>({
|
||||
id: 'shareBoard.confirm-change-team-role.body',
|
||||
defaultMessage: 'Everyone on this board with a lower permission than the "{role}" role will <b>now be promoted to {role}</b>. Are you sure you want to change the minimum role for the board?',
|
||||
}, {
|
||||
|
||||
@@ -193,7 +193,10 @@ describe('components/sidebarCategory', () => {
|
||||
expect(mockTemplateClose).toBeCalled()
|
||||
})
|
||||
|
||||
test('sidebar template close other', async () => {
|
||||
// TODO: Remove when fetch is mocked correctly
|
||||
// https://mattermost.atlassian.net/browse/MM-52212
|
||||
// eslint-disable-next-line no-only-tests/no-only-tests
|
||||
test.skip('sidebar template close other', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore(state)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -167,7 +168,7 @@ const SidebarCategory = (props: Props) => {
|
||||
id: 'SidebarCategories.CategoryMenu.DeleteModal.Title',
|
||||
defaultMessage: 'Delete this category?',
|
||||
}),
|
||||
subText: intl.formatMessage(
|
||||
subText: intl.formatMessage<ReactNode>(
|
||||
{
|
||||
id: 'SidebarCategories.CategoryMenu.DeleteModal.Body',
|
||||
defaultMessage: 'Boards in <b>{categoryName}</b> will move back to the Boards categories. You\'re not removed from any boards.',
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
}
|
||||
],
|
||||
"max-lines": ["warn", {"max": 800, "skipBlankLines": true, "skipComments": true}],
|
||||
"formatjs/no-multiple-whitespaces": ["error"]
|
||||
"formatjs/no-multiple-whitespaces": 2
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
# Mattermost Web App
|
||||
# [](https://mattermost.com)
|
||||
|
||||
[Mattermost](https://mattermost.com) is an open source platform for secure collaboration across the entire software development lifecycle. This repo is the primary source for core development on the Mattermost platform; it's written in Golang and React, and runs as a single Linux binary with MySQL or PostgreSQL. A new compiled version is released under an MIT license every month on the 16th.
|
||||
|
||||
<img width="1006" alt="mattermost-hero" src="https://user-images.githubusercontent.com/7205829/136107976-7a894c9e-290a-490d-8501-e5fdbfc3785a.png">
|
||||
|
||||
This repo hosts the webapp client code. If you'd like to report an issue with Mattermost, please create an issue on [mattermost-server](https://github.com/mattermost/mattermost-server), which hosts the server code. You can also look at the [Get Help With Mattermost](https://docs.mattermost.com/guides/get-help.html) guide to find the resources available to our community.
|
||||
|
||||
- [Product documentation](https://docs.mattermost.com/)
|
||||
- [Developer documentation](https://developers.mattermost.com/)
|
||||
- [Download compiled version](https://mattermost.com/download)
|
||||
|
||||
## Try out Mattermost
|
||||
|
||||
- [Join the Mattermost Contributor's server](https://community.mattermost.com/signup_user_complete/?id=codoy5s743rq5mk18i7u5ksz7e) to join community discussions about contributions, development and more
|
||||
- [Get started with Mattermost Cloud](https://customers.mattermost.com/cloud/signup) to try out Mattermost
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/mattermost/mattermost-heroku)
|
||||
|
||||
_Note: Heroku preview does not include email or persistent storage._
|
||||
|
||||
## Install Mattermost
|
||||
|
||||
- [Deploy Guide](https://docs.mattermost.com/guides/deployment.html) - Deploy Mattermost in minutes via Docker, Ubuntu, or tar.
|
||||
- [Developer Machine Setup](https://developers.mattermost.com/contribute/server/developer-setup) - Follow this guide if you want to write code for Mattermost
|
||||
|
||||
Other Install Guides:
|
||||
- [Deploy Mattermost on Docker](https://docs.mattermost.com/install/install-docker.html)
|
||||
- [Mattermost Omnibus](https://docs.mattermost.com/install/installing-mattermost-omnibus.html)
|
||||
- [Install Mattermost from Tar](https://docs.mattermost.com/install/install-tar.html)
|
||||
- [Ubuntu 20.04 LTS](https://docs.mattermost.com/install/installing-ubuntu-2004-LTS.html)
|
||||
- [Kubernetes](https://docs.mattermost.com/install/install-kubernetes.html)
|
||||
- [Helm](https://docs.mattermost.com/install/install-kubernetes.html#installing-the-operators-via-helm)
|
||||
- [Debian Buster](https://docs.mattermost.com/install/install-debian.html)
|
||||
- [RHEL 8](https://docs.mattermost.com/install/install-rhel-8.html)
|
||||
- [More server install guides](https://docs.mattermost.com/guides/deployment.html)
|
||||
|
||||
## Get Involved
|
||||
|
||||
- [Contribute to Mattermost](https://handbook.mattermost.com/contributors/contributors/ways-to-contribute)
|
||||
- [Find "Help Wanted" projects](https://github.com/mattermost/mattermost-server/issues?page=1&q=is%3Aissue+is%3Aopen+%22Help+Wanted%22&utf8=%E2%9C%93)
|
||||
- [Join Developer Discussion on a Mattermost Server for contributors](https://docs.mattermost.com/guides/community-chat.html)
|
||||
- [Get Help With Mattermost](https://docs.mattermost.com/guides/get-help.html)
|
||||
|
||||
## Learn More
|
||||
|
||||
- [API Options - webhooks, slash commands, drivers and web service](https://api.mattermost.com/)
|
||||
- [Localization Guide](https://handbook.mattermost.com/contributors/contributors/localization)
|
||||
|
||||
## Get the Latest News
|
||||
|
||||
- **Twitter** - Follow [Mattermost](https://twitter.com/Mattermost)
|
||||
- **Email** - Subscribe to our [newsletter](https://mattermost.com/community-newsletter/)
|
||||
|
||||
Any other questions, mail us at [info@mattermost.com](mailto:info@mattermost.com). We'd love to meet you!
|
||||
@@ -13,4 +13,4 @@ test:
|
||||
before_script:
|
||||
- npm ci --ignore-scripts
|
||||
script:
|
||||
- npm run test:speed
|
||||
- npm run test
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"browser": {
|
||||
"./client/web_client.jsx": "./client/browser_web_client.jsx"
|
||||
},
|
||||
"version": "7.9.0",
|
||||
"version": "7.9.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "1.0.0",
|
||||
@@ -51,7 +51,7 @@
|
||||
"localforage": "1.10.0",
|
||||
"localforage-observable": "2.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.0.4",
|
||||
"luxon": "3.3.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "github:mattermost/marked#2ef7f28cc7718e3f551c4ce9ea75fdd7580c2008",
|
||||
"memoize-one": "6.0.0",
|
||||
@@ -70,7 +70,7 @@
|
||||
"react-day-picker": "8.3.6",
|
||||
"react-dom": "17.0.2",
|
||||
"react-hot-loader": "4.13.0",
|
||||
"react-intl": "5.20.10",
|
||||
"react-intl": "*",
|
||||
"react-is": "17.0.2",
|
||||
"react-overlays": "0.9.3",
|
||||
"react-popper": "2.3.0",
|
||||
@@ -116,6 +116,7 @@
|
||||
"@redux-devtools/extension": "3.2.3",
|
||||
"@testing-library/jest-dom": "5.16.4",
|
||||
"@testing-library/react": "12.1.4",
|
||||
"@testing-library/user-event": "12.1.4",
|
||||
"@types/bootstrap": "4.5.0",
|
||||
"@types/country-list": "2.1.0",
|
||||
"@types/enzyme": "3.10.11",
|
||||
@@ -155,14 +156,12 @@
|
||||
"babel-plugin-typescript-to-proptypes": "1.4.2",
|
||||
"bundle-loader": "0.2.0",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "5.2.6",
|
||||
"dotenv-webpack": "8.0.1",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-17-updated": "1.0.2",
|
||||
"enzyme-to-json": "3.6.2",
|
||||
"eslint-import-resolver-webpack": "0.13.2",
|
||||
"eslint-plugin-formatjs": "4.9.1",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#5b0c972eacf19286e4c66221b39113bf8728a99e",
|
||||
@@ -215,15 +214,13 @@
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"run": "webpack --progress --watch",
|
||||
"dev-server": "webpack serve --mode development",
|
||||
"test": "cross-env TZ=Etc/UTC jest --maxWorkers=50%",
|
||||
"test": "cross-env TZ=Etc/UTC jest",
|
||||
"test:watch": "cross-env TZ=Etc/UTC jest --watch",
|
||||
"test:updatesnapshot": "cross-env TZ=Etc/UTC jest --updateSnapshot",
|
||||
"test:debug": "cross-env TZ=Etc/UTC jest --forceExit --detectOpenHandles --verbose",
|
||||
"test-ci": "cross-env TZ=Etc/UTC jest --ci --maxWorkers=100%",
|
||||
"clean": "rm -rf dist node_modules .eslintcache .stylelintcache tsconfig.tsbuildinfo",
|
||||
"stats": "cross-env NODE_ENV=production webpack --profile --json > webpack_stats.json",
|
||||
"updatesnapshot": "cross-env TZ=Etc/UTC jest --updateSnapshot",
|
||||
"test:debug": "cross-env TZ=Etc/UTC jest --forceExit --detectOpenHandles",
|
||||
"test:speed": "cross-env TZ=Etc/UTC jest",
|
||||
"test:watch": "cross-env TZ=Etc/UTC jest --watch",
|
||||
"test:coverage": "cross-env TZ=Etc/UTC jest --coverage",
|
||||
"mmjstool": "mmjstool",
|
||||
"i18n-extract": "npm run mmjstool -- i18n extract-webapp --webapp-dir ./src",
|
||||
"i18n-clean-empty": "npm run mmjstool -- i18n clean-empty --webapp-dir ./src",
|
||||
|
||||
@@ -175,7 +175,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFunc {
|
||||
const errorResponse = res.error;
|
||||
return createErrorMessage(errorResponse.text || intlShim.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error.',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFunc {
|
||||
));
|
||||
}
|
||||
} catch (err: any) {
|
||||
return createErrorMessage(err.message || localizeMessage('apps.error.unknown', 'Unknown error.'));
|
||||
return createErrorMessage(err.message || localizeMessage('apps.error.unknown', 'Unknown error occurred.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ import {isSystemMessage, isUserAddedInChannel} from 'mattermost-redux/utils/post
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {isThreadOpen} from 'selectors/views/threads';
|
||||
import {getChannelURL, getPermalinkURL} from 'selectors/urls';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import Constants, {NotificationLevels, UserStatuses} from 'utils/constants';
|
||||
import * as NotificationSounds from 'utils/notification_sounds';
|
||||
import {showNotification} from 'utils/notifications';
|
||||
import {isDesktopApp, isMobileApp, isWindowsApp} from 'utils/user_agent';
|
||||
import * as Utils from 'utils/utils';
|
||||
@@ -178,17 +180,17 @@ export function sendDesktopNotification(post, msgProps) {
|
||||
|
||||
if (notify) {
|
||||
const updatedState = getState();
|
||||
let url = Utils.getChannelURL(updatedState, channel, teamId);
|
||||
let url = getChannelURL(updatedState, channel, teamId);
|
||||
|
||||
if (isCrtReply) {
|
||||
url = Utils.getPermalinkURL(updatedState, teamId, post.id);
|
||||
url = getPermalinkURL(updatedState, teamId, post.id);
|
||||
}
|
||||
|
||||
dispatch(notifyMe(title, body, channel, teamId, !sound, soundName, url));
|
||||
|
||||
//Don't add extra sounds on native desktop clients
|
||||
if (sound && !isDesktopApp() && !isMobileApp()) {
|
||||
Utils.ding(soundName);
|
||||
NotificationSounds.ding(soundName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@ import testConfigureStore from 'tests/test_store';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import Constants, {NotificationLevels, UserStatuses} from 'utils/constants';
|
||||
import * as NotificationSounds from 'utils/notification_sounds';
|
||||
import * as utils from 'utils/notifications';
|
||||
import * as baseUtils from 'utils/utils';
|
||||
|
||||
import {sendDesktopNotification} from './notification_actions';
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('notification_actions', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
spy = jest.spyOn(utils, 'showNotification');
|
||||
baseUtils.ding = jest.fn();
|
||||
NotificationSounds.ding = jest.fn();
|
||||
|
||||
crt = {
|
||||
user_id: 'current_user_id',
|
||||
@@ -315,7 +315,7 @@ describe('notification_actions', () => {
|
||||
});
|
||||
|
||||
test('should default sound when no sound is specified', () => {
|
||||
const dingSpy = jest.spyOn(baseUtils, 'ding');
|
||||
const dingSpy = jest.spyOn(NotificationSounds, 'ding');
|
||||
baseState.entities.users.profiles.current_user_id.notify_props.desktop_sound = 'true';
|
||||
const store = testConfigureStore(baseState);
|
||||
return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
|
||||
@@ -324,7 +324,7 @@ describe('notification_actions', () => {
|
||||
});
|
||||
|
||||
test('should use specified sound when specified', () => {
|
||||
const dingSpy = jest.spyOn(baseUtils, 'ding');
|
||||
const dingSpy = jest.spyOn(NotificationSounds, 'ding');
|
||||
baseState.entities.users.profiles.current_user_id.notify_props.desktop_sound = 'true';
|
||||
baseState.entities.users.profiles.current_user_id.notify_props.desktop_notification_sound = 'Crackle';
|
||||
const store = testConfigureStore(baseState);
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
makeOnSubmit,
|
||||
makeOnEditLatestPost,
|
||||
} from 'actions/views/create_comment';
|
||||
import {removeDraft} from 'actions/views/drafts';
|
||||
import {removeDraft, setGlobalDraftSource} from 'actions/views/drafts';
|
||||
import {setGlobalItem, actionOnGlobalItemsWithPrefix} from 'actions/storage';
|
||||
import * as PostActions from 'actions/post_actions';
|
||||
import {executeCommand} from 'actions/command';
|
||||
@@ -205,12 +205,13 @@ describe('rhs view actions', () => {
|
||||
|
||||
const testStore = mockStore(initialState);
|
||||
|
||||
testStore.dispatch(setGlobalItem(`${StoragePrefixes.COMMENT_DRAFT}${rootId}`, {
|
||||
const expectedKey = `${StoragePrefixes.COMMENT_DRAFT}${rootId}`;
|
||||
testStore.dispatch(setGlobalItem(expectedKey, {
|
||||
...draft,
|
||||
createAt: 42,
|
||||
updateAt: 42,
|
||||
remote: false,
|
||||
}));
|
||||
testStore.dispatch(setGlobalDraftSource(expectedKey, false));
|
||||
|
||||
expect(store.getActions()).toEqual(testStore.getActions());
|
||||
jest.useRealTimers();
|
||||
|
||||
@@ -13,7 +13,7 @@ import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {removeDraft, updateDraft} from './drafts';
|
||||
import {removeDraft, setGlobalDraftSource, updateDraft} from './drafts';
|
||||
|
||||
jest.mock('mattermost-redux/client', () => {
|
||||
const original = jest.requireActual('mattermost-redux/client');
|
||||
@@ -146,12 +146,13 @@ describe('draft actions', () => {
|
||||
|
||||
const testStore = mockStore(initialState);
|
||||
|
||||
testStore.dispatch(setGlobalItem(StoragePrefixes.DRAFT + channelId, {
|
||||
const expectedKey = StoragePrefixes.DRAFT + channelId;
|
||||
testStore.dispatch(setGlobalItem(expectedKey, {
|
||||
...draft,
|
||||
createAt: 42,
|
||||
updateAt: 42,
|
||||
remote: false,
|
||||
}));
|
||||
testStore.dispatch(setGlobalDraftSource(expectedKey, false));
|
||||
|
||||
expect(store.getActions()).toEqual(testStore.getActions());
|
||||
jest.useRealTimers();
|
||||
|
||||
@@ -15,7 +15,7 @@ import {PostDraft} from 'types/store/draft';
|
||||
import {getGlobalItem} from 'selectors/storage';
|
||||
import {makeGetDrafts} from 'selectors/drafts';
|
||||
|
||||
import {StoragePrefixes} from 'utils/constants';
|
||||
import {ActionTypes, StoragePrefixes} from 'utils/constants';
|
||||
|
||||
import type {Draft as ServerDraft} from '@mattermost/types/drafts';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
@@ -101,11 +101,10 @@ export function updateDraft(key: string, value: PostDraft|null, rootId = '', sav
|
||||
...value,
|
||||
createAt: data.createAt || timestamp,
|
||||
updateAt: timestamp,
|
||||
remote: false,
|
||||
};
|
||||
}
|
||||
|
||||
dispatch(setGlobalItem(key, updatedValue));
|
||||
dispatch(setGlobalDraft(key, updatedValue, false));
|
||||
|
||||
if (syncedDraftsAreAllowedAndEnabled(state) && save && updatedValue) {
|
||||
const connectionId = getConnectionId(state);
|
||||
@@ -153,6 +152,24 @@ export function setDraftsTourTipPreference(initializationState: Record<string, b
|
||||
};
|
||||
}
|
||||
|
||||
export function setGlobalDraft(key: string, value: PostDraft|null, isRemote: boolean) {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
dispatch(setGlobalItem(key, value));
|
||||
dispatch(setGlobalDraftSource(key, isRemote));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function setGlobalDraftSource(key: string, isRemote: boolean) {
|
||||
return {
|
||||
type: ActionTypes.SET_DRAFT_SOURCE,
|
||||
data: {
|
||||
key,
|
||||
isRemote,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function transformServerDraft(draft: ServerDraft): Draft {
|
||||
let key: Draft['key'] = `${StoragePrefixes.DRAFT}${draft.channel_id}`;
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ import {
|
||||
} from 'mattermost-redux/actions/users';
|
||||
import {removeNotVisibleUsers} from 'mattermost-redux/actions/websocket';
|
||||
import {setGlobalItem} from 'actions/storage';
|
||||
import {transformServerDraft} from 'actions/views/drafts';
|
||||
import {setGlobalDraft, transformServerDraft} from 'actions/views/drafts';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getCurrentUser, getCurrentUserId, getUser, getIsManualStatusForUserId, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
@@ -89,7 +89,6 @@ import {getStandardAnalytics} from 'mattermost-redux/actions/admin';
|
||||
|
||||
import {fetchAppBindings, fetchRHSAppsBindings} from 'mattermost-redux/actions/apps';
|
||||
|
||||
import {getConnectionId} from 'selectors/general';
|
||||
import {getSelectedChannelId, getSelectedPost} from 'selectors/rhs';
|
||||
import {isThreadOpen, isThreadManuallyUnread} from 'selectors/views/threads';
|
||||
|
||||
@@ -1700,20 +1699,12 @@ function handlePostAcknowledgementRemoved(msg) {
|
||||
}
|
||||
|
||||
function handleUpsertDraftEvent(msg) {
|
||||
return async (doDispatch, doGetState) => {
|
||||
const state = doGetState();
|
||||
const connectionId = getConnectionId(state);
|
||||
|
||||
return async (doDispatch) => {
|
||||
const draft = JSON.parse(msg.data.draft);
|
||||
const {key, value} = transformServerDraft(draft);
|
||||
value.show = true;
|
||||
value.remote = false;
|
||||
|
||||
if (msg.broadcast.omit_connection_id !== connectionId) {
|
||||
value.remote = true;
|
||||
}
|
||||
|
||||
doDispatch(setGlobalItem(key, value));
|
||||
doDispatch(setGlobalDraft(key, value, true));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1722,7 +1713,11 @@ function handleDeleteDraftEvent(msg) {
|
||||
const draft = JSON.parse(msg.data.draft);
|
||||
const {key} = transformServerDraft(draft);
|
||||
|
||||
doDispatch(setGlobalItem(key, {message: '', fileInfos: [], uploadsInProgress: [], remote: true}));
|
||||
doDispatch(setGlobalItem(key, {
|
||||
message: '',
|
||||
fileInfos: [],
|
||||
uploadsInProgress: [],
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`components/FormattedMarkdownMessage should allow to disable links 1`] =
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={
|
||||
@@ -14,6 +15,7 @@ exports[`components/FormattedMarkdownMessage should allow to disable links 1`] =
|
||||
}
|
||||
}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
@@ -41,6 +43,7 @@ exports[`components/FormattedMarkdownMessage should backup to default 1`] = `
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={
|
||||
@@ -51,6 +54,7 @@ exports[`components/FormattedMarkdownMessage should backup to default 1`] = `
|
||||
}
|
||||
}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
@@ -72,6 +76,7 @@ exports[`components/FormattedMarkdownMessage should escape non-BR 1`] = `
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={
|
||||
@@ -82,6 +87,7 @@ exports[`components/FormattedMarkdownMessage should escape non-BR 1`] = `
|
||||
}
|
||||
}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
@@ -109,6 +115,7 @@ exports[`components/FormattedMarkdownMessage should render message 1`] = `
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={
|
||||
@@ -119,6 +126,7 @@ exports[`components/FormattedMarkdownMessage should render message 1`] = `
|
||||
}
|
||||
}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
@@ -140,6 +148,7 @@ exports[`components/FormattedMarkdownMessage values should work 1`] = `
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={
|
||||
@@ -150,6 +159,7 @@ exports[`components/FormattedMarkdownMessage values should work 1`] = `
|
||||
}
|
||||
}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<FormattedMarkdownMessage
|
||||
|
||||
@@ -34,7 +34,7 @@ exports[`components/SettingItemMin should match snapshot with active Save button
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}"
|
||||
id="setting_picture.help.profile"
|
||||
id="setting_picture.help.profile.example"
|
||||
values={
|
||||
Object {
|
||||
"max": 52428800,
|
||||
@@ -139,7 +139,7 @@ exports[`components/SettingItemMin should match snapshot with active Save button
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}"
|
||||
id="setting_picture.help.profile"
|
||||
id="setting_picture.help.profile.example"
|
||||
values={
|
||||
Object {
|
||||
"max": 52428800,
|
||||
@@ -254,7 +254,7 @@ exports[`components/SettingItemMin should match snapshot, on loading picture 1`]
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}"
|
||||
id="setting_picture.help.profile"
|
||||
id="setting_picture.help.profile.example"
|
||||
values={
|
||||
Object {
|
||||
"max": 52428800,
|
||||
@@ -381,7 +381,7 @@ exports[`components/SettingItemMin should match snapshot, profile picture on fil
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}"
|
||||
id="setting_picture.help.profile"
|
||||
id="setting_picture.help.profile.example"
|
||||
values={
|
||||
Object {
|
||||
"max": 52428800,
|
||||
@@ -496,7 +496,7 @@ exports[`components/SettingItemMin should match snapshot, profile picture on sou
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}"
|
||||
id="setting_picture.help.profile"
|
||||
id="setting_picture.help.profile.example"
|
||||
values={
|
||||
Object {
|
||||
"max": 52428800,
|
||||
@@ -623,7 +623,7 @@ exports[`components/SettingItemMin should match snapshot, team icon on file 1`]
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}"
|
||||
id="setting_picture.help.profile"
|
||||
id="setting_picture.help.profile.example"
|
||||
values={
|
||||
Object {
|
||||
"max": 52428800,
|
||||
@@ -792,7 +792,7 @@ exports[`components/SettingItemMin should match snapshot, team icon on source 1`
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}"
|
||||
id="setting_picture.help.profile"
|
||||
id="setting_picture.help.profile.example"
|
||||
values={
|
||||
Object {
|
||||
"max": 52428800,
|
||||
@@ -961,7 +961,7 @@ exports[`components/SettingItemMin should match snapshot, user icon on source 1`
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}"
|
||||
id="setting_picture.help.profile"
|
||||
id="setting_picture.help.profile.example"
|
||||
values={
|
||||
Object {
|
||||
"max": 52428800,
|
||||
|
||||
@@ -1,980 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/AboutBuildModal should match snapshot for cloud edition 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<AboutBuildModalCloud
|
||||
config={
|
||||
Object {
|
||||
"BuildDate": "21 January 2017",
|
||||
"BuildEnterpriseReady": "true",
|
||||
"BuildHash": "abcdef1234567890",
|
||||
"BuildHashEnterprise": "0123456789abcdef",
|
||||
"BuildNumber": "3.6.2",
|
||||
"PrivacyPolicyLink": "https://about.custom.com/privacy-policy/",
|
||||
"SQLDriverName": "Postgres",
|
||||
"SchemaVersion": "77",
|
||||
"TermsOfServiceLink": "https://about.custom.com/default-terms/",
|
||||
"Version": "3.6.0",
|
||||
}
|
||||
}
|
||||
doHide={[MockFunction]}
|
||||
license={
|
||||
Object {
|
||||
"Cloud": "true",
|
||||
"Company": "Mattermost Inc",
|
||||
"IsLicensed": "true",
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
show={true}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
exports[`components/AboutBuildModal should match snapshot for enterprise edition 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="aboutModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal about-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="aboutModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="About {appTitle}"
|
||||
id="about.title"
|
||||
values={
|
||||
Object {
|
||||
"appTitle": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="about-modal__content"
|
||||
>
|
||||
<div
|
||||
className="about-modal__logo"
|
||||
>
|
||||
<MattermostLogo />
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
className="about-modal__title"
|
||||
>
|
||||
<strong>
|
||||
Mattermost
|
||||
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Enterprise Edition"
|
||||
id="about.enterpriseEditione1"
|
||||
/>
|
||||
</strong>
|
||||
</h3>
|
||||
<p
|
||||
className="about-modal__subtitle pb-2"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Modern communication from behind your firewall."
|
||||
id="about.enterpriseEditionSt"
|
||||
/>
|
||||
</p>
|
||||
<div
|
||||
className="form-group less"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost Version:"
|
||||
id="about.version"
|
||||
/>
|
||||
<span
|
||||
id="versionString"
|
||||
>
|
||||
3.6.2
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database Schema Version:"
|
||||
id="about.dbversion"
|
||||
/>
|
||||
<span
|
||||
id="dbversionString"
|
||||
>
|
||||
77
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database:"
|
||||
id="about.database"
|
||||
/>
|
||||
Postgres
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Licensed to:"
|
||||
id="about.licensed"
|
||||
/>
|
||||
<Nbsp />
|
||||
Mattermost Inc
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__footer"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Learn more about Enterprise Edition at "
|
||||
id="about.enterpriseEditionLearn"
|
||||
/>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/"
|
||||
location="about_build_modal"
|
||||
>
|
||||
mattermost.com
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="about-modal__copyright"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
|
||||
id="about.copyright"
|
||||
values={
|
||||
Object {
|
||||
"currentYear": 2017,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__links"
|
||||
>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/terms-of-use/"
|
||||
id="tosLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Terms of Use"
|
||||
id="about.tos"
|
||||
/>
|
||||
</ExternalLink>
|
||||
-
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/privacy-policy/"
|
||||
id="privacyLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Privacy Policy"
|
||||
id="about.privacy"
|
||||
/>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__notice form-group pt-3"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
|
||||
id="about.notice"
|
||||
values={
|
||||
Object {
|
||||
"linkDesktop": [Function],
|
||||
"linkMobile": [Function],
|
||||
"linkServer": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__hash"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Hash:"
|
||||
id="about.hash"
|
||||
/>
|
||||
<Nbsp />
|
||||
abcdef1234567890
|
||||
<br />
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="EE Build Hash:"
|
||||
id="about.hashee"
|
||||
/>
|
||||
<Nbsp />
|
||||
0123456789abcdef
|
||||
</p>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Date:"
|
||||
id="about.date"
|
||||
/>
|
||||
<Nbsp />
|
||||
21 January 2017
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/AboutBuildModal should match snapshot for team edition 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="aboutModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal about-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="aboutModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="About {appTitle}"
|
||||
id="about.title"
|
||||
values={
|
||||
Object {
|
||||
"appTitle": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="about-modal__content"
|
||||
>
|
||||
<div
|
||||
className="about-modal__logo"
|
||||
>
|
||||
<MattermostLogo />
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
className="about-modal__title"
|
||||
>
|
||||
<strong>
|
||||
Mattermost
|
||||
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Team Edition"
|
||||
id="about.teamEditiont0"
|
||||
/>
|
||||
</strong>
|
||||
</h3>
|
||||
<p
|
||||
className="about-modal__subtitle pb-2"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="All your team communication in one place, instantly searchable and accessible anywhere."
|
||||
id="about.teamEditionSt"
|
||||
/>
|
||||
</p>
|
||||
<div
|
||||
className="form-group less"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost Version:"
|
||||
id="about.version"
|
||||
/>
|
||||
<span
|
||||
id="versionString"
|
||||
>
|
||||
3.6.2
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database Schema Version:"
|
||||
id="about.dbversion"
|
||||
/>
|
||||
<span
|
||||
id="dbversionString"
|
||||
>
|
||||
77
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database:"
|
||||
id="about.database"
|
||||
/>
|
||||
Postgres
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__footer"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Join the Mattermost community at "
|
||||
id="about.teamEditionLearn"
|
||||
/>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/community/"
|
||||
location="about_build_modal"
|
||||
>
|
||||
mattermost.com/community/
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="about-modal__copyright"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
|
||||
id="about.copyright"
|
||||
values={
|
||||
Object {
|
||||
"currentYear": 2017,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__links"
|
||||
>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/terms-of-use/"
|
||||
id="tosLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Terms of Use"
|
||||
id="about.tos"
|
||||
/>
|
||||
</ExternalLink>
|
||||
-
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/privacy-policy/"
|
||||
id="privacyLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Privacy Policy"
|
||||
id="about.privacy"
|
||||
/>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__notice form-group pt-3"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
|
||||
id="about.notice"
|
||||
values={
|
||||
Object {
|
||||
"linkDesktop": [Function],
|
||||
"linkMobile": [Function],
|
||||
"linkServer": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__hash"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Hash:"
|
||||
id="about.hash"
|
||||
/>
|
||||
<Nbsp />
|
||||
abcdef1234567890
|
||||
<br />
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="EE Build Hash:"
|
||||
id="about.hashee"
|
||||
/>
|
||||
<Nbsp />
|
||||
</p>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Date:"
|
||||
id="about.date"
|
||||
/>
|
||||
<Nbsp />
|
||||
21 January 2017
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/AboutBuildModal should show ci if a ci build 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="aboutModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal about-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="aboutModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="About {appTitle}"
|
||||
id="about.title"
|
||||
values={
|
||||
Object {
|
||||
"appTitle": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="about-modal__content"
|
||||
>
|
||||
<div
|
||||
className="about-modal__logo"
|
||||
>
|
||||
<MattermostLogo />
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
className="about-modal__title"
|
||||
>
|
||||
<strong>
|
||||
Mattermost
|
||||
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Team Edition"
|
||||
id="about.teamEditiont0"
|
||||
/>
|
||||
</strong>
|
||||
</h3>
|
||||
<p
|
||||
className="about-modal__subtitle pb-2"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="All your team communication in one place, instantly searchable and accessible anywhere."
|
||||
id="about.teamEditionSt"
|
||||
/>
|
||||
</p>
|
||||
<div
|
||||
className="form-group less"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost Version:"
|
||||
id="about.version"
|
||||
/>
|
||||
<span
|
||||
id="versionString"
|
||||
>
|
||||
ci
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database Schema Version:"
|
||||
id="about.dbversion"
|
||||
/>
|
||||
<span
|
||||
id="dbversionString"
|
||||
>
|
||||
77
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Number:"
|
||||
id="about.buildnumber"
|
||||
/>
|
||||
<span
|
||||
id="buildnumberString"
|
||||
>
|
||||
123
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database:"
|
||||
id="about.database"
|
||||
/>
|
||||
Postgres
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__footer"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Join the Mattermost community at "
|
||||
id="about.teamEditionLearn"
|
||||
/>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/community/"
|
||||
location="about_build_modal"
|
||||
>
|
||||
mattermost.com/community/
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="about-modal__copyright"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
|
||||
id="about.copyright"
|
||||
values={
|
||||
Object {
|
||||
"currentYear": 2017,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__links"
|
||||
>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/terms-of-use/"
|
||||
id="tosLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Terms of Use"
|
||||
id="about.tos"
|
||||
/>
|
||||
</ExternalLink>
|
||||
-
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/privacy-policy/"
|
||||
id="privacyLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Privacy Policy"
|
||||
id="about.privacy"
|
||||
/>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__notice form-group pt-3"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
|
||||
id="about.notice"
|
||||
values={
|
||||
Object {
|
||||
"linkDesktop": [Function],
|
||||
"linkMobile": [Function],
|
||||
"linkServer": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__hash"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Hash:"
|
||||
id="about.hash"
|
||||
/>
|
||||
<Nbsp />
|
||||
abcdef1234567890
|
||||
<br />
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="EE Build Hash:"
|
||||
id="about.hashee"
|
||||
/>
|
||||
<Nbsp />
|
||||
</p>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Date:"
|
||||
id="about.date"
|
||||
/>
|
||||
<Nbsp />
|
||||
21 January 2017
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/AboutBuildModal should show dev if this is a dev build 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="aboutModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal about-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="aboutModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="About {appTitle}"
|
||||
id="about.title"
|
||||
values={
|
||||
Object {
|
||||
"appTitle": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="about-modal__content"
|
||||
>
|
||||
<div
|
||||
className="about-modal__logo"
|
||||
>
|
||||
<MattermostLogo />
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
className="about-modal__title"
|
||||
>
|
||||
<strong>
|
||||
Mattermost
|
||||
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Team Edition"
|
||||
id="about.teamEditiont0"
|
||||
/>
|
||||
</strong>
|
||||
</h3>
|
||||
<p
|
||||
className="about-modal__subtitle pb-2"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="All your team communication in one place, instantly searchable and accessible anywhere."
|
||||
id="about.teamEditionSt"
|
||||
/>
|
||||
</p>
|
||||
<div
|
||||
className="form-group less"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost Version:"
|
||||
id="about.version"
|
||||
/>
|
||||
<span
|
||||
id="versionString"
|
||||
>
|
||||
dev
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database Schema Version:"
|
||||
id="about.dbversion"
|
||||
/>
|
||||
<span
|
||||
id="dbversionString"
|
||||
>
|
||||
77
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Database:"
|
||||
id="about.database"
|
||||
/>
|
||||
Postgres
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__footer"
|
||||
>
|
||||
<div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Join the Mattermost community at "
|
||||
id="about.teamEditionLearn"
|
||||
/>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/community/"
|
||||
location="about_build_modal"
|
||||
>
|
||||
mattermost.com/community/
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="about-modal__copyright"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
|
||||
id="about.copyright"
|
||||
values={
|
||||
Object {
|
||||
"currentYear": 2017,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__links"
|
||||
>
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/terms-of-use/"
|
||||
id="tosLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Terms of Use"
|
||||
id="about.tos"
|
||||
/>
|
||||
</ExternalLink>
|
||||
-
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/privacy-policy/"
|
||||
id="privacyLink"
|
||||
location="about_build_modal"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Privacy Policy"
|
||||
id="about.privacy"
|
||||
/>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__notice form-group pt-3"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
|
||||
id="about.notice"
|
||||
values={
|
||||
Object {
|
||||
"linkDesktop": [Function],
|
||||
"linkMobile": [Function],
|
||||
"linkServer": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="about-modal__hash"
|
||||
>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Hash:"
|
||||
id="about.hash"
|
||||
/>
|
||||
<Nbsp />
|
||||
abcdef1234567890
|
||||
<br />
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="EE Build Hash:"
|
||||
id="about.hashee"
|
||||
/>
|
||||
<Nbsp />
|
||||
</p>
|
||||
<p>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Build Date:"
|
||||
id="about.date"
|
||||
/>
|
||||
<Nbsp />
|
||||
21 January 2017
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
@@ -2,8 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {shallow} from 'enzyme';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
@@ -12,11 +10,13 @@ import {ClientConfig, ClientLicense} from '@mattermost/types/config';
|
||||
|
||||
import AboutBuildModal from 'components/about_build_modal/about_build_modal';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
import {AboutLinks} from 'utils/constants';
|
||||
|
||||
import AboutBuildModalCloud from './about_build_modal_cloud/about_build_modal_cloud';
|
||||
import {screen} from '@testing-library/react';
|
||||
import {renderWithIntl} from 'tests/react_testing_utils';
|
||||
import store from 'stores/redux_store';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('components/AboutBuildModal', () => {
|
||||
const RealDate: DateConstructor = Date;
|
||||
@@ -60,10 +60,17 @@ describe('components/AboutBuildModal', () => {
|
||||
});
|
||||
|
||||
test('should match snapshot for enterprise edition', () => {
|
||||
const wrapper = shallowAboutBuildModal({config, license});
|
||||
expect(wrapper.find('#versionString').text()).toBe('\u00a03.6.2');
|
||||
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077');
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
renderAboutBuildModal({config, license});
|
||||
expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: 3.6.2');
|
||||
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
|
||||
expect(screen.getByText('Mattermost Enterprise Edition')).toBeInTheDocument();
|
||||
expect(screen.getByText('Modern communication from behind your firewall.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', {name: 'mattermost.com'})).toHaveAttribute('href', 'https://mattermost.com/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
|
||||
expect(screen.getByText('EE Build Hash: 0123456789abcdef', {exact: false})).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
|
||||
});
|
||||
|
||||
test('should match snapshot for team edition', () => {
|
||||
@@ -73,19 +80,25 @@ describe('components/AboutBuildModal', () => {
|
||||
BuildHashEnterprise: '',
|
||||
};
|
||||
|
||||
const wrapper = shallowAboutBuildModal({config: teamConfig, license: {}});
|
||||
expect(wrapper.find('#versionString').text()).toBe('\u00a03.6.2');
|
||||
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077');
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
renderAboutBuildModal({config: teamConfig, license: {}});
|
||||
expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: 3.6.2');
|
||||
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
|
||||
expect(screen.getByText('Mattermost Team Edition')).toBeInTheDocument();
|
||||
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
|
||||
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
|
||||
});
|
||||
|
||||
test('should match snapshot for cloud edition', () => {
|
||||
if (license !== null) {
|
||||
license.Cloud = 'true';
|
||||
}
|
||||
const store = mockStore();
|
||||
|
||||
const wrapper = shallow(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<AboutBuildModalCloud
|
||||
config={config}
|
||||
@@ -96,7 +109,14 @@ describe('components/AboutBuildModal', () => {
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(screen.getByText('Mattermost Cloud')).toBeInTheDocument();
|
||||
expect(screen.getByText('High trust messaging for the enterprise')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('0123456789abcdef', {exact: false})).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
|
||||
});
|
||||
|
||||
test('should show dev if this is a dev build', () => {
|
||||
@@ -109,10 +129,18 @@ describe('components/AboutBuildModal', () => {
|
||||
BuildNumber: 'dev',
|
||||
};
|
||||
|
||||
const wrapper = shallowAboutBuildModal({config: sameBuildConfig, license: {}});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('#versionString').text()).toBe('\u00a0dev');
|
||||
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077');
|
||||
renderAboutBuildModal({config: sameBuildConfig, license: {}});
|
||||
|
||||
expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: dev');
|
||||
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
|
||||
expect(screen.getByText('Mattermost Team Edition')).toBeInTheDocument();
|
||||
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
|
||||
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
|
||||
});
|
||||
|
||||
test('should show ci if a ci build', () => {
|
||||
@@ -125,11 +153,19 @@ describe('components/AboutBuildModal', () => {
|
||||
BuildNumber: '123',
|
||||
};
|
||||
|
||||
const wrapper = shallowAboutBuildModal({config: differentBuildConfig, license: {}});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('#versionString').text()).toBe('\u00a0ci');
|
||||
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077');
|
||||
expect(wrapper.find('#buildnumberString').text()).toBe('\u00a0123');
|
||||
renderAboutBuildModal({config: differentBuildConfig, license: {}});
|
||||
|
||||
expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: ci');
|
||||
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
|
||||
expect(screen.getByTestId('aboutModalBuildNumber')).toHaveTextContent('Build Number: 123');
|
||||
expect(screen.getByText('Mattermost Team Edition')).toBeInTheDocument();
|
||||
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
|
||||
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
|
||||
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
|
||||
});
|
||||
|
||||
test('should call onExited callback when the modal is hidden', () => {
|
||||
@@ -148,7 +184,7 @@ describe('components/AboutBuildModal', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<AboutBuildModal
|
||||
config={config}
|
||||
@@ -158,7 +194,7 @@ describe('components/AboutBuildModal', () => {
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
wrapper.find(Modal).first().props().onExited?.(document.createElement('div'));
|
||||
userEvent.click(screen.getByText('Close'));
|
||||
expect(onExited).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -176,7 +212,7 @@ describe('components/AboutBuildModal', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<AboutBuildModal
|
||||
config={config}
|
||||
@@ -186,24 +222,15 @@ describe('components/AboutBuildModal', () => {
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(AboutBuildModal).find('a#tosLink').props().href,
|
||||
).toBe(
|
||||
AboutLinks.TERMS_OF_SERVICE +
|
||||
'?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=',
|
||||
);
|
||||
expect(
|
||||
wrapper.find(AboutBuildModal).find('a#privacyLink').props().href,
|
||||
).toBe(
|
||||
AboutLinks.PRIVACY_POLICY +
|
||||
'?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=',
|
||||
);
|
||||
expect(screen.getByRole('link', {name: 'Terms of Use'})).toHaveAttribute('href', `${AboutLinks.TERMS_OF_SERVICE}?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=`);
|
||||
|
||||
expect(wrapper.find(AboutBuildModal).find('a#tosLink').props().href).not.toBe(config?.TermsOfServiceLink);
|
||||
expect(wrapper.find(AboutBuildModal).find('a#privacyLink').props().href).not.toBe(config?.PrivacyPolicyLink);
|
||||
expect(screen.getByRole('link', {name: 'Privacy Policy'})).toHaveAttribute('href', `${AboutLinks.PRIVACY_POLICY}?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=`);
|
||||
|
||||
expect(screen.getByRole('link', {name: 'Terms of Use'})).not.toHaveAttribute('href', config?.TermsOfServiceLink);
|
||||
expect(screen.getByRole('link', {name: 'Privacy Policy'})).not.toHaveAttribute('href', config?.PrivacyPolicyLink);
|
||||
});
|
||||
|
||||
function shallowAboutBuildModal(props = {}) {
|
||||
function renderAboutBuildModal(props = {}) {
|
||||
const onExited = jest.fn();
|
||||
const show = true;
|
||||
|
||||
@@ -215,6 +242,6 @@ describe('components/AboutBuildModal', () => {
|
||||
...props,
|
||||
};
|
||||
|
||||
return shallow(<AboutBuildModal {...allProps}/>);
|
||||
return renderWithIntl(<Provider store={store}><AboutBuildModal {...allProps}/></Provider>);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,6 +49,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
|
||||
|
||||
doHide = () => {
|
||||
this.setState({show: false});
|
||||
this.props.onExited();
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -172,7 +173,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
|
||||
|
||||
// Only show build number if it's a number (so only builds from Jenkins)
|
||||
let buildnumber: JSX.Element | null = (
|
||||
<div>
|
||||
<div data-testid='aboutModalBuildNumber'>
|
||||
<FormattedMessage
|
||||
id='about.buildnumber'
|
||||
defaultMessage='Build Number:'
|
||||
@@ -227,7 +228,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
|
||||
{subTitle}
|
||||
</p>
|
||||
<div className='form-group less'>
|
||||
<div>
|
||||
<div data-testid='aboutModalVersion'>
|
||||
<FormattedMessage
|
||||
id='about.version'
|
||||
defaultMessage='Mattermost Version:'
|
||||
@@ -236,7 +237,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
|
||||
{'\u00a0' + mmversion}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div data-testid='aboutModalDBVersionString'>
|
||||
<FormattedMessage
|
||||
id='about.dbversion'
|
||||
defaultMessage='Database Schema Version:'
|
||||
|
||||
@@ -6,8 +6,7 @@ import {Link, useRouteMatch, useLocation, matchPath} from 'react-router-dom';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '@mattermost/compass-components/foundations/icon'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import {ChartLineIcon} from '@mattermost/compass-icons/components';
|
||||
import {insightsAreEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
||||
|
||||
@@ -62,10 +61,7 @@ const ActivityAndInsightsLink = () => {
|
||||
tabIndex={0}
|
||||
>
|
||||
<span className='icon'>
|
||||
<Icon
|
||||
size={12}
|
||||
glyph={'chart-line'}
|
||||
/>
|
||||
<ChartLineIcon size={14}/>
|
||||
</span>
|
||||
<div className='SidebarChannelLinkLabel_wrapper'>
|
||||
<span className='SidebarChannelLinkLabel sidebar-item__name'>
|
||||
|
||||
@@ -222,7 +222,7 @@ exports[`components/activity_and_insights/insights/top_channels should match sna
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Top Channels"
|
||||
id="analytics.system.totalBotPosts"
|
||||
id="analytics.system.topChannels"
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -237,7 +237,7 @@ exports[`components/activity_and_insights/insights/top_channels should match sna
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Top Channels"
|
||||
id="analytics.system.totalBotPosts"
|
||||
id="analytics.system.topChannels"
|
||||
>
|
||||
<span>
|
||||
Top Channels
|
||||
|
||||
@@ -115,7 +115,7 @@ const TopChannelsLineChart = ({topChannels, timeFrame, channelLineChartData, tim
|
||||
<LineChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalBotPosts'
|
||||
id='analytics.system.topChannels'
|
||||
defaultMessage='Top Channels'
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -353,6 +353,36 @@ exports[`components/DatabaseSettings should match snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="control-label col-sm-4"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Active Search Backend:"
|
||||
id="admin.database.search_backend.title"
|
||||
/>
|
||||
</label>
|
||||
<div
|
||||
className="col-sm-8"
|
||||
>
|
||||
<input
|
||||
className="form-control"
|
||||
disabled={true}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="help-text"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Shows the currently active backend used for search. Values can be none, database, elasticsearch, bleve etc."
|
||||
id="admin.database.search_backend.help_text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsGroup>
|
||||
<div
|
||||
className="admin-console-save"
|
||||
|
||||
@@ -2869,7 +2869,7 @@ const AdminDefinition = {
|
||||
label: t('admin.customization.enableSVGsTitle'),
|
||||
label_default: 'Enable SVGs:',
|
||||
help_text: t('admin.customization.enableSVGsDesc'),
|
||||
help_text_default: 'Enable previews for SVG file attachments and allow them to appear in messages.',
|
||||
help_text_default: 'Enable previews for SVG file attachments and allow them to appear in messages.\n\nEnabling SVGs is not recommended in environments where not all users are trusted.',
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
|
||||
},
|
||||
{
|
||||
@@ -2878,7 +2878,7 @@ const AdminDefinition = {
|
||||
label: t('admin.customization.enableLatexTitle'),
|
||||
label_default: 'Enable Latex Rendering:',
|
||||
help_text: t('admin.customization.enableLatexDesc'),
|
||||
help_text_default: 'Enable rendering of Latex in code blocks. If false, Latex code will be highlighted only.',
|
||||
help_text_default: 'Enable rendering of Latex in code blocks. If false, Latex code will be highlighted only.\n\nEnabling Latex is not recommended in environments where not all users are trusted.',
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/admin_console/billing/billing_history should match snapshot 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<BillingHistory />
|
||||
</ContextProvider>
|
||||
`;
|
||||
@@ -5,11 +5,9 @@ import React from 'react';
|
||||
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import {screen} from '@testing-library/react';
|
||||
|
||||
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {renderWithIntl, renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import {CloudLinks, HostedCustomerLinks} from 'utils/constants';
|
||||
@@ -35,7 +33,7 @@ const invoiceA = {
|
||||
quantity: 1,
|
||||
price_per_unit: 1000,
|
||||
description:
|
||||
'1 × Cloud Professional (at $10.00 / month)',
|
||||
'1 × Cloud Professional (at $10.00 / month)',
|
||||
type: 'onpremise',
|
||||
metadata: {},
|
||||
},
|
||||
@@ -59,7 +57,7 @@ const invoiceB = {
|
||||
quantity: 1,
|
||||
price_per_unit: 1000,
|
||||
description:
|
||||
'Trial period for Cloud Professional',
|
||||
'Trial period for Cloud Professional',
|
||||
type: 'onpremise',
|
||||
metadata: {},
|
||||
},
|
||||
@@ -98,13 +96,21 @@ describe('components/admin_console/billing/billing_history', () => {
|
||||
|
||||
const store = mockStore(state);
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
test('should match the default state of the component with given props', () => {
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(screen.queryByText('Billing History')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Transactions')).toBeInTheDocument();
|
||||
expect(screen.queryByText('All of your invoices will be shown here')).toBeInTheDocument();
|
||||
expect(screen.getByTestId(invoiceA.number)).toHaveTextContent((invoiceA.total / 100.0).toString());
|
||||
expect(screen.getByTestId(invoiceB.number)).toHaveTextContent((invoiceB.total / 100.0).toString());
|
||||
|
||||
expect(screen.getByTestId(invoiceA.id)).toHaveTextContent('Pending');
|
||||
expect(screen.getByTestId(invoiceB.id)).toHaveTextContent('Paid');
|
||||
});
|
||||
|
||||
test('Billing history section shows template when no invoices have been emitted yet', () => {
|
||||
@@ -113,42 +119,54 @@ describe('components/admin_console/billing/billing_history', () => {
|
||||
entities: {...state.entities, cloud: {invoices: {}, errors: {}}},
|
||||
};
|
||||
const storeNoBillingHistory = mockStore(noBillingHistoryState);
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={storeNoBillingHistory}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const legend = wrapper.find(
|
||||
'.BillingHistory__cardHeaderText-bottom span',
|
||||
);
|
||||
expect(legend.text()).toBe(NO_INVOICES_LEGEND);
|
||||
expect(screen.queryByText('Date')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Description')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Total')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Status')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId(invoiceA.number)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(invoiceB.number)).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId(invoiceA.id)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(invoiceB.id)).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', 'https://docs.mattermost.com/cloud/cloud-billing/cloud-billing.html?utm_source=mattermost&utm_medium=in-product-cloud&utm_content=billing_history&uid=current_user_id&sid=');
|
||||
expect(screen.getByRole('link')).toHaveTextContent('See how billing works');
|
||||
expect(screen.getByTestId('no-invoices')).toHaveTextContent(NO_INVOICES_LEGEND);
|
||||
});
|
||||
|
||||
test('Billing history section shows two invoices to download', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const invoiceTableRows = wrapper.find('table.BillingHistory__table tr.BillingHistory__table-row');
|
||||
expect(screen.queryByText('Date')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Description')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Total')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Status')).toBeInTheDocument();
|
||||
|
||||
expect(invoiceTableRows.length).toBe(2);
|
||||
expect(screen.getAllByTestId('billingHistoryTableRow')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('Billing history section download button has the target property set as _self so it works well in desktop app', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const invoiceTableRow = wrapper.find('table.BillingHistory__table tr.BillingHistory__table-row').at(0);
|
||||
|
||||
const downloadLink = invoiceTableRow.find('td.BillingHistory__table-invoice a');
|
||||
|
||||
expect(downloadLink.prop('target')).toBe('_self');
|
||||
expect(screen.getByTestId(`billingHistoryLink-${invoiceA.id}`)).toHaveAttribute('target', '_self');
|
||||
expect(screen.getByTestId(`billingHistoryLink-${invoiceB.id}`)).toHaveAttribute('target', '_self');
|
||||
expect(screen.getByTestId(`billingHistoryLink-${invoiceA.id}`)).toHaveAttribute('href', '/api/v4/cloud/subscription/invoices/in_1KNb3DI67GP2qpb4ueaJYBt8/pdf');
|
||||
expect(screen.getByTestId(`billingHistoryLink-${invoiceB.id}`)).toHaveAttribute('href', '/api/v4/cloud/subscription/invoices/in_1KIWNTI67GP2qpb4KjGj1KAy/pdf');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,30 +209,42 @@ describe('BillingHistory -- self-hosted', () => {
|
||||
entities: {...state.entities, hostedCustomer: {invoices: {invoices: {}, invoicesLoaded: true}, errors: {}}},
|
||||
};
|
||||
const storeNoBillingHistory = mockStore(noBillingHistoryState);
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={storeNoBillingHistory}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const legend = wrapper.find(
|
||||
'.BillingHistory__cardHeaderText-bottom span',
|
||||
);
|
||||
expect(legend.text()).toBe(NO_INVOICES_LEGEND);
|
||||
expect(screen.queryByText('Date')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Description')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Total')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Status')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId(invoiceA.number)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(invoiceB.number)).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId(invoiceA.id)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(invoiceB.id)).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', 'https://docs.mattermost.com/manage/self-hosted-billing.html?utm_source=mattermost&utm_medium=in-product&utm_content=billing_history&uid=current_user_id&sid=');
|
||||
expect(screen.getByRole('link')).toHaveTextContent('See how billing works');
|
||||
expect(screen.getByTestId('no-invoices')).toHaveTextContent(NO_INVOICES_LEGEND);
|
||||
});
|
||||
|
||||
test('Billing history section shows two invoices to download', () => {
|
||||
const store = mockStore(state);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
renderWithIntl(
|
||||
<Provider store={store}>
|
||||
<BillingHistory/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const invoiceTableRows = wrapper.find('table.BillingHistory__table tr.BillingHistory__table-row');
|
||||
|
||||
expect(invoiceTableRows.length).toBe(2);
|
||||
expect(screen.queryByText('Date')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Description')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Total')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Status')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('billingHistoryTableRow')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export const NoBillingHistorySection = (props: NoBillingHistorySectionProps) =>
|
||||
/>
|
||||
</div>
|
||||
<ExternalLink
|
||||
data-testid='billingHistoryLink'
|
||||
location='billing_history'
|
||||
href={props.selfHosted ? HostedCustomerLinks.SELF_HOSTED_BILLING : CloudLinks.BILLING_DOCS}
|
||||
className='BillingHistory__noHistory-link'
|
||||
@@ -84,7 +85,10 @@ const BillingHistory = () => {
|
||||
defaultMessage='Transactions'
|
||||
/>
|
||||
</div>
|
||||
<div className='BillingHistory__cardHeaderText-bottom'>
|
||||
<div
|
||||
data-testid='no-invoices'
|
||||
className='BillingHistory__cardHeaderText-bottom'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.billing.history.allPaymentsShowHere'
|
||||
defaultMessage='All of your invoices will be shown here'
|
||||
|
||||
@@ -176,7 +176,7 @@ export default function BillingHistoryTable({invoices}: BillingHistoryTableProps
|
||||
);
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<td data-testid='billingHistoryTableRow'>
|
||||
<FormattedDate
|
||||
value={new Date(invoice.period_start)}
|
||||
month='2-digit'
|
||||
@@ -191,7 +191,10 @@ export default function BillingHistoryTable({invoices}: BillingHistoryTableProps
|
||||
<InvoiceUserCount invoice={invoice}/>
|
||||
</div>
|
||||
</td>
|
||||
<td className='BillingHistory__table-total'>
|
||||
<td
|
||||
data-testid={invoice.number}
|
||||
className='BillingHistory__table-total'
|
||||
>
|
||||
<FormattedNumber
|
||||
value={invoice.total / 100.0}
|
||||
// eslint-disable-next-line react/style-prop-object
|
||||
@@ -199,9 +202,10 @@ export default function BillingHistoryTable({invoices}: BillingHistoryTableProps
|
||||
currency='USD'
|
||||
/>
|
||||
</td>
|
||||
<td>{getPaymentStatus(invoice.status)}</td>
|
||||
<td data-testid={invoice.id}>{getPaymentStatus(invoice.status)}</td>
|
||||
<td className='BillingHistory__table-invoice'>
|
||||
<a
|
||||
data-testid={`billingHistoryLink-${invoice.id}`}
|
||||
target='_self'
|
||||
rel='noopener noreferrer'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import LimitReachedBanner from './limit_reached_banner';
|
||||
import CancelSubscription from './cancel_subscription';
|
||||
import {ToYearlyNudgeBanner} from './to_yearly_nudge_banner';
|
||||
import {ToPaidNudgeBanner} from './to_paid_plan_nudge_banner';
|
||||
|
||||
import './billing_subscriptions.scss';
|
||||
|
||||
@@ -136,6 +137,7 @@ const BillingSubscriptions = () => {
|
||||
/>
|
||||
{shouldShowPaymentFailedBanner() && paymentFailedBanner()}
|
||||
{<ToYearlyNudgeBanner/>}
|
||||
{<ToPaidNudgeBanner/>}
|
||||
{showCreditCardBanner &&
|
||||
isCardExpired &&
|
||||
creditCardExpiredBanner(setShowCreditCardBanner)}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
@import 'utils/mixins';
|
||||
|
||||
.ToPaidNudgeBanner {
|
||||
&__actions {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
&__primary {
|
||||
font-size: 12px;
|
||||
|
||||
@include primary-button;
|
||||
|
||||
&:hover {
|
||||
color: var(--button-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__secondary {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
@include tertiary-button;
|
||||
|
||||
&:hover {
|
||||
color: var(--button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {screen} from '@testing-library/react';
|
||||
|
||||
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
|
||||
import {ToPaidNudgeBanner, ToPaidPlanBannerDismissable} from './to_paid_plan_nudge_banner';
|
||||
|
||||
const initialState = {
|
||||
views: {
|
||||
announcementBar: {
|
||||
announcementBarState: {
|
||||
announcementBarCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
CWSURL: '',
|
||||
FeatureFlagDeprecateCloudFree: 'true',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_user'},
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
cloud: {},
|
||||
},
|
||||
};
|
||||
|
||||
describe('ToPaidPlanBannerDismissable', () => {
|
||||
test('should only show for admins on cloud free', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_starter',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_starter: {
|
||||
id: 'prod_starter',
|
||||
sku: CloudProducts.STARTER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||
|
||||
screen.getByTestId('cloud-free-deprecation-announcement-bar');
|
||||
});
|
||||
|
||||
test('should NOT show for NON admins', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_user'},
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_starter',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_starter: {
|
||||
id: 'prod_starter',
|
||||
sku: CloudProducts.STARTER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins on cloud pro', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_pro',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_pro: {
|
||||
id: 'prod_pro',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins on cloud enterprise', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_enterprise',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_enterprise: {
|
||||
id: 'prod_enterprise',
|
||||
sku: CloudProducts.ENTERPRISE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins when banner was dismissed in preferences', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
};
|
||||
state.entities.preferences = {
|
||||
myPreferences: {
|
||||
'to_paid_plan_nudge--nudge_to_paid_plan_snoozed': {
|
||||
category: 'to_paid_plan_nudge',
|
||||
name: 'nudge_to_paid_plan_snoozed',
|
||||
value: '{"range": 0, "show": false}',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_starter',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_starter: {
|
||||
id: 'prod_starter',
|
||||
sku: CloudProducts.STARTER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ToPaidNudgeBanner', () => {
|
||||
test('should show only for cloud free', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_starter',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_starter: {
|
||||
id: 'prod_starter',
|
||||
sku: CloudProducts.STARTER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidNudgeBanner/>, state);
|
||||
|
||||
screen.getByTestId('cloud-free-deprecation-alert-banner');
|
||||
});
|
||||
|
||||
test('should NOT show for cloud professional', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_pro',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_pro: {
|
||||
id: 'prod_pro',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidNudgeBanner/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for cloud enterprise', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_ent',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_ent: {
|
||||
id: 'prod_ent',
|
||||
sku: CloudProducts.ENTERPRISE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToPaidNudgeBanner/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useIntl, FormattedMessage} from 'react-intl';
|
||||
import moment from 'moment';
|
||||
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
|
||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
|
||||
|
||||
import {getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {deprecateCloudFree, get as getPreference} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {AnnouncementBarTypes, CloudBanners, CloudProducts, Preferences} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import './to_paid_plan_nudge_banner.scss';
|
||||
|
||||
enum DismissShowRange {
|
||||
GreaterThanEqual90 = '>=90',
|
||||
BetweenNinetyAnd60 = '89-61',
|
||||
SixtyTo31 = '60-31',
|
||||
ThirtyTo11 = '30-11',
|
||||
TenTo1 = '10-1',
|
||||
Zero = '0'
|
||||
}
|
||||
|
||||
const cloudFreeCloseMoment = '20230727';
|
||||
|
||||
interface ToPaidPlanDismissPreference {
|
||||
|
||||
// range represents the range for the days to the deprecation of cloud free e.g. in 30 to 10 days to deprecate cloud free
|
||||
// Incase of dismissing the banner, range represents the time (days) period when this banner was dismissed.
|
||||
// This is important because in case the banner was dismissed for a certain period, it helps us know that we should not show it again for that period.
|
||||
range: DismissShowRange;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export const ToPaidPlanBannerDismissable = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const openPricingModal = useOpenPricingModal();
|
||||
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
const isAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
const currentProductStarter = product?.sku === CloudProducts.STARTER;
|
||||
|
||||
const now = moment(Date.now());
|
||||
const cloudFreeEndDate = moment(cloudFreeCloseMoment, 'YYYYMMDD');
|
||||
const daysToCloudFreeEnd = cloudFreeEndDate.diff(now, 'days');
|
||||
|
||||
const snoozePreferenceVal = useSelector((state: GlobalState) => getPreference(state, Preferences.TO_PAID_PLAN_NUDGE, CloudBanners.NUDGE_TO_PAID_PLAN_SNOOZED, '{"range": 0, "show": true}'));
|
||||
const snoozeInfo = JSON.parse(snoozePreferenceVal) as ToPaidPlanDismissPreference;
|
||||
const show = snoozeInfo.show;
|
||||
|
||||
const snoozedForRange = (range: DismissShowRange) => {
|
||||
return snoozeInfo.range === range;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!snoozeInfo.show) {
|
||||
if (daysToCloudFreeEnd >= 90 && !snoozedForRange(DismissShowRange.GreaterThanEqual90)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd < 90 && daysToCloudFreeEnd > 60 && !snoozedForRange(DismissShowRange.BetweenNinetyAnd60)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd <= 60 && daysToCloudFreeEnd > 30 && !snoozedForRange(DismissShowRange.SixtyTo31)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd <= 30 && daysToCloudFreeEnd > 10 && !snoozedForRange(DismissShowRange.ThirtyTo11)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd <= 10) {
|
||||
showBanner(true);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showBanner = (show = false) => {
|
||||
let dRange = DismissShowRange.Zero;
|
||||
if (daysToCloudFreeEnd >= 90) {
|
||||
dRange = DismissShowRange.GreaterThanEqual90;
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd < 90 && daysToCloudFreeEnd > 60) {
|
||||
dRange = DismissShowRange.BetweenNinetyAnd60;
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd <= 60 && daysToCloudFreeEnd > 30) {
|
||||
dRange = DismissShowRange.SixtyTo31;
|
||||
}
|
||||
|
||||
if (daysToCloudFreeEnd <= 30 && daysToCloudFreeEnd > 10) {
|
||||
dRange = DismissShowRange.ThirtyTo11;
|
||||
}
|
||||
|
||||
// ideally this case should not happen because snooze button is not shown when TenTo1 days are remaining
|
||||
if (daysToCloudFreeEnd <= 10 && daysToCloudFreeEnd > 0) {
|
||||
dRange = DismissShowRange.TenTo1;
|
||||
}
|
||||
|
||||
const snoozeInfo: ToPaidPlanDismissPreference = {
|
||||
range: dRange,
|
||||
show,
|
||||
};
|
||||
|
||||
dispatch(savePreferences(currentUser.id, [{
|
||||
category: Preferences.TO_PAID_PLAN_NUDGE,
|
||||
name: CloudBanners.NUDGE_TO_PAID_PLAN_SNOOZED,
|
||||
user_id: currentUser.id,
|
||||
value: JSON.stringify(snoozeInfo),
|
||||
}]));
|
||||
};
|
||||
|
||||
if (!cloudFreeDeprecated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentProductStarter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let message = {
|
||||
id: 'cloud_billing.nudge_to_paid.announcement_bar',
|
||||
defaultMessage: 'Cloud Free will be deprecated on {date}. To keep your workspace, upgrade to a paid plan',
|
||||
values: {
|
||||
date: moment(cloudFreeCloseMoment, 'YYYYMMDD').format('MMMM DD, YYYY'),
|
||||
},
|
||||
};
|
||||
|
||||
if (daysToCloudFreeEnd < 0) {
|
||||
message = {
|
||||
id: 'cloud_billing.nudge_to_paid.announcement_bar_deprecated',
|
||||
defaultMessage: 'Cloud Free was deprecated. To keep your workspace, upgrade to a paid plan',
|
||||
} as any;
|
||||
}
|
||||
|
||||
const announcementType = (daysToCloudFreeEnd <= 10) ? AnnouncementBarTypes.CRITICAL : AnnouncementBarTypes.ANNOUNCEMENT;
|
||||
|
||||
return (
|
||||
<AnnouncementBar
|
||||
id='cloud-free-deprecation-announcement-bar'
|
||||
type={announcementType}
|
||||
showCloseButton={daysToCloudFreeEnd > 10}
|
||||
onButtonClick={openPricingModal}
|
||||
modalButtonText={t('cloud_billing.nudge_to_paid.view_plans')}
|
||||
modalButtonDefaultText='View plans'
|
||||
message={<FormattedMessage {...message}/>}
|
||||
showLinkAsButton={true}
|
||||
handleClose={showBanner}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToPaidNudgeBanner = () => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
const currentProductStarter = product?.sku === CloudProducts.STARTER;
|
||||
|
||||
if (!cloudFreeDeprecated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentProductStarter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = moment(Date.now());
|
||||
const cloudFreeEndDate = moment(cloudFreeCloseMoment, 'YYYYMMDD');
|
||||
const daysToCloudFreeEnd = cloudFreeEndDate.diff(now, 'days');
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_paid.title'
|
||||
defaultMessage='Upgrade to paid plan to keep your workspace'
|
||||
/>
|
||||
);
|
||||
|
||||
const description = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_paid.description'
|
||||
defaultMessage='Cloud Free will be deprecated in {days} days. Upgrade to a paid plan or contact sales.'
|
||||
values={{days: daysToCloudFreeEnd < 0 ? 0 : daysToCloudFreeEnd}}
|
||||
/>
|
||||
);
|
||||
|
||||
const viewPlansAction = (
|
||||
<button
|
||||
onClick={() => openPurchaseModal({trackingLocation: 'to_paid_plan_nudge_banner'})}
|
||||
className='btn ToPaidNudgeBanner__primary'
|
||||
>
|
||||
{formatMessage({id: 'cloud_billing.nudge_to_paid.learn_more', defaultMessage: 'Upgrade'})}
|
||||
</button>
|
||||
);
|
||||
|
||||
const contactSalesAction = (
|
||||
<button
|
||||
onClick={openSalesLink}
|
||||
className='btn ToPaidNudgeBanner__secondary'
|
||||
>
|
||||
{formatMessage({id: 'cloud_billing.nudge_to_paid.contact_sales', defaultMessage: 'Contact sales'})}
|
||||
</button>
|
||||
);
|
||||
|
||||
const bannerMode = (daysToCloudFreeEnd <= 10) ? 'danger' : 'info';
|
||||
|
||||
return (
|
||||
<AlertBanner
|
||||
id='cloud-free-deprecation-alert-banner'
|
||||
mode={bannerMode}
|
||||
title={title}
|
||||
message={description}
|
||||
className='ToYearlyNudgeBanner'
|
||||
actionButtonLeft={viewPlansAction}
|
||||
actionButtonRight={contactSalesAction}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,11 +2,9 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {screen} from '@testing-library/react';
|
||||
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||
import {CloudProducts, RecurringIntervals} from 'utils/constants';
|
||||
|
||||
import {ToYearlyNudgeBanner, ToYearlyNudgeBannerDismissable} from './to_yearly_nudge_banner';
|
||||
@@ -42,7 +40,7 @@ const initialState = {
|
||||
},
|
||||
};
|
||||
|
||||
describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () => {
|
||||
describe('ToYearlyNudgeBannerDismissable', () => {
|
||||
test('should show for admins cloud professional monthly', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
@@ -63,14 +61,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(true);
|
||||
screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar');
|
||||
});
|
||||
|
||||
test('should NOT show for NON admins', () => {
|
||||
@@ -93,14 +86,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins on cloud free', () => {
|
||||
@@ -123,14 +111,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins on cloud enterprise', () => {
|
||||
@@ -153,14 +136,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins on cloud pro annual', () => {
|
||||
@@ -182,15 +160,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for admins when banner was dismissed in preferences', () => {
|
||||
@@ -200,10 +172,10 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
};
|
||||
state.entities.preferences = {
|
||||
myPreferences: {
|
||||
'cloud_yearly_nudge_banner--nudge_to_yearly_banner_dismissed': {
|
||||
category: 'cloud_yearly_nudge_banner',
|
||||
name: 'nudge_to_yearly_banner_dismissed',
|
||||
value: 'true',
|
||||
'to_cloud_yearly_plan_nudge--nudge_to_cloud_yearly_plan_snoozed': {
|
||||
category: 'to_cloud_yearly_plan_nudge',
|
||||
name: 'nudge_to_cloud_yearly_plan_snoozed',
|
||||
value: '{"range": 0, "show": false}',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -221,19 +193,13 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBannerDismissable/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
|
||||
describe('ToYearlyNudgeBanner', () => {
|
||||
test('should show for cloud professional monthly', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
@@ -251,14 +217,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBanner/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
|
||||
|
||||
expect(wrapper.find('AlertBanner').exists()).toBe(true);
|
||||
screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner');
|
||||
});
|
||||
|
||||
test('should NOT show for non cloud professional monthly', () => {
|
||||
@@ -278,13 +239,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<ToYearlyNudgeBanner/>
|
||||
</Provider>,
|
||||
);
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
|
||||
|
||||
expect(wrapper.find('AlertBanner').exists()).toBe(false);
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useIntl, FormattedMessage} from 'react-intl';
|
||||
import moment from 'moment';
|
||||
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
@@ -22,12 +23,35 @@ import {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import './to_yearly_nudge_banner.scss';
|
||||
|
||||
enum DismissShowRange {
|
||||
GreaterThanEqual90 = '>=90',
|
||||
BetweenNinetyAnd60 = '89-61',
|
||||
SixtyTo31 = '60-31',
|
||||
ThirtyTo11 = '30-11',
|
||||
TenTo1 = '10-1',
|
||||
Zero = '0'
|
||||
}
|
||||
|
||||
const cloudProMonthlyCloseMoment = '20230727';
|
||||
|
||||
interface ToYearlyPlanDismissPreference {
|
||||
|
||||
// range represents the range for the days to the deprecation of cloud free e.g. in 30 to 10 days to deprecate cloud free
|
||||
// Incase of dismissing the banner, range represents the time (days) period when this banner was dismissed.
|
||||
// This is important because in case the banner was dismissed for a certain period, it helps us know that we should not show it again for that period.
|
||||
range: DismissShowRange;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const ToYearlyNudgeBannerDismissable = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
|
||||
const nudgeDismissed = useSelector((state: GlobalState) => getPreference(state, Preferences.CLOUD_YEARLY_NUDGE_BANNER, CloudBanners.NUDGE_TO_YEARLY_BANNER_DISMISSED)) === 'true';
|
||||
const snoozePreferenceVal = useSelector((state: GlobalState) => getPreference(state, Preferences.TO_CLOUD_YEARLY_PLAN_NUDGE, CloudBanners.NUDGE_TO_CLOUD_YEARLY_PLAN_SNOOZED, '{"range": 0, "show": true}'));
|
||||
const snoozeInfo = JSON.parse(snoozePreferenceVal) as ToYearlyPlanDismissPreference;
|
||||
const show = snoozeInfo.show;
|
||||
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
const isAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
@@ -35,16 +59,75 @@ const ToYearlyNudgeBannerDismissable = () => {
|
||||
const currentProductIsMonthly = product?.recurring_interval === RecurringIntervals.MONTH;
|
||||
const currentProductProMonthly = currentProductProfessional && currentProductIsMonthly;
|
||||
|
||||
const savedDismissedPref = () => {
|
||||
const now = moment(Date.now());
|
||||
const proMonthlyEndDate = moment(cloudProMonthlyCloseMoment, 'YYYYMMDD');
|
||||
const daysToProMonthlyEnd = proMonthlyEndDate.diff(now, 'days');
|
||||
|
||||
const snoozedForRange = (range: DismissShowRange) => {
|
||||
return snoozeInfo.range === range;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!snoozeInfo.show) {
|
||||
if (daysToProMonthlyEnd >= 90 && !snoozedForRange(DismissShowRange.GreaterThanEqual90)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd < 90 && daysToProMonthlyEnd > 60 && !snoozedForRange(DismissShowRange.BetweenNinetyAnd60)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd <= 60 && daysToProMonthlyEnd > 30 && !snoozedForRange(DismissShowRange.SixtyTo31)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd <= 30 && daysToProMonthlyEnd > 10 && !snoozedForRange(DismissShowRange.ThirtyTo11)) {
|
||||
showBanner(true);
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd <= 10) {
|
||||
showBanner(true);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showBanner = (show = false) => {
|
||||
let dRange = DismissShowRange.Zero;
|
||||
if (daysToProMonthlyEnd >= 90) {
|
||||
dRange = DismissShowRange.GreaterThanEqual90;
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd < 90 && daysToProMonthlyEnd > 60) {
|
||||
dRange = DismissShowRange.BetweenNinetyAnd60;
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd <= 60 && daysToProMonthlyEnd > 30) {
|
||||
dRange = DismissShowRange.SixtyTo31;
|
||||
}
|
||||
|
||||
if (daysToProMonthlyEnd <= 30 && daysToProMonthlyEnd > 10) {
|
||||
dRange = DismissShowRange.ThirtyTo11;
|
||||
}
|
||||
|
||||
// ideally this case should not happen because snooze button is not shown when TenTo1 days are remaining
|
||||
if (daysToProMonthlyEnd <= 10 && daysToProMonthlyEnd > 0) {
|
||||
dRange = DismissShowRange.TenTo1;
|
||||
}
|
||||
|
||||
const snoozeInfo: ToYearlyPlanDismissPreference = {
|
||||
range: dRange,
|
||||
show,
|
||||
};
|
||||
|
||||
dispatch(savePreferences(currentUser.id, [{
|
||||
category: Preferences.CLOUD_YEARLY_NUDGE_BANNER,
|
||||
name: CloudBanners.NUDGE_TO_YEARLY_BANNER_DISMISSED,
|
||||
category: Preferences.TO_CLOUD_YEARLY_PLAN_NUDGE,
|
||||
name: CloudBanners.NUDGE_TO_CLOUD_YEARLY_PLAN_SNOOZED,
|
||||
user_id: currentUser.id,
|
||||
value: 'true',
|
||||
value: JSON.stringify(snoozeInfo),
|
||||
}]));
|
||||
};
|
||||
|
||||
if (nudgeDismissed) {
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -56,21 +139,30 @@ const ToYearlyNudgeBannerDismissable = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: 'cloud_billing.nudge_to_yearly.announcement_bar',
|
||||
defaultMessage: 'Simplify your billing and switch to an annual plan today',
|
||||
};
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_yearly.announcement_bar'
|
||||
defaultMessage='Monthly billing will be discontinued in {days} days . Switch to annual billing'
|
||||
values={{
|
||||
days: daysToProMonthlyEnd,
|
||||
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const announcementType = (daysToProMonthlyEnd <= 10) ? AnnouncementBarTypes.CRITICAL : AnnouncementBarTypes.ANNOUNCEMENT;
|
||||
|
||||
return (
|
||||
<AnnouncementBar
|
||||
type={AnnouncementBarTypes.ANNOUNCEMENT}
|
||||
showCloseButton={true}
|
||||
id='cloud-pro-monthly-deprecation-announcement-bar'
|
||||
type={announcementType}
|
||||
showCloseButton={daysToProMonthlyEnd > 10}
|
||||
onButtonClick={() => openPurchaseModal({trackingLocation: 'to_yearly_nudge_annoucement_bar'})}
|
||||
modalButtonText={t('cloud_billing.nudge_to_yearly.learn_more')}
|
||||
modalButtonDefaultText='Learn more'
|
||||
message={<FormattedMessage {...message}/>}
|
||||
message={message}
|
||||
showLinkAsButton={true}
|
||||
handleClose={savedDismissedPref}
|
||||
handleClose={showBanner}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -90,17 +182,22 @@ const ToYearlyNudgeBanner = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = moment(Date.now());
|
||||
const proMonthlyEndDate = moment(cloudProMonthlyCloseMoment, 'YYYYMMDD');
|
||||
const daysToProMonthlyEnd = proMonthlyEndDate.diff(now, 'days');
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_yearly.title'
|
||||
defaultMessage='Switch to an annual plan today'
|
||||
defaultMessage='Action required: Switch to annual billing to keep your workspace.'
|
||||
/>
|
||||
);
|
||||
|
||||
const description = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_yearly.description'
|
||||
defaultMessage='Simplify your billing by switching to an annual subscription.'
|
||||
defaultMessage='Monthly billing will be discontinued on {date}. To keep your workspace, switch to annual billing.'
|
||||
values={{date: moment(cloudProMonthlyCloseMoment, 'YYYYMMDD').format('MMMM DD, YYYY')}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -122,9 +219,12 @@ const ToYearlyNudgeBanner = () => {
|
||||
</button>
|
||||
);
|
||||
|
||||
const bannerMode = (daysToProMonthlyEnd <= 10) ? 'danger' : 'info';
|
||||
|
||||
return (
|
||||
<AlertBanner
|
||||
mode='info'
|
||||
id='cloud-pro-monthly-deprecation-alert-banner'
|
||||
mode={bannerMode}
|
||||
title={title}
|
||||
message={description}
|
||||
className='ToYearlyNudgeBanner'
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from 'react';
|
||||
import {injectIntl, WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import {Feedback} from '@mattermost/types/cloud';
|
||||
import FeedbackModal from 'components/feedback_modal/feedback';
|
||||
import FeedbackModal, {FeedbackOption} from 'components/feedback_modal/feedback';
|
||||
|
||||
type Props = {
|
||||
onSubmit: (deleteFeedback: Feedback) => void;
|
||||
@@ -28,23 +28,35 @@ const DeleteFeedbackModal = (props: Props) => {
|
||||
defaultMessage: 'Delete Workspace',
|
||||
});
|
||||
|
||||
const deleteFeedbackOptions = [
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackNoValue',
|
||||
defaultMessage: 'No longer found value',
|
||||
}),
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackMoving',
|
||||
defaultMessage: 'Moving to a different solution',
|
||||
}),
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackMistake',
|
||||
defaultMessage: 'Created a workspace by mistake',
|
||||
}),
|
||||
props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackHosting',
|
||||
defaultMessage: 'Moving to hosting my own Mattermost instance (self-hosted)',
|
||||
}),
|
||||
const deleteFeedbackOptions: FeedbackOption[] = [
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackNoValue',
|
||||
defaultMessage: 'No longer found value',
|
||||
}),
|
||||
submissionValue: 'No longer found value',
|
||||
},
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackMoving',
|
||||
defaultMessage: 'Moving to a different solution',
|
||||
}),
|
||||
submissionValue: 'Moving to a different solution',
|
||||
},
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackMistake',
|
||||
defaultMessage: 'Created a workspace by mistake',
|
||||
}),
|
||||
submissionValue: 'Created a workspace by mistake',
|
||||
},
|
||||
{
|
||||
translatedMessage: props.intl.formatMessage({
|
||||
id: 'feedback.deleteWorkspace.feedbackHosting',
|
||||
defaultMessage: 'Moving to hosting my own Mattermost instance (self-hosted)',
|
||||
}),
|
||||
submissionValue: 'Moving to hosting my own Mattermost instance (self-hosted)',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {recycleDatabaseConnection} from 'actions/admin_actions.jsx';
|
||||
import {recycleDatabaseConnection, ping} from 'actions/admin_actions';
|
||||
import * as Utils from 'utils/utils';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
@@ -19,6 +19,15 @@ import TextSetting from './text_setting';
|
||||
import MigrationsTable from './database';
|
||||
|
||||
export default class DatabaseSettings extends AdminSettings {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
searchBackend: '',
|
||||
};
|
||||
}
|
||||
|
||||
getConfigFromState = (config) => {
|
||||
// driverName and dataSource are read-only from the UI
|
||||
|
||||
@@ -34,6 +43,17 @@ export default class DatabaseSettings extends AdminSettings {
|
||||
return config;
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getSearchBackend().then((searchBackend) => {
|
||||
this.setState({searchBackend});
|
||||
});
|
||||
}
|
||||
|
||||
async getSearchBackend() {
|
||||
const res = await ping()();
|
||||
return res.ActiveSearchBackend;
|
||||
}
|
||||
|
||||
getStateFromConfig(config) {
|
||||
return {
|
||||
driverName: config.SqlSettings.DriverName,
|
||||
@@ -368,6 +388,30 @@ export default class DatabaseSettings extends AdminSettings {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.database.search_backend.title'
|
||||
defaultMessage='Active Search Backend:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
value={this.state.searchBackend}
|
||||
disabled={true}
|
||||
/>
|
||||
<div className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.database.search_backend.help_text'
|
||||
defaultMessage='Shows the currently active backend used for search. Values can be none, database, elasticsearch, bleve etc.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,8 +7,14 @@ import {shallow} from 'enzyme';
|
||||
import DatabaseSettings from 'components/admin_console/database_settings.jsx';
|
||||
|
||||
jest.mock('actions/admin_actions.jsx', () => {
|
||||
const pingFn = () => {
|
||||
return jest.fn(() => {
|
||||
return {ActiveSearchBackend: 'none'};
|
||||
});
|
||||
};
|
||||
return {
|
||||
recycleDatabaseConnection: jest.fn(),
|
||||
ping: pingFn,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris
|
||||
admin: {
|
||||
config: {
|
||||
ServiceSettings: {
|
||||
SelfHostedExpansion: true,
|
||||
SelfHostedPurchase: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -59,13 +59,13 @@ const EnterpriseEditionLeftPanel = ({
|
||||
const canExpand = useCanSelfHostedExpand();
|
||||
const selfHostedExpansionModal = useControlSelfHostedExpansionModal({trackingLocation: 'license_settings_add_seats'});
|
||||
const expandableLink = useSelector(getExpandSeatsLink);
|
||||
const isSelfHostedExpansionEnabled = useSelector(getConfig)?.ServiceSettings?.SelfHostedExpansion;
|
||||
const isSelfHostedPurchaseEnabled = useSelector(getConfig)?.ServiceSettings?.SelfHostedPurchase;
|
||||
|
||||
const query = useQuery();
|
||||
const actionQueryParam = query.get('action');
|
||||
|
||||
useEffect(() => {
|
||||
if (actionQueryParam === 'show_expansion_modal') {
|
||||
if (actionQueryParam === 'show_expansion_modal' && canExpand && isSelfHostedPurchaseEnabled) {
|
||||
selfHostedExpansionModal.open();
|
||||
}
|
||||
}, []);
|
||||
@@ -99,7 +99,7 @@ const EnterpriseEditionLeftPanel = ({
|
||||
);
|
||||
|
||||
const handleClickAddSeats = () => {
|
||||
if (!isSelfHostedExpansionEnabled || !canExpand) {
|
||||
if (!isSelfHostedPurchaseEnabled || !canExpand) {
|
||||
window.open(expandableLink(unsanitizedLicense.Id), '_blank');
|
||||
} else {
|
||||
selfHostedExpansionModal.open();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect, useState, ReactNode} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
@@ -106,7 +106,7 @@ const TrialBanner = ({
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const btnText = (status: TrialLoadStatus): string => {
|
||||
const btnText = (status: TrialLoadStatus) => {
|
||||
switch (status) {
|
||||
case TrialLoadStatus.Started:
|
||||
return formatMessage({id: 'start_trial.modal.gettingTrial', defaultMessage: 'Getting Trial...'});
|
||||
@@ -115,7 +115,22 @@ const TrialBanner = ({
|
||||
case TrialLoadStatus.Failed:
|
||||
return formatMessage({id: 'start_trial.modal.failed', defaultMessage: 'Failed'});
|
||||
case TrialLoadStatus.Embargoed:
|
||||
return formatMessage({id: 'admin.license.trial-request.embargoed'});
|
||||
return formatMessage<ReactNode>(
|
||||
{
|
||||
id: 'admin.license.trial-request.embargoed',
|
||||
defaultMessage: 'We were unable to process the request due to limitations for embargoed countries. <link>Learn more in our documentation</link>, or reach out to legal@mattermost.com for questions around export limitations.',
|
||||
},
|
||||
{
|
||||
link: (text: string) => (
|
||||
<ExternalLink
|
||||
location='trial_banner'
|
||||
href={LicenseLinks.EMBARGOED_COUNTRIES}
|
||||
>
|
||||
{text}
|
||||
</ExternalLink>
|
||||
),
|
||||
},
|
||||
);
|
||||
default:
|
||||
return formatMessage({id: 'admin.license.trial-request.startTrial', defaultMessage: 'Start trial'});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Button Text"
|
||||
id="test"
|
||||
id="test2"
|
||||
/>
|
||||
</LoadingWrapper>
|
||||
</button>
|
||||
@@ -30,7 +30,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Help Text"
|
||||
id="test"
|
||||
id="test1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
buttonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Button Text"
|
||||
id="test"
|
||||
id="test2"
|
||||
/>
|
||||
}
|
||||
disabled={false}
|
||||
@@ -55,7 +55,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
helpText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Help Text"
|
||||
id="test"
|
||||
id="test1"
|
||||
/>
|
||||
}
|
||||
includeDetailedError={true}
|
||||
@@ -103,7 +103,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Button Text"
|
||||
id="test"
|
||||
id="test2"
|
||||
>
|
||||
<span>
|
||||
Button Text
|
||||
@@ -154,7 +154,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Help Text"
|
||||
id="test"
|
||||
id="test1"
|
||||
>
|
||||
<span>
|
||||
Help Text
|
||||
@@ -171,7 +171,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
buttonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Button Text"
|
||||
id="test"
|
||||
id="test2"
|
||||
/>
|
||||
}
|
||||
disabled={false}
|
||||
@@ -184,7 +184,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
helpText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Help Text"
|
||||
id="test"
|
||||
id="test1"
|
||||
/>
|
||||
}
|
||||
includeDetailedError={false}
|
||||
@@ -240,7 +240,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Button Text"
|
||||
id="test"
|
||||
id="test2"
|
||||
>
|
||||
<span>
|
||||
Button Text
|
||||
@@ -291,7 +291,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Help Text"
|
||||
id="test"
|
||||
id="test1"
|
||||
>
|
||||
<span>
|
||||
Help Text
|
||||
@@ -308,7 +308,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
buttonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Button Text"
|
||||
id="test"
|
||||
id="test2"
|
||||
/>
|
||||
}
|
||||
disabled={false}
|
||||
@@ -321,7 +321,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
helpText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Help Text"
|
||||
id="test"
|
||||
id="test1"
|
||||
/>
|
||||
}
|
||||
includeDetailedError={false}
|
||||
@@ -369,7 +369,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Button Text"
|
||||
id="test"
|
||||
id="test2"
|
||||
>
|
||||
<span>
|
||||
Button Text
|
||||
@@ -413,7 +413,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Help Text"
|
||||
id="test"
|
||||
id="test1"
|
||||
>
|
||||
<span>
|
||||
Help Text
|
||||
@@ -430,7 +430,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
buttonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Button Text"
|
||||
id="test"
|
||||
id="test2"
|
||||
/>
|
||||
}
|
||||
disabled={false}
|
||||
@@ -443,7 +443,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
helpText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Help Text"
|
||||
id="test"
|
||||
id="test1"
|
||||
/>
|
||||
}
|
||||
includeDetailedError={false}
|
||||
@@ -499,7 +499,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Button Text"
|
||||
id="test"
|
||||
id="test2"
|
||||
>
|
||||
<span>
|
||||
Button Text
|
||||
@@ -513,7 +513,7 @@ exports[`components/admin_console/request_button/request_button.jsx should match
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Help Text"
|
||||
id="test"
|
||||
id="test1"
|
||||
>
|
||||
<span>
|
||||
Help Text
|
||||
|
||||
@@ -18,13 +18,13 @@ describe('components/admin_console/request_button/request_button.jsx', () => {
|
||||
requestAction={emptyFunction}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test1'
|
||||
defaultMessage='Help Text'
|
||||
/>
|
||||
}
|
||||
buttonText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test2'
|
||||
defaultMessage='Button Text'
|
||||
/>
|
||||
}
|
||||
@@ -42,13 +42,13 @@ describe('components/admin_console/request_button/request_button.jsx', () => {
|
||||
requestAction={requestActionSuccess}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test1'
|
||||
defaultMessage='Help Text'
|
||||
/>
|
||||
}
|
||||
buttonText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test2'
|
||||
defaultMessage='Button Text'
|
||||
/>
|
||||
}
|
||||
@@ -72,13 +72,13 @@ describe('components/admin_console/request_button/request_button.jsx', () => {
|
||||
requestAction={requestActionSuccess}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test1'
|
||||
defaultMessage='Help Text'
|
||||
/>
|
||||
}
|
||||
buttonText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test2'
|
||||
defaultMessage='Button Text'
|
||||
/>
|
||||
}
|
||||
@@ -102,13 +102,13 @@ describe('components/admin_console/request_button/request_button.jsx', () => {
|
||||
requestAction={requestActionSuccess}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test1'
|
||||
defaultMessage='Help Text'
|
||||
/>
|
||||
}
|
||||
buttonText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test2'
|
||||
defaultMessage='Button Text'
|
||||
/>
|
||||
}
|
||||
@@ -129,13 +129,13 @@ describe('components/admin_console/request_button/request_button.jsx', () => {
|
||||
requestAction={requestActionSuccess}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test1'
|
||||
defaultMessage='Help Text'
|
||||
/>
|
||||
}
|
||||
buttonText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test2'
|
||||
defaultMessage='Button Text'
|
||||
/>
|
||||
}
|
||||
@@ -164,13 +164,13 @@ describe('components/admin_console/request_button/request_button.jsx', () => {
|
||||
requestAction={requestActionFailure}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test1'
|
||||
defaultMessage='Help Text'
|
||||
/>
|
||||
}
|
||||
buttonText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test2'
|
||||
defaultMessage='Button Text'
|
||||
/>
|
||||
}
|
||||
@@ -191,13 +191,13 @@ describe('components/admin_console/request_button/request_button.jsx', () => {
|
||||
requestAction={requestActionFailure}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test1'
|
||||
defaultMessage='Help Text'
|
||||
/>
|
||||
}
|
||||
buttonText={
|
||||
<FormattedMessage
|
||||
id='test'
|
||||
id='test2'
|
||||
defaultMessage='Button Text'
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -671,7 +671,7 @@ export default class SchemaAdminSettings extends React.PureComponent {
|
||||
};
|
||||
|
||||
handleGeneratedChange = (id, s) => {
|
||||
this.handleChange(id, s.replace('+', '-').replace('/', '_'));
|
||||
this.handleChange(id, s.replace(/\+/g, '-').replace(/\//g, '_'));
|
||||
};
|
||||
|
||||
handleChange = (id, value, confirm = false, doSubmit = false, warning = false) => {
|
||||
|
||||
@@ -95,7 +95,7 @@ export default class LogList extends React.PureComponent<Props, State> {
|
||||
);
|
||||
const level: JSX.Element = (
|
||||
<FormattedMessage
|
||||
id='admin.log.logLevel'
|
||||
id='admin.log.Level'
|
||||
defaultMessage='Level'
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -213,9 +213,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud restore sho
|
||||
content="name"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -242,6 +244,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud restore sho
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -457,9 +460,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
content="name"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -486,6 +491,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -603,9 +609,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
id="sharedTooltip"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -632,6 +640,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -834,9 +843,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
content="name"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -863,6 +874,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {shallow} from 'enzyme';
|
||||
|
||||
import ChipsList, {ChipsInfoType} from 'components/admin_console/workspace-optimization/chips_list';
|
||||
|
||||
import {ItemStatus} from './dashboard.data';
|
||||
import {ItemStatus} from './dashboard.type';
|
||||
|
||||
describe('components/admin_console/workspace-optimization/chips_list', () => {
|
||||
const overallScoreChips: ChipsInfoType = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Chip from 'components/common/chip/chip';
|
||||
|
||||
import {ItemStatus} from './dashboard.data';
|
||||
import {ItemStatus} from './dashboard.type';
|
||||
|
||||
import './dashboard.scss';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
@@ -14,56 +14,23 @@ import {
|
||||
AccountMultipleOutlineIcon,
|
||||
} from '@mattermost/compass-icons/components';
|
||||
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getLicense, getServerVersion} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {CloudLinks, ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {ConsolePages} from 'utils/constants';
|
||||
import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {runConfigChecks} from './dashboard_checks/config';
|
||||
import {DataModel, ItemStatus, Options} from './dashboard.type';
|
||||
import {runAccessChecks} from './dashboard_checks/access';
|
||||
import {runDataPrivacyChecks} from './dashboard_checks/data_privacy';
|
||||
import {runPerformanceChecks} from './dashboard_checks/performance';
|
||||
import {runEaseOfUseChecks} from './dashboard_checks/easy_management';
|
||||
import {runUpdateChecks} from './dashboard_checks/updates';
|
||||
|
||||
export type DataModel = {
|
||||
[key: string]: {
|
||||
title: string;
|
||||
description: string;
|
||||
descriptionOk: string;
|
||||
items: ItemModel[];
|
||||
icon: React.ReactNode;
|
||||
hide?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export enum ItemStatus {
|
||||
NONE = 'none',
|
||||
OK = 'ok',
|
||||
INFO = 'info',
|
||||
WARNING = 'warning',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export type ItemModel = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: ItemStatus;
|
||||
scoreImpact: number;
|
||||
impactModifier: number;
|
||||
configUrl?: string;
|
||||
configText?: string;
|
||||
telemetryAction?: string;
|
||||
infoUrl?: string;
|
||||
infoText?: string;
|
||||
}
|
||||
|
||||
export type UpdatesParam = {
|
||||
serverVersion: {
|
||||
type: string;
|
||||
status: ItemStatus;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
const impactModifiers: Record<ItemStatus, number> = {
|
||||
export const impactModifiers: Record<ItemStatus, number> = {
|
||||
[ItemStatus.NONE]: 1,
|
||||
[ItemStatus.OK]: 1,
|
||||
[ItemStatus.INFO]: 0.5,
|
||||
@@ -71,11 +38,191 @@ const impactModifiers: Record<ItemStatus, number> = {
|
||||
[ItemStatus.ERROR]: 0,
|
||||
};
|
||||
|
||||
const useMetricsData = () => {
|
||||
const getUpdatesData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.title',
|
||||
defaultMessage: 'Server updates',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.description',
|
||||
defaultMessage: 'An update is available.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.descriptionOk',
|
||||
defaultMessage: 'Your workspace is completely up to date!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ArrowUpBoldCircleOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runUpdateChecks(config, formatMessage, options),
|
||||
});
|
||||
|
||||
const getConfigurationData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.title',
|
||||
defaultMessage: 'Configuration',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.description',
|
||||
defaultMessage: 'You have configuration issues to resolve',
|
||||
}),
|
||||
hide: options.isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.descriptionOk',
|
||||
defaultMessage: 'You\'ve successfully configured SSL and Session Lengths!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<TuneIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runConfigChecks(config, formatMessage, options),
|
||||
});
|
||||
|
||||
const getAccessData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.title',
|
||||
defaultMessage: 'Workspace access',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.description',
|
||||
defaultMessage: 'Web server configuration may be affecting access to your Mattermost workspace.',
|
||||
}),
|
||||
hide: options.isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.descriptionOk',
|
||||
defaultMessage: 'Your web server configuration is passing a live URL test!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ServerVariantIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runAccessChecks(config, formatMessage),
|
||||
});
|
||||
|
||||
const getPerformanceData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.title',
|
||||
defaultMessage: 'Performance',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.description',
|
||||
defaultMessage: 'Your server would benefit from some performance tweaks.',
|
||||
}),
|
||||
hide: options.isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.descriptionOk',
|
||||
defaultMessage: 'Your search performance suits your workspace usage!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ChartLineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runPerformanceChecks(config, formatMessage, options),
|
||||
});
|
||||
|
||||
const getDataPrivacyData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.title',
|
||||
defaultMessage: 'Data privacy',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.description',
|
||||
defaultMessage: 'Get better insight and control over your data.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.descriptionOk',
|
||||
defaultMessage: 'You\'ve enabled data retention and compliance features!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<LockOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runDataPrivacyChecks(config, formatMessage, options),
|
||||
});
|
||||
|
||||
const getEaseOfManagementData = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.title',
|
||||
defaultMessage: 'Ease of management',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.description',
|
||||
defaultMessage: 'Make it easier to manage your Mattermost workspace.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.descriptionOk',
|
||||
defaultMessage: 'Your user authentication setup is appropriate based on your current usage!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<AccountMultipleOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: await runEaseOfUseChecks(config, formatMessage, options),
|
||||
});
|
||||
|
||||
const useMetricsData = (
|
||||
config: Partial<AdminConfig>,
|
||||
) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState<DataModel | undefined>(undefined);
|
||||
|
||||
const {formatMessage} = useIntl();
|
||||
const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense);
|
||||
const license = useSelector(getLicense);
|
||||
|
||||
// get the currently installed server version
|
||||
const installedVersion = useSelector((state: GlobalState) => getServerVersion(state));
|
||||
const analytics = useSelector((state: GlobalState) => state.entities.admin.analytics) as unknown as Options['analytics'];
|
||||
|
||||
const canStartTrial = license?.IsLicensed !== 'true' && prevTrialLicense?.IsLicensed !== 'true';
|
||||
const daysUntilExpiration = daysToLicenseExpire(license) || -1;
|
||||
|
||||
@@ -87,358 +234,46 @@ const useMetricsData = () => {
|
||||
|
||||
const [, contactSalesLink] = useOpenSalesLink();
|
||||
|
||||
const trialOrEnterpriseCtaConfig = {
|
||||
const trialOrEnterpriseCtaConfig = useMemo(() => ({
|
||||
configUrl: canStartTrial ? ConsolePages.LICENSE : contactSalesLink,
|
||||
configText: canStartTrial ? formatMessage({id: 'admin.reporting.workspace_optimization.cta.startTrial', defaultMessage: 'Start trial'}) : formatMessage({id: 'admin.reporting.workspace_optimization.cta.upgradeLicense', defaultMessage: 'Contact sales'}),
|
||||
}), [canStartTrial, contactSalesLink, formatMessage]);
|
||||
|
||||
const options: Options = useMemo(() => ({
|
||||
isLicensed,
|
||||
isEnterpriseLicense,
|
||||
trialOrEnterpriseCtaConfig,
|
||||
isStarterLicense,
|
||||
isCloud,
|
||||
analytics,
|
||||
installedVersion,
|
||||
}), [isLicensed, isEnterpriseLicense, trialOrEnterpriseCtaConfig, isStarterLicense, isCloud, analytics, installedVersion]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const refreshData = async () => {
|
||||
const data = {
|
||||
updates: await getUpdatesData(config, formatMessage, options),
|
||||
configuration: await getConfigurationData(config, formatMessage, options),
|
||||
access: await getAccessData(config, formatMessage, options),
|
||||
performance: await getPerformanceData(config, formatMessage, options),
|
||||
dataPrivacy: await getDataPrivacyData(config, formatMessage, options),
|
||||
easyManagement: await getEaseOfManagementData(config, formatMessage, options),
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
refreshData().then((data) => {
|
||||
setData(data);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [config, formatMessage, options]);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
};
|
||||
|
||||
const getUpdatesData = (data: UpdatesParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.title',
|
||||
defaultMessage: 'Server updates',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.description',
|
||||
defaultMessage: 'An update is available.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.descriptionOk',
|
||||
defaultMessage: 'Your workspace is completely up to date!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ArrowUpBoldCircleOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'server_version',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.status.title',
|
||||
defaultMessage: '{type} version update available.',
|
||||
}, {type: data.serverVersion.type}),
|
||||
description: data.serverVersion.description,
|
||||
configUrl: CloudLinks.DOWNLOAD_UPDATE,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.downloadUpdate', defaultMessage: 'Download update'}),
|
||||
infoUrl: DocLinks.UPGRADE_SERVER,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'server-version',
|
||||
status: data.serverVersion.status,
|
||||
scoreImpact: 15,
|
||||
impactModifier: impactModifiers[data.serverVersion.status],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type ConfigurationParam = {
|
||||
ssl: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
sessionLength: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
}
|
||||
|
||||
const getConfigurationData = (data: ConfigurationParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.title',
|
||||
defaultMessage: 'Configuration',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.description',
|
||||
defaultMessage: 'You have configuration issues to resolve',
|
||||
}),
|
||||
hide: isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.descriptionOk',
|
||||
defaultMessage: 'You\'ve successfully configured SSL and Session Lengths!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<TuneIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'ssl',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.ssl.title',
|
||||
defaultMessage: 'Configure SSL to make your server more secure',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.ssl.description',
|
||||
defaultMessage: 'We strongly recommend securing your Mattermost workspace by configuring SSL in production environments.',
|
||||
}),
|
||||
infoUrl: DocLinks.SSL_CERTIFICATE,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'ssl',
|
||||
status: data.ssl.status,
|
||||
scoreImpact: 25,
|
||||
impactModifier: impactModifiers[data.ssl.status],
|
||||
},
|
||||
{
|
||||
id: 'session-length',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.session_length.title',
|
||||
defaultMessage: 'Session lengths is set to default',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.session_length.description',
|
||||
defaultMessage: 'Your session length is set to the default of 30 days. A longer session length provides convenience, and a shorter session provides tighter security. We recommend adjusting this based on your organization\'s security policies.',
|
||||
}),
|
||||
configUrl: ConsolePages.SESSION_LENGTHS,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureSessionLength', defaultMessage: 'Configure session length'}),
|
||||
infoUrl: DocLinks.SESSION_LENGTHS,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'session-length',
|
||||
status: data.sessionLength.status,
|
||||
scoreImpact: 8,
|
||||
impactModifier: impactModifiers[data.sessionLength.status],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type AccessParam = {
|
||||
siteUrl: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
}
|
||||
|
||||
const getAccessData = (data: AccessParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.title',
|
||||
defaultMessage: 'Workspace access',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.description',
|
||||
defaultMessage: 'Web server configuration may be affecting access to your Mattermost workspace.',
|
||||
}),
|
||||
hide: isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.descriptionOk',
|
||||
defaultMessage: 'Your web server configuration is passing a live URL test!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ServerVariantIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'site-url',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.site_url.title',
|
||||
defaultMessage: 'Misconfigured web server',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.site_url.description',
|
||||
defaultMessage: 'Your web server settings aren\'t passing a live URL test which means your workspace may not be accessible to users. We recommend updating your web server settings.',
|
||||
}),
|
||||
configUrl: ConsolePages.WEB_SERVER,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureWebServer', defaultMessage: 'Configure web server'}),
|
||||
infoUrl: DocLinks.SITE_URL,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'site-url',
|
||||
status: data.siteUrl.status,
|
||||
scoreImpact: 12,
|
||||
impactModifier: impactModifiers[data.siteUrl.status],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type PerformanceParam = {
|
||||
search: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
}
|
||||
|
||||
const getPerformanceData = (data: PerformanceParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.title',
|
||||
defaultMessage: 'Performance',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.description',
|
||||
defaultMessage: 'Your server would benefit from some performance tweaks.',
|
||||
}),
|
||||
hide: isCloud,
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.descriptionOk',
|
||||
defaultMessage: 'Your search performance suits your workspace usage!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<ChartLineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'search',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.search.title',
|
||||
defaultMessage: 'Search performance',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.search.description',
|
||||
defaultMessage: 'Your server has reached over 500 users and 2 million posts which can result in slow search performance. We recommend enabling Elasticsearch for better performance.',
|
||||
}),
|
||||
...(isLicensed && isEnterpriseLicense ? {
|
||||
configUrl: ConsolePages.ELASTICSEARCH,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureElasticsearch', defaultMessage: 'Try Elasticsearch'}),
|
||||
} : trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.ELASTICSEARCH,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'search-optimization',
|
||||
status: data.search.status,
|
||||
scoreImpact: 20,
|
||||
impactModifier: impactModifiers[data.search.status],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type DataPrivacyParam = {
|
||||
retention: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
}
|
||||
|
||||
// TBD
|
||||
const getDataPrivacyData = (data: DataPrivacyParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.title',
|
||||
defaultMessage: 'Data privacy',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.description',
|
||||
defaultMessage: 'Get better insight and control over your data.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.descriptionOk',
|
||||
defaultMessage: 'You\'ve enabled data retention and compliance features!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<LockOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'data-retention',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.retention.title',
|
||||
defaultMessage: 'Become more data aware',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.retention.description',
|
||||
defaultMessage: 'Organizations in highly regulated industries require more control and insight with their data. We recommend enabling Data Retention and Compliance features.',
|
||||
}),
|
||||
...(isLicensed && isEnterpriseLicense ? {
|
||||
configUrl: ConsolePages.DATA_RETENTION,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureDataRetention', defaultMessage: 'Try data retention'}),
|
||||
} : trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.DATA_RETENTION_POLICY,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'data-retention',
|
||||
status: data.retention.status,
|
||||
scoreImpact: 16,
|
||||
impactModifier: impactModifiers[data.retention.status],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type EaseOfManagementParam = {
|
||||
ldap: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
guestAccounts?: {
|
||||
status: ItemStatus;
|
||||
};
|
||||
}
|
||||
|
||||
// TBD
|
||||
const getEaseOfManagementData = (data: EaseOfManagementParam) => ({
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.title',
|
||||
defaultMessage: 'Ease of management',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.description',
|
||||
defaultMessage: 'Make it easier to manage your Mattermost workspace.',
|
||||
}),
|
||||
descriptionOk: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.descriptionOk',
|
||||
defaultMessage: 'Your user authentication setup is appropriate based on your current usage!',
|
||||
}),
|
||||
icon: (
|
||||
<div className='icon'>
|
||||
<AccountMultipleOutlineIcon
|
||||
size={20}
|
||||
color={'var(--sys-center-channel-color)'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
items: [
|
||||
{
|
||||
id: 'ad-ldap',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.title',
|
||||
defaultMessage: 'AD/LDAP integration recommended',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.description',
|
||||
defaultMessage: 'You\'ve reached over 100 users! We recommend setting up AD/LDAP user authentication for easier onboarding as well as automated deactivations and role assignments.',
|
||||
}),
|
||||
...(isLicensed && !isStarterLicense ? {
|
||||
configUrl: ConsolePages.AD_LDAP,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureLDAP', defaultMessage: 'Try AD/LDAP'}),
|
||||
} : trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.AD_LDAP,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'ad-ldap',
|
||||
status: data.ldap.status,
|
||||
scoreImpact: 22,
|
||||
impactModifier: impactModifiers[data.ldap.status],
|
||||
},
|
||||
|
||||
// commented out for now.
|
||||
// @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385
|
||||
// {
|
||||
// id: 'guest-accounts',
|
||||
// title: formatMessage({
|
||||
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.title',
|
||||
// defaultMessage: 'Guest Accounts recommended',
|
||||
// }),
|
||||
// description: formatMessage({
|
||||
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.description',
|
||||
// defaultMessage: 'Several user accounts are using different domains than your Site URL. You can control user access to channels and teams with guest accounts. We recommend starting an Enterprise trial and enabling Guest Access.',
|
||||
// }),
|
||||
// ...trialOrEnterpriseCtaConfig,
|
||||
// infoUrl: 'https://docs.mattermost.com/onboard/guest-accounts.html',
|
||||
// infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
// telemetryAction: 'guest-accounts',
|
||||
// status: data.guestAccounts.status,
|
||||
// scoreImpact: 6,
|
||||
// impactModifier: impactModifiers[data.guestAccounts.status],
|
||||
// },
|
||||
],
|
||||
});
|
||||
|
||||
return {getAccessData, getConfigurationData, getUpdatesData, getPerformanceData, getDataPrivacyData, getEaseOfManagementData, isLicensed, isEnterpriseLicense};
|
||||
};
|
||||
|
||||
export default useMetricsData;
|
||||
|
||||
@@ -4,17 +4,11 @@
|
||||
import {CheckIcon} from '@mattermost/compass-icons/components';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
import {getServerVersion} from 'mattermost-redux/selectors/entities/general';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import Accordion, {AccordionItemType} from 'components/common/accordion/accordion';
|
||||
|
||||
import {elasticsearchTest, ldapTest, testSiteURL} from '../../../actions/admin_actions';
|
||||
import LoadingScreen from '../../loading_screen';
|
||||
import FormattedAdminHeader from '../../widgets/admin_console/formatted_admin_header';
|
||||
import {Props} from '../admin_console';
|
||||
@@ -22,10 +16,11 @@ import {Props} from '../admin_console';
|
||||
import ChipsList, {ChipsInfoType} from './chips_list';
|
||||
import CtaButtons from './cta_buttons';
|
||||
|
||||
import useMetricsData, {DataModel, ItemStatus, UpdatesParam} from './dashboard.data';
|
||||
import useMetricsData from './dashboard.data';
|
||||
|
||||
import './dashboard.scss';
|
||||
import OverallScore from './overall-score';
|
||||
import {ItemStatus} from './dashboard.type';
|
||||
|
||||
const AccordionItem = styled.div`
|
||||
padding: 12px;
|
||||
@@ -49,180 +44,7 @@ const successIcon = (
|
||||
);
|
||||
|
||||
const WorkspaceOptimizationDashboard = (props: Props) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [versionData, setVersionData] = useState<UpdatesParam['serverVersion']>({type: '', description: '', status: ItemStatus.NONE});
|
||||
|
||||
// const [guestAccountStatus, setGuestAccountStatus] = useState<ItemStatus>('none');
|
||||
const [liveUrlStatus, setLiveUrlStatus] = useState<ItemStatus>(ItemStatus.ERROR);
|
||||
const [elastisearchStatus, setElasticsearchStatus] = useState<ItemStatus>(ItemStatus.INFO);
|
||||
const [ldapStatus, setLdapStatus] = useState<ItemStatus>(ItemStatus.INFO);
|
||||
const [dataRetentionStatus, setDataRetentionStatus] = useState<ItemStatus>(ItemStatus.INFO);
|
||||
const {formatMessage} = useIntl();
|
||||
const {getAccessData, getConfigurationData, getUpdatesData, getPerformanceData, getDataPrivacyData, getEaseOfManagementData, isLicensed, isEnterpriseLicense} = useMetricsData();
|
||||
|
||||
// get the currently installed server version
|
||||
const installedVersion = useSelector((state: GlobalState) => getServerVersion(state));
|
||||
const analytics = useSelector((state: GlobalState) => state.entities.admin.analytics);
|
||||
const {TOTAL_USERS: totalUsers, TOTAL_POSTS: totalPosts} = analytics!;
|
||||
|
||||
// gather locally available data
|
||||
const {
|
||||
ServiceSettings,
|
||||
DataRetentionSettings,
|
||||
ElasticsearchSettings,
|
||||
LdapSettings,
|
||||
|
||||
// TeamSettings,
|
||||
// GuestAccountsSettings,
|
||||
} = props.config;
|
||||
const {location} = document;
|
||||
|
||||
const sessionLengthWebInHours = ServiceSettings?.SessionLengthWebInHours || -1;
|
||||
|
||||
const testURL = () => {
|
||||
if (!ServiceSettings?.SiteURL) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const onSuccess = ({status}: any) => setLiveUrlStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.ERROR);
|
||||
const onError = () => setLiveUrlStatus(ItemStatus.ERROR);
|
||||
return testSiteURL(onSuccess, onError, ServiceSettings?.SiteURL);
|
||||
};
|
||||
|
||||
const testDataRetention = async () => {
|
||||
if (!isLicensed || !isEnterpriseLicense) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (DataRetentionSettings?.EnableMessageDeletion || DataRetentionSettings?.EnableFileDeletion) {
|
||||
setDataRetentionStatus(ItemStatus.OK);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const result = await fetch(`${Client4.getBaseRoute()}/data_retention/policies?page=0&per_page=0`).then((result) => result.json());
|
||||
|
||||
setDataRetentionStatus(result.total_count > 0 ? ItemStatus.OK : ItemStatus.INFO);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const fetchVersion = async () => {
|
||||
const result = await fetch(`${Client4.getBaseRoute()}/latest_version`).then((result) => result.json());
|
||||
|
||||
if (result.tag_name) {
|
||||
const sanitizedVersion = result.tag_name.startsWith('v') ? result.tag_name.slice(1) : result.tag_name;
|
||||
const newVersionParts = sanitizedVersion.split('.');
|
||||
const installedVersionParts = installedVersion.split('.').slice(0, 3);
|
||||
|
||||
// quick general check if a newer version is available
|
||||
let type = '';
|
||||
let status: ItemStatus = ItemStatus.OK;
|
||||
|
||||
if (newVersionParts.join('') > installedVersionParts.join('')) {
|
||||
// get correct values to be inserted into the accordion item
|
||||
switch (true) {
|
||||
case newVersionParts[0] > installedVersionParts[0]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.major',
|
||||
defaultMessage: 'Major',
|
||||
});
|
||||
status = ItemStatus.ERROR;
|
||||
break;
|
||||
case newVersionParts[1] > installedVersionParts[1]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.minor',
|
||||
defaultMessage: 'Minor',
|
||||
});
|
||||
status = ItemStatus.WARNING;
|
||||
break;
|
||||
case newVersionParts[2] > installedVersionParts[2]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.patch',
|
||||
defaultMessage: 'Patch',
|
||||
});
|
||||
status = ItemStatus.INFO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setVersionData({type, description: result.body, status});
|
||||
}
|
||||
};
|
||||
|
||||
const testElasticsearch = () => {
|
||||
if (!isLicensed || !isEnterpriseLicense || !(ElasticsearchSettings?.EnableIndexing && ElasticsearchSettings?.EnableSearching)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const onSuccess = ({status}: any) => setElasticsearchStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.INFO);
|
||||
const onError = () => setElasticsearchStatus(ItemStatus.INFO);
|
||||
|
||||
return elasticsearchTest(props.config, onSuccess, onError);
|
||||
};
|
||||
|
||||
const testLdap = () => {
|
||||
if (!isLicensed || !LdapSettings?.Enable) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const onSuccess = ({status}: any) => setLdapStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.INFO);
|
||||
const onError = () => setLdapStatus(ItemStatus.INFO);
|
||||
|
||||
return ldapTest(onSuccess, onError);
|
||||
};
|
||||
|
||||
// commented out for now.
|
||||
// @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385
|
||||
// const fetchGuestAccounts = async () => {
|
||||
// if (TeamSettings?.EnableOpenServer && GuestAccountsSettings?.Enable) {
|
||||
// let usersArray = await fetch(`${Client4.getBaseRoute()}/users/invalid_emails`).then((result) => result.json());
|
||||
//
|
||||
// // this setting is just a string with a list of domains, or an empty string
|
||||
// if (GuestAccountsSettings?.RestrictCreationToDomains) {
|
||||
// const domainList = GuestAccountsSettings?.RestrictCreationToDomains;
|
||||
// usersArray = usersArray.filter(({email}: Record<string, unknown>) => domainList.includes((email as string).split('@')[1]));
|
||||
// }
|
||||
//
|
||||
// // if guest accounts make up more than 5% of the user base show the info accordion
|
||||
// if (usersArray.length > (totalUsers as number * 0.05)) {
|
||||
// setGuestAccountStatus(ItemStatus.INFO);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// setGuestAccountStatus(ItemStatus.OK);
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
const promises = [];
|
||||
promises.push(testURL());
|
||||
promises.push(testLdap());
|
||||
promises.push(fetchVersion());
|
||||
promises.push(testElasticsearch());
|
||||
promises.push(testDataRetention());
|
||||
|
||||
// promises.push(fetchGuestAccounts());
|
||||
Promise.all(promises).then(() => setLoading(false));
|
||||
}, [props.config, isLicensed, isEnterpriseLicense]);
|
||||
|
||||
const data: DataModel = {
|
||||
updates: getUpdatesData({serverVersion: versionData}),
|
||||
configuration: getConfigurationData({
|
||||
ssl: {status: location.protocol === 'https:' ? ItemStatus.OK : ItemStatus.ERROR},
|
||||
sessionLength: {status: sessionLengthWebInHours === 720 ? ItemStatus.INFO : ItemStatus.OK},
|
||||
}),
|
||||
access: getAccessData({siteUrl: {status: liveUrlStatus}}),
|
||||
performance: getPerformanceData({
|
||||
search: {
|
||||
status: totalPosts < 2_000_000 && totalUsers < 500 ? ItemStatus.OK : elastisearchStatus,
|
||||
},
|
||||
}),
|
||||
dataPrivacy: getDataPrivacyData({retention: {status: dataRetentionStatus}}),
|
||||
easyManagement: getEaseOfManagementData({
|
||||
ldap: {status: totalUsers < 100 ? ItemStatus.OK : ldapStatus},
|
||||
|
||||
// guestAccounts: {status: guestAccountStatus},
|
||||
}),
|
||||
};
|
||||
const {data, loading} = useMetricsData(props.config);
|
||||
|
||||
const overallScoreChips: ChipsInfoType = {
|
||||
[ItemStatus.INFO]: 0,
|
||||
@@ -236,7 +58,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => {
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const accData: AccordionItemType[] = Object.entries(data).filter(([_, y]) => !y.hide).map(([accordionKey, accordionData]) => {
|
||||
const accordionItemsData: AccordionItemType[] | undefined = data && Object.entries(data).filter(([_, y]) => !y.hide).map(([accordionKey, accordionData]) => {
|
||||
const accordionDataChips: ChipsInfoType = {
|
||||
[ItemStatus.INFO]: 0,
|
||||
[ItemStatus.WARNING]: 0,
|
||||
@@ -297,7 +119,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => {
|
||||
};
|
||||
});
|
||||
|
||||
return loading ? <LoadingScreen/> : (
|
||||
return loading || !accordionItemsData ? <LoadingScreen/> : (
|
||||
<div className='WorkspaceOptimizationDashboard wrapper--fixed'>
|
||||
<FormattedAdminHeader
|
||||
id={'admin.reporting.workspace_optimization.title'}
|
||||
@@ -314,7 +136,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => {
|
||||
chartValue={Math.floor((overallScore.current / overallScore.max) * 100)}
|
||||
/>
|
||||
<Accordion
|
||||
accordionItemsData={accData}
|
||||
accordionItemsData={accordionItemsData}
|
||||
expandMultiple={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type DataModel = {
|
||||
[key: string]: {
|
||||
title: string;
|
||||
description: string;
|
||||
descriptionOk: string;
|
||||
items: ItemModel[];
|
||||
icon: React.ReactNode;
|
||||
hide?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export enum ItemStatus {
|
||||
|
||||
/** Return NONE if it's not relevant, not configured, or not an option */
|
||||
NONE = 'none',
|
||||
|
||||
/** Return OK if all checks for this have passed */
|
||||
OK = 'ok',
|
||||
|
||||
/** Return info if it might not be relevant to them, but they could utilize it */
|
||||
INFO = 'info',
|
||||
WARNING = 'warning',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export type ItemModel = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: ItemStatus;
|
||||
scoreImpact: number;
|
||||
impactModifier: number;
|
||||
configUrl?: string;
|
||||
configText?: string;
|
||||
telemetryAction?: string;
|
||||
infoUrl?: string;
|
||||
infoText?: string;
|
||||
}
|
||||
|
||||
export type UpdatesParam = {
|
||||
serverVersion: {
|
||||
type: string;
|
||||
status: ItemStatus;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
type Analytics = {
|
||||
DAILY_ACTIVE_USERS: number;
|
||||
MONTHLY_ACTIVE_USERS: number;
|
||||
TOTAL_INACTIVE_USERS: number;
|
||||
TOTAL_MASTER_DB_CONNECTIONS: number;
|
||||
TOTAL_POSTS: number;
|
||||
TOTAL_PRIVATE_GROUPS: number;
|
||||
TOTAL_PUBLIC_CHANNELS: number;
|
||||
TOTAL_READ_DB_CONNECTIONS: number;
|
||||
TOTAL_TEAMS: number;
|
||||
TOTAL_USERS: number;
|
||||
TOTAL_WEBSOCKET_CONNECTIONS: number;
|
||||
}
|
||||
export type Options = {
|
||||
isLicensed: boolean;
|
||||
isEnterpriseLicense: boolean;
|
||||
trialOrEnterpriseCtaConfig: {
|
||||
configUrl: string;
|
||||
configText: string;
|
||||
};
|
||||
isCloud: boolean;
|
||||
isStarterLicense: boolean;
|
||||
analytics: Analytics | undefined;
|
||||
installedVersion: string;
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ItemModel, ItemStatus} from '../dashboard.type';
|
||||
import {ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {testSiteURL} from 'actions/admin_actions';
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Checking to see if the siteURL is configured correctly by running it through the same "check siteURL" button that exists on the webserver page.
|
||||
*/
|
||||
const siteURLCheck = async (config: Partial<AdminConfig>, formatMessage: ReturnType<typeof useIntl>['formatMessage']): Promise<ItemModel> => {
|
||||
let status = ItemStatus.OK;
|
||||
const testURL = async () => {
|
||||
if (!config.ServiceSettings?.SiteURL) {
|
||||
status = ItemStatus.ERROR;
|
||||
}
|
||||
|
||||
const onSuccess = ({status: s}: any) => {
|
||||
if (s === 'OK') {
|
||||
status = ItemStatus.OK;
|
||||
}
|
||||
};
|
||||
const onError = () => {
|
||||
status = ItemStatus.ERROR;
|
||||
};
|
||||
await testSiteURL(onSuccess, onError, config.ServiceSettings?.SiteURL);
|
||||
};
|
||||
|
||||
await testURL();
|
||||
return {
|
||||
id: 'site-url',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.site_url.title',
|
||||
defaultMessage: 'Misconfigured web server',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.access.site_url.description',
|
||||
defaultMessage: 'Your web server settings aren\'t passing a live URL test which means your workspace may not be accessible to users. We recommend updating your web server settings.',
|
||||
}),
|
||||
configUrl: ConsolePages.WEB_SERVER,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.access.site_url.cta', defaultMessage: 'Configure web server'}),
|
||||
infoUrl: DocLinks.SITE_URL,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'site-url',
|
||||
status,
|
||||
scoreImpact: 12,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
const checks = [
|
||||
siteURLCheck,
|
||||
];
|
||||
|
||||
export const runAccessChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
) => {
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage)));
|
||||
return results;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {ItemModel, ItemStatus, Options} from '../dashboard.type';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
|
||||
/**
|
||||
*
|
||||
* @description This checks to see if the user's active session is done over https. This does not check if the server is configured to use https.
|
||||
*/
|
||||
const ssl = (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
): ItemModel => {
|
||||
const status = document.location.protocol === 'https:' ? ItemStatus.OK : ItemStatus.ERROR;
|
||||
|
||||
return {
|
||||
id: 'ssl',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.ssl.title',
|
||||
defaultMessage: 'Configure SSL to make your server more secure',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.ssl.description',
|
||||
defaultMessage: 'We strongly recommend securing your Mattermost workspace by configuring SSL in production environments.',
|
||||
}),
|
||||
infoUrl: DocLinks.SSL_CERTIFICATE,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'ssl',
|
||||
status,
|
||||
scoreImpact: 25,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @description This checks to see if the user has adjusted the default session lengths to something other than 720 hours.
|
||||
*/
|
||||
const sessionLength = (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
): ItemModel => {
|
||||
const status = config.ServiceSettings?.SessionLengthMobileInHours === 720 ? ItemStatus.INFO : ItemStatus.OK;
|
||||
return {
|
||||
id: 'session-length',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.session_length.title',
|
||||
defaultMessage: 'Session lengths is set to default',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.configuration.session_length.description',
|
||||
defaultMessage: 'Your session length is set to the default of 30 days. A longer session length provides convenience, and a shorter session provides tighter security. We recommend adjusting this based on your organization\'s security policies.',
|
||||
}),
|
||||
configUrl: ConsolePages.SESSION_LENGTHS,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.configuration.session_length.cta', defaultMessage: 'Configure session length'}),
|
||||
infoUrl: DocLinks.SESSION_LENGTHS,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'session-length',
|
||||
status,
|
||||
scoreImpact: 8,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
export const runConfigChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const checks = [
|
||||
ssl,
|
||||
sessionLength,
|
||||
];
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||
return results;
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useIntl} from 'react-intl';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
import {ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {ItemStatus, Options} from '../dashboard.type';
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Checks if they they have a global policy deletion enabled, or if a custom policy has been created.
|
||||
*/
|
||||
const dataRetentionCheck = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const testDataRetention = async (
|
||||
config: Partial<AdminConfig>,
|
||||
options: Options,
|
||||
) => {
|
||||
if (!options.isLicensed || !options.isEnterpriseLicense) {
|
||||
return ItemStatus.INFO;
|
||||
}
|
||||
|
||||
if (config.DataRetentionSettings?.EnableMessageDeletion || config.DataRetentionSettings?.EnableFileDeletion) {
|
||||
return ItemStatus.OK;
|
||||
}
|
||||
|
||||
const policyCount: {total_count: number} = await fetch(`${Client4.getBaseRoute()}/data_retention/policies_count`).then((result) => result.json());
|
||||
return policyCount.total_count > 0 ? ItemStatus.OK : ItemStatus.INFO;
|
||||
};
|
||||
|
||||
const status = await testDataRetention(config, options);
|
||||
return {
|
||||
id: 'data-retention',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.retention.title',
|
||||
defaultMessage: 'Become more data aware',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.data_privacy.retention.description',
|
||||
defaultMessage: 'Organizations in highly regulated industries require more control and insight with their data. We recommend enabling Data Retention and Compliance features.',
|
||||
}),
|
||||
...(options.isLicensed && options.isEnterpriseLicense ? {
|
||||
configUrl: ConsolePages.DATA_RETENTION,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.data_privacy.retention.cta', defaultMessage: 'Try data retention'}),
|
||||
} : options.trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.DATA_RETENTION_POLICY,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'data-retention',
|
||||
status,
|
||||
scoreImpact: 16,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
export const runDataPrivacyChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const checks = [
|
||||
dataRetentionCheck,
|
||||
];
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||
return results;
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ItemModel, ItemStatus, Options} from '../dashboard.type';
|
||||
import {ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
|
||||
// import {Client4} from 'mattermost-redux/client';
|
||||
// import {AnalyticsRow} from '@mattermost/types/admin';
|
||||
import {ldapTest} from 'actions/admin_actions';
|
||||
|
||||
const usesLDAP = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
): Promise<ItemModel> => {
|
||||
const testLdap = async (
|
||||
config: Partial<AdminConfig>,
|
||||
options: Options,
|
||||
): Promise<ItemStatus> => {
|
||||
let check = ItemStatus.INFO;
|
||||
|
||||
if (!options.isLicensed || !config.LdapSettings?.Enable) {
|
||||
return check;
|
||||
}
|
||||
|
||||
const onSuccess = ({status}: any) => {
|
||||
if (status === 'OK') {
|
||||
check = ItemStatus.OK;
|
||||
}
|
||||
};
|
||||
|
||||
await ldapTest(onSuccess);
|
||||
|
||||
return check;
|
||||
};
|
||||
|
||||
// something feels flawed in this check.
|
||||
const status = options.analytics?.TOTAL_USERS as number > 100 ? await testLdap(config, options) : ItemStatus.OK;
|
||||
|
||||
return {
|
||||
id: 'ad-ldap',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.title',
|
||||
defaultMessage: 'AD/LDAP integration recommended',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.description',
|
||||
defaultMessage: 'You\'ve reached over 100 users! We recommend setting up AD/LDAP user authentication for easier onboarding as well as automated deactivations and role assignments.',
|
||||
}),
|
||||
...(options.isLicensed && !options.isStarterLicense ? {
|
||||
configUrl: ConsolePages.AD_LDAP,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.cta', defaultMessage: 'Try AD/LDAP'}),
|
||||
} : options.trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.AD_LDAP,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'ad-ldap',
|
||||
status,
|
||||
scoreImpact: 22,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
// // commented out for now.
|
||||
// // @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385
|
||||
// const fetchGuestAccounts = async (
|
||||
// config: Partial<AdminConfig>,
|
||||
// analytics: Record<string, number | AnalyticsRow[]> | undefined,
|
||||
// ) => {
|
||||
// if (config.TeamSettings?.EnableOpenServer && config.GuestAccountsSettings?.Enable) {
|
||||
// let usersArray = await fetch(`${Client4.getBaseRoute()}/users/invalid_emails`).then((result) => result.json());
|
||||
|
||||
// // this setting is just a string with a list of domains, or an empty string
|
||||
// if (config.GuestAccountsSettings?.RestrictCreationToDomains) {
|
||||
// const domainList = config.GuestAccountsSettings?.RestrictCreationToDomains;
|
||||
// usersArray = usersArray.filter(({email}: Record<string, unknown>) => domainList.includes((email as string).split('@')[1]));
|
||||
// }
|
||||
|
||||
// // if guest accounts make up more than 5% of the user base show the info accordion
|
||||
// if (analytics && usersArray.length > (analytics.totalUsers as number * 0.05)) {
|
||||
// return ItemStatus.INFO;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return ItemStatus.OK;
|
||||
// };
|
||||
|
||||
// const guestAccounts = async (
|
||||
// config: Partial<AdminConfig>,
|
||||
// formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
// options: Options,
|
||||
// ): Promise<ItemModel> => {
|
||||
// const status = await fetchGuestAccounts(config, options.analytics);
|
||||
// return {
|
||||
// id: 'guest-accounts',
|
||||
// title: formatMessage({
|
||||
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.title',
|
||||
// defaultMessage: 'Guest Accounts recommended',
|
||||
// }),
|
||||
// description: formatMessage({
|
||||
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.description',
|
||||
// defaultMessage: 'Several user accounts are using different domains than your Site URL. You can control user access to channels and teams with guest accounts. We recommend starting an Enterprise trial and enabling Guest Access.',
|
||||
// }),
|
||||
// ...options.trialOrEnterpriseCtaConfig,
|
||||
// infoUrl: 'https://docs.mattermost.com/onboard/guest-accounts.html',
|
||||
// infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
// telemetryAction: 'guest-accounts',
|
||||
// status,
|
||||
// scoreImpact: 6,
|
||||
// impactModifier: impactModifiers[status],
|
||||
// };
|
||||
// };
|
||||
|
||||
export const runEaseOfUseChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
): Promise<ItemModel[]> => {
|
||||
const checks = [
|
||||
usesLDAP,
|
||||
|
||||
// guestAccounts,
|
||||
];
|
||||
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||
return results;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {ItemModel, ItemStatus, Options} from '../dashboard.type';
|
||||
import {elasticsearchTest} from 'actions/admin_actions';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ConsolePages, DocLinks} from 'utils/constants';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
|
||||
const search = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
): Promise<ItemModel> => {
|
||||
const testElasticsearch = async (
|
||||
config: Partial<AdminConfig>,
|
||||
options: Options,
|
||||
) => {
|
||||
let check = ItemStatus.INFO;
|
||||
|
||||
if (!options.isLicensed || !options.isEnterpriseLicense || !(config.ElasticsearchSettings?.EnableIndexing && config.ElasticsearchSettings?.EnableSearching)) {
|
||||
return check;
|
||||
}
|
||||
|
||||
const onSuccess = ({status}: any) => {
|
||||
if (status === 'OK') {
|
||||
check = ItemStatus.OK;
|
||||
}
|
||||
};
|
||||
await elasticsearchTest(config, onSuccess);
|
||||
return check;
|
||||
};
|
||||
|
||||
const totalPosts = options.analytics?.TOTAL_POSTS as number;
|
||||
const totalUsers = options.analytics?.TOTAL_USERS as number;
|
||||
const status = totalPosts < 2_000_000 && totalUsers < 500 ? ItemStatus.OK : await testElasticsearch(config, options);
|
||||
return {
|
||||
id: 'search',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.search.title',
|
||||
defaultMessage: 'Search performance',
|
||||
}),
|
||||
description: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.performance.search.description',
|
||||
defaultMessage: 'Your server has reached over 500 users and 2 million posts which can result in slow search performance. We recommend enabling Elasticsearch for better performance.',
|
||||
}),
|
||||
...(options.isLicensed && options.isEnterpriseLicense ? {
|
||||
configUrl: ConsolePages.ELASTICSEARCH,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.search.cta', defaultMessage: 'Try Elasticsearch'}),
|
||||
} : options.trialOrEnterpriseCtaConfig),
|
||||
infoUrl: DocLinks.ELASTICSEARCH,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'search-optimization',
|
||||
status,
|
||||
scoreImpact: 20,
|
||||
impactModifier: impactModifiers[status],
|
||||
};
|
||||
};
|
||||
|
||||
export const runPerformanceChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const checks = [
|
||||
search,
|
||||
];
|
||||
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||
return results;
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {ItemStatus, Options} from '../dashboard.type';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {CloudLinks, DocLinks} from 'utils/constants';
|
||||
import {impactModifiers} from '../dashboard.data';
|
||||
|
||||
const testServerVersion = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const fetchVersion = async (
|
||||
installedVersion: string,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
) => {
|
||||
const result = await fetch(`${Client4.getBaseRoute()}/latest_version`).then((result) => result.json());
|
||||
|
||||
if (result.tag_name) {
|
||||
const sanitizedVersion = result.tag_name.startsWith('v') ? result.tag_name.slice(1) : result.tag_name;
|
||||
const newVersionParts = sanitizedVersion.split('.');
|
||||
const installedVersionParts = installedVersion.split('.').slice(0, 3);
|
||||
|
||||
// quick general check if a newer version is available
|
||||
let type = '';
|
||||
let status: ItemStatus = ItemStatus.OK;
|
||||
|
||||
if (newVersionParts.join('') > installedVersionParts.join('')) {
|
||||
// get correct values to be inserted into the accordion item
|
||||
switch (true) {
|
||||
case newVersionParts[0] > installedVersionParts[0]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.major',
|
||||
defaultMessage: 'Major',
|
||||
});
|
||||
status = ItemStatus.ERROR;
|
||||
break;
|
||||
case newVersionParts[1] > installedVersionParts[1]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.minor',
|
||||
defaultMessage: 'Minor',
|
||||
});
|
||||
status = ItemStatus.WARNING;
|
||||
break;
|
||||
case newVersionParts[2] > installedVersionParts[2]:
|
||||
type = formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.patch',
|
||||
defaultMessage: 'Patch',
|
||||
});
|
||||
status = ItemStatus.INFO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {type, description: result.body, status};
|
||||
}
|
||||
|
||||
return {type: '', description: '', status: ItemStatus.OK};
|
||||
};
|
||||
|
||||
const serverVersion = await fetchVersion(options.installedVersion, formatMessage);
|
||||
return {
|
||||
id: 'server_version',
|
||||
title: formatMessage({
|
||||
id: 'admin.reporting.workspace_optimization.updates.server_version.status.title',
|
||||
defaultMessage: '{type} version update available.',
|
||||
}, {type: serverVersion.type}),
|
||||
description: serverVersion.description,
|
||||
configUrl: CloudLinks.DOWNLOAD_UPDATE,
|
||||
configText: formatMessage({id: 'admin.reporting.workspace_optimization.updates.server_version.cta', defaultMessage: 'Download update'}),
|
||||
infoUrl: DocLinks.UPGRADE_SERVER,
|
||||
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
|
||||
telemetryAction: 'server-version',
|
||||
status: serverVersion.status,
|
||||
scoreImpact: 15,
|
||||
impactModifier: impactModifiers[serverVersion.status],
|
||||
};
|
||||
};
|
||||
|
||||
export const runUpdateChecks = async (
|
||||
config: Partial<AdminConfig>,
|
||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||
options: Options,
|
||||
) => {
|
||||
const checks = [
|
||||
testServerVersion,
|
||||
];
|
||||
|
||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||
return results;
|
||||
};
|
||||
@@ -40,6 +40,7 @@ describe('components/AdvancedCreateComment', () => {
|
||||
uploadsInProgress: [{}],
|
||||
fileInfos: [{}, {}, {}],
|
||||
},
|
||||
isRemoteDraft: false,
|
||||
enableAddButton: true,
|
||||
ctrlSend: false,
|
||||
latestPostId,
|
||||
@@ -84,9 +85,10 @@ describe('components/AdvancedCreateComment', () => {
|
||||
|
||||
test('should match snapshot, empty comment', () => {
|
||||
const draft = emptyDraft;
|
||||
const isRemoteDraft = false;
|
||||
const enableAddButton = false;
|
||||
const ctrlSend = true;
|
||||
const props = {...baseProps, draft, enableAddButton, ctrlSend};
|
||||
const props = {...baseProps, draft, isRemoteDraft, enableAddButton, ctrlSend};
|
||||
|
||||
const wrapper = shallow(
|
||||
<AdvancedCreateComment {...props}/>,
|
||||
@@ -104,8 +106,9 @@ describe('components/AdvancedCreateComment', () => {
|
||||
uploadsInProgress: [],
|
||||
fileInfos: [],
|
||||
};
|
||||
const isRemoteDraft = false;
|
||||
const ctrlSend = true;
|
||||
const props = {...baseProps, ctrlSend, draft, clearCommentDraftUploads, onResetHistoryIndex, getChannelMemberCountsByGroup};
|
||||
const props = {...baseProps, ctrlSend, draft, isRemoteDraft, clearCommentDraftUploads, onResetHistoryIndex, getChannelMemberCountsByGroup};
|
||||
|
||||
const wrapper = shallow(
|
||||
<AdvancedCreateComment {...props}/>,
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as GlobalActions from 'actions/global_actions';
|
||||
|
||||
import Constants, {AdvancedTextEditor as AdvancedTextEditorConst, Locations, ModalIdentifiers, Preferences} from 'utils/constants';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import * as Utils from 'utils/utils';
|
||||
import {
|
||||
@@ -73,6 +74,9 @@ type Props = {
|
||||
// The current draft of the comment
|
||||
draft: PostDraft;
|
||||
|
||||
// Data used for knowing if the draft came from a WS event
|
||||
isRemoteDraft: boolean;
|
||||
|
||||
// Determines if the submit button should be rendered
|
||||
enableAddButton?: boolean;
|
||||
|
||||
@@ -232,8 +236,14 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
|
||||
const rootChanged = props.rootId !== state.rootId;
|
||||
const messageInHistoryChanged = props.messageInHistory !== state.messageInHistory;
|
||||
if (rootChanged || messageInHistoryChanged || props.draft.remote) {
|
||||
updatedState = {...updatedState, draft: {...props.draft, uploadsInProgress: rootChanged ? [] : props.draft.uploadsInProgress}};
|
||||
if (rootChanged || messageInHistoryChanged || (props.isRemoteDraft && props.draft.message !== state.draft?.message)) {
|
||||
updatedState = {
|
||||
...updatedState,
|
||||
draft: {
|
||||
...props.draft,
|
||||
uploadsInProgress: rootChanged ? [] : props.draft.uploadsInProgress,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return updatedState;
|
||||
@@ -251,6 +261,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
serverError: null,
|
||||
showFormat: false,
|
||||
isFormattingBarHidden: props.isFormattingBarHidden,
|
||||
caretPosition: props.draft.caretPosition,
|
||||
};
|
||||
|
||||
this.textboxRef = React.createRef();
|
||||
@@ -342,7 +353,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
const updatedDraft = {
|
||||
...this.state.draft,
|
||||
show: !isDraftEmpty(this.state.draft),
|
||||
remote: false,
|
||||
} as PostDraft;
|
||||
|
||||
this.props.onUpdateCommentDraft(updatedDraft, true);
|
||||
@@ -355,7 +365,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
draft: {
|
||||
...prev.draft,
|
||||
show: !isDraftEmpty(prev.draft),
|
||||
remote: false,
|
||||
} as PostDraft,
|
||||
};
|
||||
}
|
||||
@@ -819,11 +828,11 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
|
||||
handleKeyDown = (e: React.KeyboardEvent<TextboxElement>) => {
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Utils.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
|
||||
const ctrlKeyCombo = Utils.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Utils.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Utils.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Keyboard.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
|
||||
// listen for line break key combo and insert new line character
|
||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||
@@ -838,7 +847,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
|
||||
if (
|
||||
(this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) &&
|
||||
Utils.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
(e.ctrlKey || e.metaKey)
|
||||
) {
|
||||
this.setShowPreview(false);
|
||||
@@ -849,7 +858,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
const draft = this.state.draft!;
|
||||
const {message} = draft;
|
||||
|
||||
if (Utils.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
this.textboxRef.current?.blur();
|
||||
}
|
||||
|
||||
@@ -858,7 +867,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
Utils.isKeyPressed(e, KeyCodes.UP) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.UP) &&
|
||||
message === ''
|
||||
) {
|
||||
e.preventDefault();
|
||||
@@ -879,13 +888,13 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
} = e.target as TextboxElement;
|
||||
|
||||
if (ctrlKeyCombo) {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.UP)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.UP)) {
|
||||
e.preventDefault();
|
||||
this.props.onMoveHistoryIndexBack();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
e.preventDefault();
|
||||
this.props.onMoveHistoryIndexForward();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.B)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.B)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -894,7 +903,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.I)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.I)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -905,7 +914,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
} else if (ctrlAltCombo) {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.K)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -914,7 +923,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.C)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.C)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -923,21 +932,21 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.E)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleEmojiPicker();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.T)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleAdvanceTextEditor();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.P) && draft.message.length) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.P) && draft.message.length) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setShowPreview(!this.props.shouldShowPreview);
|
||||
}
|
||||
} else if (shiftAltCombo) {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.X)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.X)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -946,7 +955,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ol',
|
||||
@@ -954,7 +963,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ul',
|
||||
@@ -962,7 +971,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'quote',
|
||||
|
||||
@@ -64,6 +64,7 @@ function makeMapStateToProps() {
|
||||
const err = state.requests.posts.createPost.error || {};
|
||||
|
||||
const draft = getPostDraft(state, StoragePrefixes.COMMENT_DRAFT, ownProps.rootId);
|
||||
const isRemoteDraft = state.views.drafts.remotes[`${StoragePrefixes.COMMENT_DRAFT}${ownProps.rootId}`] || false;
|
||||
|
||||
const channelMembersCount = getAllChannelStats(state)[ownProps.channelId] ? getAllChannelStats(state)[ownProps.channelId].member_count : 1;
|
||||
const messageInHistory = getMessageInHistoryItem(state);
|
||||
@@ -91,6 +92,7 @@ function makeMapStateToProps() {
|
||||
return {
|
||||
currentTeamId,
|
||||
draft,
|
||||
isRemoteDraft,
|
||||
messageInHistory,
|
||||
channelMembersCount,
|
||||
currentUserId,
|
||||
@@ -121,11 +123,11 @@ function makeMapStateToProps() {
|
||||
}
|
||||
|
||||
function makeOnUpdateCommentDraft(rootId: string, channelId: string) {
|
||||
return (draft?: PostDraft, save = false) => updateCommentDraft(rootId, draft ? {...draft, channelId, remote: false} : draft, save);
|
||||
return (draft?: PostDraft, save = false) => updateCommentDraft(rootId, draft ? {...draft, channelId} : draft, save);
|
||||
}
|
||||
|
||||
function makeUpdateCommentDraftWithRootId(channelId: string) {
|
||||
return (rootId: string, draft?: PostDraft, save = false) => updateCommentDraft(rootId, draft ? {...draft, channelId, remote: false} : draft, save);
|
||||
return (rootId: string, draft?: PostDraft, save = false) => updateCommentDraft(rootId, draft ? {...draft, channelId} : draft, save);
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
|
||||
@@ -115,6 +115,7 @@ function advancedCreatePost({
|
||||
fullWidthTextBox={fullWidthTextBox}
|
||||
currentChannelMembersCount={currentChannelMembersCount}
|
||||
draft={draft}
|
||||
isRemoteDraft={false}
|
||||
recentPostIdInChannel={recentPostIdInChannel}
|
||||
latestReplyablePostId={latestReplyablePostId}
|
||||
locale={locale}
|
||||
|
||||
@@ -22,6 +22,7 @@ import Constants, {
|
||||
Preferences,
|
||||
AdvancedTextEditor as AdvancedTextEditorConst,
|
||||
} from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {
|
||||
containsAtChannel,
|
||||
specialMentionsInText,
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
} from 'utils/post_utils';
|
||||
import {getTable, hasHtmlLink, formatMarkdownMessage, formatGithubCodePaste, isGitHubCodeBlock} from 'utils/paste';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import {isMac} from 'utils/utils';
|
||||
import * as Utils from 'utils/utils';
|
||||
import EmojiMap from 'utils/emoji_map';
|
||||
import {applyMarkdown, ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||
@@ -124,6 +124,9 @@ type Props = {
|
||||
// Data used for populating message state from previous draft
|
||||
draft: PostDraft;
|
||||
|
||||
// Data used for knowing if the draft came from a WS event
|
||||
isRemoteDraft: boolean;
|
||||
|
||||
// Data used dispatching handleViewAction ex: edit post
|
||||
latestReplyablePostId?: string;
|
||||
locale: string;
|
||||
@@ -279,7 +282,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
};
|
||||
if (
|
||||
props.currentChannel.id !== state.currentChannel.id ||
|
||||
(props.draft.remote && props.draft.message !== state.message)
|
||||
(props.isRemoteDraft && props.draft.message !== state.message)
|
||||
) {
|
||||
updatedState = {
|
||||
...updatedState,
|
||||
@@ -294,8 +297,8 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
message: this.props.draft.message,
|
||||
caretPosition: this.props.draft.message.length,
|
||||
message: props.draft.message,
|
||||
caretPosition: props.draft.message.length,
|
||||
submitting: false,
|
||||
showEmojiPicker: false,
|
||||
uploadsProgressPercent: {},
|
||||
@@ -387,7 +390,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
this.draftsForChannel[channelId] = {
|
||||
...draft,
|
||||
show: !isDraftEmpty(draft),
|
||||
remote: false,
|
||||
} as PostDraft;
|
||||
}
|
||||
}
|
||||
@@ -1096,7 +1098,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
|
||||
documentKeyHandler = (e: KeyboardEvent) => {
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Utils.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
if (lastMessageReactionKeyCombo) {
|
||||
this.reactToLastMessage(e);
|
||||
return;
|
||||
@@ -1134,12 +1136,12 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
const ctrlEnterKeyCombo = (this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) &&
|
||||
Utils.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
ctrlOrMetaKeyPressed;
|
||||
|
||||
const ctrlKeyCombo = Utils.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Utils.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Utils.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Keyboard.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
|
||||
// listen for line break key combo and insert new line character
|
||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||
@@ -1155,7 +1157,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
|
||||
const {message} = this.state;
|
||||
|
||||
if (Utils.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
this.textboxRef.current?.blur();
|
||||
}
|
||||
|
||||
@@ -1164,7 +1166,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
Utils.isKeyPressed(e, KeyCodes.UP) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.UP) &&
|
||||
message === ''
|
||||
) {
|
||||
e.preventDefault();
|
||||
@@ -1182,15 +1184,15 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
} = e.target as TextboxElement;
|
||||
|
||||
if (ctrlKeyCombo) {
|
||||
if (draftMessageIsEmpty && Utils.isKeyPressed(e, KeyCodes.UP)) {
|
||||
if (draftMessageIsEmpty && Keyboard.isKeyPressed(e, KeyCodes.UP)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.loadPrevMessage(e);
|
||||
} else if (draftMessageIsEmpty && Utils.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
} else if (draftMessageIsEmpty && Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.loadNextMessage(e);
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.B)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.B)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -1199,7 +1201,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.I)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.I)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -1210,7 +1212,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
} else if (ctrlAltCombo) {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.K)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -1219,7 +1221,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.C)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.C)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -1228,21 +1230,21 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.E)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleEmojiPicker();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.T)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleAdvanceTextEditor();
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.P) && message.length) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.P) && message.length) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setShowPreview(!this.props.shouldShowPreview);
|
||||
}
|
||||
} else if (shiftAltCombo) {
|
||||
if (Utils.isKeyPressed(e, KeyCodes.X)) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.X)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
@@ -1251,7 +1253,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ol',
|
||||
@@ -1259,7 +1261,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ul',
|
||||
@@ -1267,7 +1269,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'quote',
|
||||
@@ -1277,21 +1279,21 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
}
|
||||
const upKeyOnly = !ctrlOrMetaKeyPressed && !e.altKey && !e.shiftKey && Utils.isKeyPressed(e, KeyCodes.UP);
|
||||
const shiftUpKeyCombo = !ctrlOrMetaKeyPressed && !e.altKey && e.shiftKey && Utils.isKeyPressed(e, KeyCodes.UP);
|
||||
const ctrlShiftCombo = Utils.cmdOrCtrlPressed(e, true) && e.shiftKey;
|
||||
const upKeyOnly = !ctrlOrMetaKeyPressed && !e.altKey && !e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
|
||||
const shiftUpKeyCombo = !ctrlOrMetaKeyPressed && !e.altKey && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
|
||||
const ctrlShiftCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.shiftKey;
|
||||
|
||||
if (upKeyOnly && messageIsEmpty) {
|
||||
this.editLastPost(e);
|
||||
} else if (shiftUpKeyCombo && messageIsEmpty) {
|
||||
this.replyToLastPost(e);
|
||||
} else if (ctrlShiftCombo && Utils.isKeyPressed(e, KeyCodes.E)) {
|
||||
} else if (ctrlShiftCombo && Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleEmojiPicker();
|
||||
} else if (((isMac() && ctrlShiftCombo) || (!isMac() && ctrlAltCombo)) && Utils.isKeyPressed(e, KeyCodes.P) && this.state.message.length) {
|
||||
} else if (((UserAgent.isMac() && ctrlShiftCombo) || (!UserAgent.isMac() && ctrlAltCombo)) && Keyboard.isKeyPressed(e, KeyCodes.P) && this.state.message.length) {
|
||||
this.setShowPreview(!this.props.shouldShowPreview);
|
||||
} else if (ctrlAltCombo && Utils.isKeyPressed(e, KeyCodes.T)) {
|
||||
} else if (ctrlAltCombo && Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||
this.toggleAdvanceTextEditor();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,6 +77,7 @@ function makeMapStateToProps() {
|
||||
const currentChannel = getCurrentChannel(state) || {};
|
||||
const currentChannelTeammateUsername = getUser(state, currentChannel.teammate_id || '')?.username;
|
||||
const draft = getChannelDraft(state, currentChannel.id);
|
||||
const isRemoteDraft = state.views.drafts.remotes[`${StoragePrefixes.DRAFT}${currentChannel.id}`] || false;
|
||||
const latestReplyablePostId = getLatestReplyablePostId(state);
|
||||
const currentChannelMembersCount = getCurrentChannelStats(state) ? getCurrentChannelStats(state).member_count : 1;
|
||||
const enableEmojiPicker = config.EnableEmojiPicker === 'true';
|
||||
@@ -117,6 +118,7 @@ function makeMapStateToProps() {
|
||||
showSendTutorialTip,
|
||||
messageInHistoryItem: getMessageInHistoryItem(state),
|
||||
draft,
|
||||
isRemoteDraft,
|
||||
latestReplyablePostId,
|
||||
locale: getCurrentLocale(state),
|
||||
currentUsersLatestPost: getCurrentUsersLatestPost(state, ''),
|
||||
@@ -181,12 +183,7 @@ function setDraft(key: string, value: PostDraft, draftChannelId: string, save =
|
||||
const channelId = draftChannelId || getCurrentChannelId(getState());
|
||||
let updatedValue = null;
|
||||
if (value) {
|
||||
updatedValue = {...value};
|
||||
updatedValue = {
|
||||
...value,
|
||||
channelId,
|
||||
remote: false,
|
||||
};
|
||||
updatedValue = {...value, channelId};
|
||||
}
|
||||
if (updatedValue) {
|
||||
return dispatch(updateDraft(key, updatedValue, '', save));
|
||||
|
||||
@@ -21,6 +21,7 @@ import './alert_banner.scss';
|
||||
export type ModeType = 'danger' | 'warning' | 'info' | 'success';
|
||||
|
||||
export type AlertBannerProps = {
|
||||
id?: string;
|
||||
mode: ModeType;
|
||||
title?: React.ReactNode;
|
||||
message?: React.ReactNode;
|
||||
@@ -35,6 +36,7 @@ export type AlertBannerProps = {
|
||||
}
|
||||
|
||||
const AlertBanner = ({
|
||||
id,
|
||||
mode,
|
||||
title,
|
||||
message,
|
||||
@@ -70,6 +72,7 @@ const AlertBanner = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid={id}
|
||||
className={classNames(
|
||||
'AlertBanner',
|
||||
mode,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import {ToYearlyNudgeBannerDismissable} from 'components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner';
|
||||
import {ToPaidPlanBannerDismissable} from 'components/admin_console/billing/billing_subscriptions/to_paid_plan_nudge_banner';
|
||||
|
||||
import {ClientLicense, ClientConfig, WarnMetricStatus} from '@mattermost/types/config';
|
||||
import withGetCloudSubscription from '../common/hocs/cloud/with_get_cloud_subscription';
|
||||
@@ -72,6 +73,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
||||
let cloudDelinquencyAnnouncementBar = null;
|
||||
let notifyAdminDowngradeDelinquencyBar = null;
|
||||
let toYearlyNudgeBannerDismissable = null;
|
||||
let toPaidPlanNudgeBannerDismissable = null;
|
||||
if (this.props.license?.Cloud === 'true') {
|
||||
paymentAnnouncementBar = (
|
||||
<PaymentAnnouncementBar/>
|
||||
@@ -89,6 +91,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
||||
<NotifyAdminDowngradeDelinquencyBar/>
|
||||
);
|
||||
toYearlyNudgeBannerDismissable = (<ToYearlyNudgeBannerDismissable/>);
|
||||
toPaidPlanNudgeBannerDismissable = (<ToPaidPlanBannerDismissable/>);
|
||||
}
|
||||
|
||||
let autoStartTrialModal = null;
|
||||
@@ -108,6 +111,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
||||
{cloudDelinquencyAnnouncementBar}
|
||||
{notifyAdminDowngradeDelinquencyBar}
|
||||
{toYearlyNudgeBannerDismissable}
|
||||
{toPaidPlanNudgeBannerDismissable}
|
||||
{this.props.license?.Cloud !== 'true' && <OverageUsersBanner/>}
|
||||
{autoStartTrialModal}
|
||||
<ShowThreeDaysLeftTrialModal/>
|
||||
|
||||
@@ -18,6 +18,8 @@ import PricingModal from 'components/pricing_modal';
|
||||
|
||||
import {ModalData} from 'types/actions';
|
||||
|
||||
import {AlertCircleOutlineIcon, AlertOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import {
|
||||
Preferences,
|
||||
CloudBanners,
|
||||
@@ -38,6 +40,7 @@ type Props = {
|
||||
daysLeftOnTrial: number;
|
||||
isCloud: boolean;
|
||||
subscription?: Subscription;
|
||||
reverseTrial: boolean;
|
||||
actions: {
|
||||
savePreferences: (userId: string, preferences: PreferenceType[]) => void;
|
||||
getCloudSubscription: () => void;
|
||||
@@ -132,15 +135,34 @@ class CloudTrialAnnouncementBar extends React.PureComponent<Props> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trialMoreThan3DaysMsg = (
|
||||
let trialMoreThan7DaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.moreThan3Days'
|
||||
id='admin.billing.subscription.cloudTrial.daysLeft'
|
||||
defaultMessage='Your trial has started! There are {daysLeftOnTrial} days left'
|
||||
values={{daysLeftOnTrial}}
|
||||
/>
|
||||
);
|
||||
|
||||
const trialLessThan3DaysMsg = (
|
||||
let modalButtonText = t('admin.billing.subscription.cloudTrial.subscribeButton');
|
||||
let modalButtonDefaultText = 'Upgrade Now';
|
||||
|
||||
if (this.props.reverseTrial) {
|
||||
modalButtonText = t('admin.billing.subscription.cloudReverseTrial.subscribeButton');
|
||||
modalButtonDefaultText = 'Review your options';
|
||||
}
|
||||
|
||||
if (this.props.reverseTrial) {
|
||||
const trialEnd = getLocaleDateFromUTC((this.props.subscription?.trial_end_at as number / 1000), 'MMMM Do');
|
||||
trialMoreThan7DaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.moreThan3Days'
|
||||
defaultMessage='Your 30 day Enterprise trial has started! Purchase before {trialEnd} to keep your workspace.'
|
||||
values={{trialEnd}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let trialLessThan7DaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.daysLeftOnTrial'
|
||||
defaultMessage='There are {daysLeftOnTrial} days left on your free trial'
|
||||
@@ -148,10 +170,20 @@ class CloudTrialAnnouncementBar extends React.PureComponent<Props> {
|
||||
/>
|
||||
);
|
||||
|
||||
const userEndTrialDate = getLocaleDateFromUTC((this.props.subscription?.trial_end_at as number / 1000), 'MMMM Do YYYY');
|
||||
const userEndTrialHour = getLocaleDateFromUTC((this.props.subscription?.trial_end_at as number / 1000), 'HH:mm:ss', this.props.currentUser.timezone?.automaticTimezone as string);
|
||||
if (this.props.reverseTrial) {
|
||||
trialLessThan7DaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudReverseTrial.daysLeftOnTrial'
|
||||
defaultMessage='{daysLeftOnTrial} days left on your trial. Purchase a plan or contact sales to keep your workspace.'
|
||||
values={{daysLeftOnTrial}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const trialLastDaysMsg = (
|
||||
const userEndTrialDate = getLocaleDateFromUTC((this.props.subscription?.trial_end_at as number / 1000), 'MMMM Do YYYY');
|
||||
const userEndTrialHour = getLocaleDateFromUTC((this.props.subscription?.trial_end_at as number / 1000), 'HH:mm', this.props.currentUser.timezone?.automaticTimezone as string);
|
||||
|
||||
let trialLastDaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.lastDay'
|
||||
defaultMessage='This is the last day of your free trial. Your access will expire on {userEndTrialDate} at {userEndTrialHour}.'
|
||||
@@ -159,21 +191,28 @@ class CloudTrialAnnouncementBar extends React.PureComponent<Props> {
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.props.reverseTrial) {
|
||||
trialLastDaysMsg = (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudReverseTrial.lastDay'
|
||||
defaultMessage='This is the last day of your trial. Purchase a plan before {userEndTrialHour} or contact sales'
|
||||
values={{userEndTrialHour}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let bannerMessage;
|
||||
let icon;
|
||||
switch (daysLeftOnTrial) {
|
||||
case TrialPeriodDays.TRIAL_WARNING_THRESHOLD:
|
||||
case TrialPeriodDays.TRIAL_2_DAYS:
|
||||
bannerMessage = trialLessThan3DaysMsg;
|
||||
break;
|
||||
case TrialPeriodDays.TRIAL_1_DAY:
|
||||
case TrialPeriodDays.TRIAL_0_DAYS:
|
||||
|
||||
if (daysLeftOnTrial >= TrialPeriodDays.TRIAL_2_DAYS && daysLeftOnTrial <= TrialPeriodDays.TRIAL_WARNING_THRESHOLD) {
|
||||
bannerMessage = trialLessThan7DaysMsg;
|
||||
icon = <AlertCircleOutlineIcon size={18}/>;
|
||||
} else if (daysLeftOnTrial <= TrialPeriodDays.TRIAL_1_DAY && daysLeftOnTrial >= TrialPeriodDays.TRIAL_0_DAYS) {
|
||||
bannerMessage = trialLastDaysMsg;
|
||||
break;
|
||||
default:
|
||||
bannerMessage = trialMoreThan3DaysMsg;
|
||||
icon = <i className='icon-check-outline-circle'/>;
|
||||
break;
|
||||
icon = <AlertOutlineIcon size={18}/>;
|
||||
} else {
|
||||
bannerMessage = trialMoreThan7DaysMsg;
|
||||
icon = <AlertCircleOutlineIcon size={18}/>;
|
||||
}
|
||||
|
||||
const dismissable = this.isDismissable();
|
||||
@@ -184,8 +223,8 @@ class CloudTrialAnnouncementBar extends React.PureComponent<Props> {
|
||||
showCloseButton={dismissable}
|
||||
handleClose={this.handleClose}
|
||||
onButtonClick={this.showModal}
|
||||
modalButtonText={t('admin.billing.subscription.cloudTrial.subscribeButton')}
|
||||
modalButtonDefaultText={'Upgrade Now'}
|
||||
modalButtonText={modalButtonText}
|
||||
modalButtonDefaultText={modalButtonDefaultText}
|
||||
message={bannerMessage}
|
||||
showLinkAsButton={true}
|
||||
icon={icon}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {Preferences, TrialPeriodDays} from 'utils/constants';
|
||||
import {getRemainingDaysFromFutureTimestamp} from 'utils/utils';
|
||||
|
||||
import CloudTrialAnnouncementBar from './cloud_trial_announcement_bar';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/admin';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const getCategory = makeGetCategory();
|
||||
@@ -29,6 +30,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
const isCloud = getLicense(state).Cloud === 'true';
|
||||
let isFreeTrial = false;
|
||||
let daysLeftOnTrial = 0;
|
||||
const config = getConfig(state);
|
||||
|
||||
if (isCloud && subscription?.is_free_trial === 'true') {
|
||||
isFreeTrial = true;
|
||||
@@ -46,6 +48,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
isCloud,
|
||||
subscription,
|
||||
preferences: getCategory(state, Preferences.CLOUD_TRIAL_BANNER),
|
||||
reverseTrial: Boolean(config.FeatureFlags?.CloudReverseTrial),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {ReactNode} from 'react';
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
@@ -395,7 +395,7 @@ const ConfigurationAnnouncementBar = (props: Props) => {
|
||||
defaultMessage = 'Please configure your <linkSite>site URL</linkSite> on the <linkConsole>System Console</linkConsole>.';
|
||||
}
|
||||
|
||||
const values = {
|
||||
const values: Record<string, ReactNode> = {
|
||||
linkSite: (msg: string) => (
|
||||
<ExternalLink
|
||||
href={props.siteURL}
|
||||
|
||||
@@ -19,6 +19,7 @@ import ToggleModalButton from 'components/toggle_modal_button';
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
showCloseButton: boolean;
|
||||
color: string;
|
||||
textColor: string;
|
||||
@@ -172,6 +173,7 @@ export default class AnnouncementBar extends React.PureComponent<Props, State> {
|
||||
style={barStyle}
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
css={{gridArea: 'announcement'}}
|
||||
data-testid={this.props.id}
|
||||
>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
|
||||
@@ -5,15 +5,13 @@ import React, {useCallback} from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Tooltip} from 'react-bootstrap';
|
||||
|
||||
import Icon from '@mattermost/compass-components/foundations/icon'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
import {ViewGridPlusOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
const AppBarMarketplace = () => {
|
||||
const {formatMessage} = useIntl();
|
||||
@@ -48,10 +46,7 @@ const AppBarMarketplace = () => {
|
||||
aria-label={label}
|
||||
onClick={handleOpenMarketplace}
|
||||
>
|
||||
<Icon
|
||||
size={16}
|
||||
glyph={'view-grid-plus-outline'}
|
||||
/>
|
||||
<ViewGridPlusOutlineIcon size={18}/>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
@@ -234,7 +234,7 @@ export class AppsForm extends React.PureComponent<Props, State> {
|
||||
const errorResponse = res.error;
|
||||
const errMsg = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error.',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
});
|
||||
this.setState({
|
||||
fieldErrors: {
|
||||
@@ -256,7 +256,7 @@ export class AppsForm extends React.PureComponent<Props, State> {
|
||||
case AppCallResponseTypes.NAVIGATE: {
|
||||
const errMsg = intl.formatMessage({
|
||||
id: 'apps.error.responses.unexpected_type',
|
||||
defaultMessage: 'App response type was not expected. Response type: {type}.',
|
||||
defaultMessage: 'App response type was not expected. Response type: {type}',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
},
|
||||
@@ -338,7 +338,7 @@ export class AppsForm extends React.PureComponent<Props, State> {
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
this.updateErrors([], undefined, this.props.intl.formatMessage({
|
||||
id: 'apps.error.responses.unexpected_type',
|
||||
defaultMessage: 'App response type was not expected. Response type: {type}.',
|
||||
defaultMessage: 'App response type was not expected. Response type: {type}',
|
||||
}, {
|
||||
type: callResponse.type,
|
||||
}));
|
||||
|
||||
@@ -48,7 +48,7 @@ class AppsFormContainer extends React.PureComponent<Props, State> {
|
||||
};
|
||||
const {form} = this.state;
|
||||
if (!form) {
|
||||
const errMsg = this.props.intl.formatMessage({id: 'apps.error.form.no_form', defaultMessage: '`form` is not defined'});
|
||||
const errMsg = this.props.intl.formatMessage({id: 'apps.error.form.no_form', defaultMessage: '`form` is not defined.'});
|
||||
return {error: makeCallErrorResponse(makeErrorMsg(errMsg))};
|
||||
}
|
||||
if (!form.submit) {
|
||||
@@ -97,7 +97,7 @@ class AppsFormContainer extends React.PureComponent<Props, State> {
|
||||
refreshOnSelect = async (field: AppField, values: AppFormValues): Promise<DoAppCallResult<FormResponseData>> => {
|
||||
const makeErrMsg = (message: string) => this.props.intl.formatMessage(
|
||||
{
|
||||
id: 'apps.error.form.refresh',
|
||||
id: 'apps.error.form.update',
|
||||
defaultMessage: 'There has been an error updating the modal. Contact the app developer. Details: {details}',
|
||||
},
|
||||
{details: message},
|
||||
@@ -144,7 +144,7 @@ class AppsFormContainer extends React.PureComponent<Props, State> {
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
return {error: makeCallErrorResponse(makeErrMsg(this.props.intl.formatMessage({
|
||||
id: 'apps.error.responses.unexpected_type',
|
||||
defaultMessage: 'App response type was not expected. Response type: {type}.',
|
||||
defaultMessage: 'App response type was not expected. Response type: {type}',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
},
|
||||
|
||||
@@ -12,9 +12,10 @@ import {Group} from '@mattermost/types/groups';
|
||||
import ProfilePopover from 'components/profile_popover';
|
||||
|
||||
import {popOverOverlayPosition} from 'utils/position_utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {getUserOrGroupFromMentionName} from 'utils/post_utils';
|
||||
import Constants from 'utils/constants';
|
||||
import {getViewportSize, isKeyPressed} from 'utils/utils';
|
||||
import {getViewportSize} from 'utils/utils';
|
||||
|
||||
import AtMentionGroup from 'components/at_mention/at_mention_group';
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ import ProfilePopover from 'components/profile_popover';
|
||||
import UserGroupPopover from 'components/user_group_popover';
|
||||
|
||||
import Constants, {A11yCustomEventTypes, A11yFocusEventDetail} from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {popOverOverlayPosition} from 'utils/position_utils';
|
||||
import {getViewportSize, isKeyPressed} from 'utils/utils';
|
||||
import {getViewportSize} from 'utils/utils';
|
||||
|
||||
import {MAX_LIST_HEIGHT, getListHeight, VIEWPORT_SCALE_FACTOR} from 'components/user_group_popover/group_member_list/group_member_list';
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ class ChannelGroupsManageModal extends React.PureComponent<Props> {
|
||||
const {formatMessage} = this.props.intl;
|
||||
return (
|
||||
<ListModal
|
||||
titleText={formatMessage({id: 'groups', defaultMessage: '{channel} Groups'}, {channel: this.props.channel.display_name})}
|
||||
titleText={formatMessage({id: 'channel_groups', defaultMessage: '{channel} Groups'}, {channel: this.props.channel.display_name})}
|
||||
searchPlaceholderText={formatMessage({id: 'manage_channel_groups_modal.search_placeholder', defaultMessage: 'Search groups'})}
|
||||
renderRow={this.renderRow}
|
||||
loadItems={this.loadItems}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
|
||||
const Title = styled.div`
|
||||
flex:1;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect, useState, ReactNode} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
@@ -14,8 +14,9 @@ import {trackEvent} from 'actions/telemetry_actions';
|
||||
import {openModal, closeModal} from 'actions/views/modals';
|
||||
|
||||
import TrialBenefitsModal from 'components/trial_benefits_modal/trial_benefits_modal';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
import {ModalIdentifiers, TELEMETRY_CATEGORIES, LicenseLinks} from 'utils/constants';
|
||||
|
||||
import RequestBusinessEmailModal from './request_business_email_modal';
|
||||
import './cloud_start_trial_btn.scss';
|
||||
@@ -129,7 +130,7 @@ const CloudStartTrialButton = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const btnText = (status: TrialLoadStatus): string => {
|
||||
const btnText = (status: TrialLoadStatus) => {
|
||||
switch (status) {
|
||||
case TrialLoadStatus.Started:
|
||||
return formatMessage({id: 'start_cloud_trial.modal.gettingTrial', defaultMessage: 'Getting Trial...'});
|
||||
@@ -138,7 +139,22 @@ const CloudStartTrialButton = ({
|
||||
case TrialLoadStatus.Failed:
|
||||
return formatMessage({id: 'start_cloud_trial.modal.failed', defaultMessage: 'Failed'});
|
||||
case TrialLoadStatus.Embargoed:
|
||||
return formatMessage({id: 'admin.license.trial-request.embargoed'});
|
||||
return formatMessage<ReactNode>(
|
||||
{
|
||||
id: 'admin.license.trial-request.embargoed',
|
||||
defaultMessage: 'We were unable to process the request due to limitations for embargoed countries. <link>Learn more in our documentation</link>, or reach out to legal@mattermost.com for questions around export limitations.',
|
||||
},
|
||||
{
|
||||
link: (text: string) => (
|
||||
<ExternalLink
|
||||
location='trial_banner'
|
||||
href={LicenseLinks.EMBARGOED_COUNTRIES}
|
||||
>
|
||||
{text}
|
||||
</ExternalLink>
|
||||
),
|
||||
},
|
||||
);
|
||||
default:
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -29,9 +29,11 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
|
||||
forwardedRef={null}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -58,6 +60,7 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
|
||||
@@ -29,9 +29,11 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
|
||||
forwardedRef={null}
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -58,6 +60,7 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
|
||||
@@ -4,10 +4,12 @@ exports[`codeBlock should render html code block with proper indentation after s
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={Object {}}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<CodeBlock
|
||||
@@ -68,9 +70,11 @@ exports[`codeBlock should render html code block with proper indentation after s
|
||||
id="copyButton"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -97,6 +101,7 @@ exports[`codeBlock should render html code block with proper indentation after s
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": undefined,
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -228,10 +233,12 @@ exports[`codeBlock should render typescript code block after syntax highlighting
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={Object {}}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<CodeBlock
|
||||
@@ -292,9 +299,11 @@ const myFunction = () => {
|
||||
id="copyButton"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -321,6 +330,7 @@ const myFunction = () => {
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": undefined,
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
@@ -452,10 +462,12 @@ exports[`codeBlock should render unknown language after syntax highlighting 1`]
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={Object {}}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<CodeBlock
|
||||
@@ -514,9 +526,11 @@ it shouldn't highlight, it's just garbage
|
||||
id="copyButton"
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
@@ -543,6 +557,7 @@ it shouldn't highlight, it's just garbage
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": undefined,
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
|
||||
@@ -25,7 +25,8 @@ import {GlobalState} from 'types/store';
|
||||
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
||||
import {A11yCustomEventTypes, A11yFocusEventDetail, Constants, ModalIdentifiers} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
import {isKeyPressed, localizeMessage} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
|
||||
import CustomStatusSuggestion from 'components/custom_status/custom_status_suggestion';
|
||||
import ExpiryMenu from 'components/custom_status/expiry_menu';
|
||||
|
||||
@@ -17,7 +17,8 @@ import DatePicker from 'components/date_picker';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import Timestamp from 'components/timestamp';
|
||||
import {getCurrentLocale} from 'selectors/i18n';
|
||||
import {isKeyPressed, localizeMessage} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
||||
import Constants, {A11yCustomEventTypes, A11yFocusEventDetail} from 'utils/constants';
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
|
||||
import './dnd_custom_time_picker_modal.scss';
|
||||
import {toUTCUnix} from 'utils/datetime';
|
||||
import {isKeyPressed, localizeMessage} from 'utils/utils';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import DatePicker from 'components/date_picker';
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import Permissions from 'mattermost-redux/constants/permissions';
|
||||
import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants';
|
||||
import DeletePostModal from 'components/delete_post_modal';
|
||||
import DelayedAction from 'utils/delayed_action';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import * as PostUtils from 'utils/post_utils';
|
||||
import * as Menu from 'components/menu';
|
||||
import * as Utils from 'utils/utils';
|
||||
@@ -337,61 +338,61 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
const isShiftKeyPressed = e.shiftKey;
|
||||
|
||||
switch (true) {
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.R):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.R):
|
||||
this.handleCommentClick(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// edit post
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.E):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.E):
|
||||
this.handleEditMenuItemActivated(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// follow thread
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.F) && !isShiftKeyPressed:
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && !isShiftKeyPressed:
|
||||
this.handleSetThreadFollow(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// forward post
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.F) && isShiftKeyPressed:
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && isShiftKeyPressed:
|
||||
this.handleForwardMenuItemActivated(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// copy link
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.K):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.K):
|
||||
this.copyLink(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// copy text
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.C):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.C):
|
||||
this.copyText(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// delete post
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.DELETE):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.DELETE):
|
||||
this.handleDeleteMenuItemActivated(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// pin / unpin
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.P):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.P):
|
||||
this.handlePinMenuItemActivated(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// save / unsave
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.S):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.S):
|
||||
this.handleFlagMenuItemActivated(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// mark as unread
|
||||
case Utils.isKeyPressed(e, Constants.KeyCodes.U):
|
||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.U):
|
||||
this.handleMarkPostAsUnread(e);
|
||||
this.handleDropdownOpened(false);
|
||||
break;
|
||||
@@ -496,7 +497,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
class: classNames('post-menu__item', {
|
||||
'post-menu__item--active': this.props.isMenuOpen,
|
||||
}),
|
||||
'aria-label': formatMessage({id: 'post_info.dot_menu.tooltip.more_actions', defaultMessage: 'Actions'}),
|
||||
'aria-label': formatMessage({id: 'post_info.dot_menu.tooltip.actions', defaultMessage: 'Actions'}),
|
||||
children: <DotsHorizontalIcon size={16}/>,
|
||||
}}
|
||||
menu={{
|
||||
@@ -509,7 +510,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
}}
|
||||
menuButtonTooltip={{
|
||||
id: `PostDotMenu-ButtonTooltip-${this.props.post.id}`,
|
||||
text: formatMessage({id: 'post_info.dot_menu.tooltip.more_actions', defaultMessage: 'More'}),
|
||||
text: formatMessage({id: 'post_info.dot_menu.tooltip.more', defaultMessage: 'More'}),
|
||||
class: 'hidden-xs',
|
||||
}}
|
||||
>
|
||||
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user