From 9105077ee11013e2f26652828f593a4836ac53e6 Mon Sep 17 00:00:00 2001 From: Nathaniel Allred Date: Fri, 24 Mar 2023 14:46:22 -0500 Subject: [PATCH 01/20] Mm 50735 (#22626) * add shipping address to self hosted signup model * purchase modals allow scroll * fix z-index issues with payment modal dropdowns --- model/hosted_customer.go | 9 +- .../choose_different_shipping.scss | 37 +++ .../choose_different_shipping/index.tsx | 45 ++++ .../src/components/dropdown_input.scss | 36 ++- .../components/payment_form/address_form.tsx | 42 ++-- .../components/payment_form/payment_form.scss | 2 - .../components/payment_form/payment_form.tsx | 2 +- .../components/purchase_modal/purchase.scss | 38 +-- .../purchase_modal/purchase_modal.tsx | 3 +- .../self_hosted_purchase_modal/address.tsx | 132 ++++++++++ .../self_hosted_purchase_modal/index.test.tsx | 24 ++ .../self_hosted_purchase_modal/index.tsx | 227 ++++++++++-------- .../self_hosted_purchase_modal.scss | 10 +- webapp/channels/src/i18n/en.json | 1 + webapp/platform/types/src/hosted_customer.ts | 1 + 15 files changed, 438 insertions(+), 171 deletions(-) create mode 100644 webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss create mode 100644 webapp/channels/src/components/choose_different_shipping/index.tsx create mode 100644 webapp/channels/src/components/self_hosted_purchase_modal/address.tsx diff --git a/model/hosted_customer.go b/model/hosted_customer.go index 4f1917bdaf..543ea12b74 100644 --- a/model/hosted_customer.go +++ b/model/hosted_customer.go @@ -21,10 +21,11 @@ type BootstrapSelfHostedSignupResponseInternal struct { // email contained in token, so not in the request body. type SelfHostedCustomerForm struct { - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - BillingAddress *Address `json:"billing_address"` - Organization string `json:"organization"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + BillingAddress *Address `json:"billing_address"` + ShippingAddress *Address `json:"shipping_address"` + Organization string `json:"organization"` } type SelfHostedConfirmPaymentMethodRequest struct { diff --git a/webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss b/webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss new file mode 100644 index 0000000000..322997fc03 --- /dev/null +++ b/webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss @@ -0,0 +1,37 @@ +.shipping-address-section { + display: flex; + align-content: flex-start; + padding-bottom: 24px; + font-weight: normal; + + button.no-style { + padding-left: 0; + border: none; + background: transparent; + outline: unset; + text-align: left; + + &:focus { + outline: unset; + } + } + + #address-same-than-billing-address { + width: 17px; + height: 17px; + flex-shrink: 0; + } + + .Form-checkbox-label { + padding-left: 12px; + cursor: default; + font-family: 'Open Sans', sans-serif; + vertical-align: middle; + } + + .billing_address_btn_text { + color: var(--center-channel-color); + font-family: 'Open Sans', sans-serif; + font-weight: bold; + } +} diff --git a/webapp/channels/src/components/choose_different_shipping/index.tsx b/webapp/channels/src/components/choose_different_shipping/index.tsx new file mode 100644 index 0000000000..43b810f2e8 --- /dev/null +++ b/webapp/channels/src/components/choose_different_shipping/index.tsx @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {useIntl} from 'react-intl'; + +import './choose_different_shipping.scss'; + +interface Props { + shippingIsSame: boolean; + setShippingIsSame: (different: boolean) => void; +} +export default function ChooseDifferentShipping(props: Props) { + const intl = useIntl(); + const toggle = () => props.setShippingIsSame(!props.shippingIsSame); + + return ( +
+ + + + +
+ ); +} diff --git a/webapp/channels/src/components/dropdown_input.scss b/webapp/channels/src/components/dropdown_input.scss index e654e78d5c..1420223f21 100644 --- a/webapp/channels/src/components/dropdown_input.scss +++ b/webapp/channels/src/components/dropdown_input.scss @@ -1,5 +1,7 @@ +$dropdown_input_index: 999999; + .DropdownInput { - z-index: 999999; + z-index: $dropdown_input_index; &.Input_container { margin-top: 20px; @@ -37,7 +39,7 @@ } .DropdownInput__option > div { - z-index: 999999; + z-index: $dropdown_input_index; padding: 10px 24px; cursor: pointer; line-height: 16px; @@ -51,3 +53,33 @@ .DropdownInput__option.focused > div { background-color: rgba(var(--center-channel-color-rgb), 0.08); } + +.second-dropdown-sibling-wrapper { + .DropdownInput { + z-index: $dropdown_input_index - 1; + } + + .DropdownInput__option > div { + z-index: $dropdown_input_index - 1; + } +} + +.third-dropdown-sibling-wrapper { + .DropdownInput { + z-index: $dropdown_input_index - 2; + } + + .DropdownInput__option > div { + z-index: $dropdown_input_index - 2; + } +} + +.fourth-dropdown-sibling-wrapper { + .DropdownInput { + z-index: $dropdown_input_index - 3; + } + + .DropdownInput__option > div { + z-index: $dropdown_input_index - 3; + } +} diff --git a/webapp/channels/src/components/payment_form/address_form.tsx b/webapp/channels/src/components/payment_form/address_form.tsx index 651861b8d1..470f45da88 100644 --- a/webapp/channels/src/components/payment_form/address_form.tsx +++ b/webapp/channels/src/components/payment_form/address_form.tsx @@ -61,25 +61,27 @@ const AddressForm = (props: AddressFormProps) => { {...props.title} /> - ({ - value: country.name, - label: country.name, - }))} - legend={formatMessage({ - id: 'payment_form.country', - defaultMessage: 'Country', - })} - placeholder={formatMessage({ - id: 'payment_form.country', - defaultMessage: 'Country', - })} - name={'billing_dropdown'} - /> +
+ ({ + value: country.name, + label: country.name, + }))} + legend={formatMessage({ + id: 'payment_form.country', + defaultMessage: 'Country', + })} + placeholder={formatMessage({ + id: 'payment_form.country', + defaultMessage: 'Country', + })} + name={'billing_dropdown'} + /> +
{ />
-
+
{ />
-
+
div { diff --git a/webapp/channels/src/components/purchase_modal/purchase_modal.tsx b/webapp/channels/src/components/purchase_modal/purchase_modal.tsx index bf4abc4804..36b39a3ab0 100644 --- a/webapp/channels/src/components/purchase_modal/purchase_modal.tsx +++ b/webapp/channels/src/components/purchase_modal/purchase_modal.tsx @@ -6,6 +6,7 @@ import React, {ReactNode} from 'react'; import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'; +import classnames from 'classnames'; import {Stripe, StripeCardElementChangeEvent} from '@stripe/stripe-js'; import {loadStripe} from '@stripe/stripe-js/pure'; // https://github.com/stripe/stripe-js#importing-loadstripe-without-side-effects import {Elements} from '@stripe/react-stripe-js'; @@ -812,7 +813,7 @@ class PurchaseModal extends React.PureComponent { } return ( -
+

{title}

void; + + address: string; + changeAddress: (e: React.ChangeEvent) => void; + + address2: string; + changeAddress2: (e: React.ChangeEvent) => void; + + city: string; + changeCity: (e: React.ChangeEvent) => void; + + state: string; + changeState: (postalCode: string) => void; + + postalCode: string; + changePostalCode: (e: React.ChangeEvent) => void; +} +export default function Address(props: Props) { + const testPrefix = props.testPrefix || 'selfHostedPurchase'; + const intl = useIntl(); + let countrySelectorId = `${testPrefix}CountrySelector`; + let stateSelectorId = `${testPrefix}StateSelector`; + if (props.type === 'shipping') { + countrySelectorId += '_Shipping'; + stateSelectorId += '_Shipping'; + } + return ( + <> +
+ ({ + value: country.name, + label: country.name, + }))} + legend={intl.formatMessage({ + id: 'payment_form.country', + defaultMessage: 'Country', + })} + placeholder={intl.formatMessage({ + id: 'payment_form.country', + defaultMessage: 'Country', + })} + name={'billing_dropdown'} + /> +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ + ); +} diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/index.test.tsx b/webapp/channels/src/components/self_hosted_purchase_modal/index.test.tsx index 5a3fbf6bda..3ccbb74ce2 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/index.test.tsx +++ b/webapp/channels/src/components/self_hosted_purchase_modal/index.test.tsx @@ -310,6 +310,15 @@ describe('SelfHostedPurchaseModal :: canSubmit', () => { state: 'string', country: 'string', postalCode: '12345', + + shippingSame: true, + shippingAddress: '', + shippingAddress2: '', + shippingCity: '', + shippingState: '', + shippingCountry: '', + shippingPostalCode: '', + cardName: 'string', organization: 'string', agreedTerms: true, @@ -361,6 +370,21 @@ describe('SelfHostedPurchaseModal :: canSubmit', () => { expect(canSubmit(state, SelfHostedSignupProgress.CREATED_CUSTOMER)).toBe(false); expect(canSubmit(state, SelfHostedSignupProgress.CREATED_INTENT)).toBe(false); }); + + it('if shipping address different and is not filled, can not submit', () => { + const state = makeHappyPathState(); + state.shippingSame = false; + expect(canSubmit(state, SelfHostedSignupProgress.START)).toBe(false); + + state.shippingAddress = 'more shipping info'; + state.shippingAddress2 = 'more shipping info'; + state.shippingCity = 'more shipping info'; + state.shippingState = 'more shipping info'; + state.shippingCountry = 'more shipping info'; + state.shippingPostalCode = 'more shipping info'; + expect(canSubmit(state, SelfHostedSignupProgress.START)).toBe(true); + }); + it('if card number missing and card has not been confirmed, can not submit', () => { const state = makeHappyPathState(); state.cardFilled = false; diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/index.tsx b/webapp/channels/src/components/self_hosted_purchase_modal/index.tsx index 03bfbccddc..af43bfd229 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/index.tsx +++ b/webapp/channels/src/components/self_hosted_purchase_modal/index.tsx @@ -26,8 +26,6 @@ import {GlobalState} from 'types/store'; import {isModalOpen} from 'selectors/views/modals'; import {isDevModeEnabled} from 'selectors/general'; -import {COUNTRIES} from 'utils/countries'; - import { ModalIdentifiers, StatTypes, @@ -35,8 +33,6 @@ import { } from 'utils/constants'; import CardInput, {CardInputType} from 'components/payment_form/card_input'; -import StateSelector from 'components/payment_form/state_selector'; -import DropdownInput from 'components/dropdown_input'; import BackgroundSvg from 'components/common/svg_images_components/background_svg'; import UpgradeSvg from 'components/common/svg_images_components/upgrade_svg'; @@ -47,6 +43,7 @@ import RootPortal from 'components/root_portal'; import useLoadStripe from 'components/common/hooks/useLoadStripe'; import useControlSelfHostedPurchaseModal from 'components/common/hooks/useControlSelfHostedPurchaseModal'; import useFetchStandardAnalytics from 'components/common/hooks/useFetchStandardAnalytics'; +import ChooseDifferentShipping from 'components/choose_different_shipping'; import {ValueOf} from '@mattermost/types/utilities'; import {UserProfile} from '@mattermost/types/users'; @@ -64,6 +61,7 @@ import SuccessPage from './success_page'; import SelfHostedCard from './self_hosted_card'; import StripeProvider from './stripe_provider'; import Terms from './terms'; +import Address from './address'; import useNoEscape from './useNoEscape'; import {SetPrefix, UnionSetActions} from './types'; @@ -73,12 +71,24 @@ import './self_hosted_purchase_modal.scss'; import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from './constants'; export interface State { + + // billing address address: string; address2: string; city: string; state: string; country: string; postalCode: string; + + // shipping address + shippingSame: boolean; + shippingAddress: string; + shippingAddress2: string; + shippingCity: string; + shippingState: string; + shippingCountry: string; + shippingPostalCode: string; + cardName: string; organization: string; agreedTerms: boolean; @@ -113,6 +123,15 @@ export function makeInitialState(): State { state: '', country: '', postalCode: '', + + shippingSame: true, + shippingAddress: '', + shippingAddress2: '', + shippingCity: '', + shippingState: '', + shippingCountry: '', + shippingPostalCode: '', + cardName: '', organization: '', agreedTerms: false, @@ -170,8 +189,18 @@ const simpleSetters: Array> = [ 'address2', 'city', 'country', - 'postalCode', 'state', + 'postalCode', + + // shipping address + 'shippingSame', + 'shippingAddress', + 'shippingAddress2', + 'shippingCity', + 'shippingState', + 'shippingCountry', + 'shippingPostalCode', + 'agreedTerms', 'cardFilled', 'cardName', @@ -220,7 +249,7 @@ export function canSubmit(state: State, progress: ValueOf
- { +
{ dispatch({type: 'set_country', data: option.value}); }} - value={ - state.country ? {value: state.country, label: state.country} : undefined - } - options={COUNTRIES.map((country) => ({ - value: country.name, - label: country.name, - }))} - legend={intl.formatMessage({ - id: 'payment_form.country', - defaultMessage: 'Country', - })} - placeholder={intl.formatMessage({ - id: 'payment_form.country', - defaultMessage: 'Country', - })} - name={'billing_dropdown'} + address={state.address} + changeAddress={(e) => { + dispatch({type: 'set_address', data: e.target.value}); + }} + address2={state.address2} + changeAddress2={(e) => { + dispatch({type: 'set_address2', data: e.target.value}); + }} + city={state.city} + changeCity={(e) => { + dispatch({type: 'set_city', data: e.target.value}); + }} + state={state.state} + changeState={(state: string) => { + dispatch({type: 'set_state', data: state}); + }} + postalCode={state.postalCode} + changePostalCode={(e) => { + dispatch({type: 'set_postalCode', data: e.target.value}); + }} /> -
- ) => { - dispatch({type: 'set_address', data: e.target.value}); - }} - placeholder={intl.formatMessage({ - id: 'payment_form.address', - defaultMessage: 'Address', - })} - required={true} - /> -
-
- ) => { - dispatch({type: 'set_address2', data: e.target.value}); - }} - placeholder={intl.formatMessage({ - id: 'payment_form.address_2', - defaultMessage: 'Address 2', - })} - /> -
-
- ) => { - dispatch({type: 'set_city', data: e.target.value}); - }} - placeholder={intl.formatMessage({ - id: 'payment_form.city', - defaultMessage: 'City', - })} - required={true} - /> -
-
-
- { - dispatch({type: 'set_state', data: state}); + { + dispatch({type: 'set_shippingSame', data: val}); + }} + /> + {!state.shippingSame && ( + <> +
+ +
+
{ + dispatch({type: 'set_shippingCountry', data: option.value}); + }} + address={state.shippingAddress} + changeAddress={(e) => { + dispatch({type: 'set_shippingAddress', data: e.target.value}); + }} + address2={state.shippingAddress2} + changeAddress2={(e) => { + dispatch({type: 'set_shippingAddress2', data: e.target.value}); + }} + city={state.shippingCity} + changeCity={(e) => { + dispatch({type: 'set_shippingCity', data: e.target.value}); + }} + state={state.shippingState} + changeState={(state: string) => { + dispatch({type: 'set_shippingState', data: state}); + }} + postalCode={state.shippingPostalCode} + changePostalCode={(e) => { + dispatch({type: 'set_shippingPostalCode', data: e.target.value}); }} /> -
-
- ) => { - dispatch({type: 'set_postalCode', data: e.target.value}); - }} - placeholder={intl.formatMessage({ - id: 'payment_form.zipcode', - defaultMessage: 'Zip/Postal Code', - })} - required={true} - /> -
-
+ + )} { diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_purchase_modal.scss b/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_purchase_modal.scss index e949e76e20..52bac49aec 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_purchase_modal.scss +++ b/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_purchase_modal.scss @@ -4,19 +4,20 @@ .form-view { display: flex; - overflow: hidden; width: 100%; height: 100%; flex-direction: row; flex-grow: 1; flex-wrap: wrap; - align-content: top; + align-items: flex-start; justify-content: center; padding: 77px 107px; color: var(--center-channel-color); font-family: "Open Sans"; font-size: 16px; font-weight: 600; + overflow-x: hidden; + overflow-y: auto; .title { font-size: 22px; @@ -39,14 +40,12 @@ margin-right: 16px; .DropdownInput { - z-index: 99999; margin-top: 0; } } .DropdownInput { position: relative; - z-index: 999999; height: 36px; margin-bottom: 24px; @@ -517,6 +516,9 @@ } input[type=checkbox] { + width: 17px; + height: 17px; + flex-shrink: 0; margin-right: 12px; } diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index c4f6025f80..6c28bff86c 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -4378,6 +4378,7 @@ "payment_form.no_billing_address": "No billing address added", "payment_form.no_credit_card": "No credit card added", "payment_form.saved_payment_method": "Saved Payment Method", + "payment_form.shipping_address": "Shipping Address", "payment_form.zipcode": "Zip/Postal Code", "payment.card_number": "Card Number", "payment.field_required": "This field is required", diff --git a/webapp/platform/types/src/hosted_customer.ts b/webapp/platform/types/src/hosted_customer.ts index d81ef15227..fcd5b4e70b 100644 --- a/webapp/platform/types/src/hosted_customer.ts +++ b/webapp/platform/types/src/hosted_customer.ts @@ -18,6 +18,7 @@ export interface SelfHostedSignupForm { first_name: string; last_name: string; billing_address: Address; + shipping_address: Address; organization: string; } From 067784dc4a0665fe66bc52aeb8cf4f251040fa53 Mon Sep 17 00:00:00 2001 From: Nathaniel Allred Date: Fri, 24 Mar 2023 14:51:28 -0500 Subject: [PATCH 02/20] Fix EmailSettings.FeedbackEmail client validation (#22611) * fix EmailSettings.FeedbackEmail client validation --- .../src/components/admin_console/admin_definition.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webapp/channels/src/components/admin_console/admin_definition.jsx b/webapp/channels/src/components/admin_console/admin_definition.jsx index 387b13825b..5eab5ed08f 100644 --- a/webapp/channels/src/components/admin_console/admin_definition.jsx +++ b/webapp/channels/src/components/admin_console/admin_definition.jsx @@ -2497,7 +2497,11 @@ const AdminDefinition = { it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.NOTIFICATIONS)), it.stateIsFalse('EmailSettings.SendEmailNotifications'), ), - validate: validators.isRequired(t('admin.environment.notifications.feedbackEmail.required'), '"Notification From Address" is required'), + + // MM-50952 + // If the setting is hidden, then it is not being set in state so there is + // nothing to validate, and validation would fail anyways and prevent saving + validate: it.configIsFalse('ExperimentalSettings', 'RestrictSystemAdmin') && validators.isRequired(t('admin.environment.notifications.feedbackEmail.required'), '"Notification From Address" is required'), }, { type: Constants.SettingsTypes.TYPE_TEXT, From d8d3c6e7a65ea7140647218280c72873f380a606 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Mon, 27 Mar 2023 10:28:16 +0530 Subject: [PATCH 03/20] MM-51699: Skip flaky test (#22635) https://mattermost.atlassian.net/browse/MM-51699 ```release-note NONE ``` --- server/boards/app/boards_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/boards/app/boards_test.go b/server/boards/app/boards_test.go index 97dbe1ccd0..9ea4c4b59d 100644 --- a/server/boards/app/boards_test.go +++ b/server/boards/app/boards_test.go @@ -140,6 +140,7 @@ func TestAddMemberToBoard(t *testing.T) { } func TestPatchBoard(t *testing.T) { + t.Skip("MM-51699") th, tearDown := SetupTestHelper(t) defer tearDown() From 5072cd7bdf3b6834112a2ea1a783baafb930e158 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Mon, 27 Mar 2023 10:01:02 +0300 Subject: [PATCH 04/20] Trigger master branch for builds (#22616) --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4f7ec1b64a..e6b27c644d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,7 +7,7 @@ stages: include: - project: mattermost/ci/mattermost-server - ref: monorepo-testing + ref: master file: private.yml variables: From ee068726bcd3519bb45ed47c222cc4cd99e2ba5a Mon Sep 17 00:00:00 2001 From: Allan Guwatudde Date: Mon, 27 Mar 2023 10:50:45 +0300 Subject: [PATCH 05/20] [MM-51467] - NotifyAdmin job reports an error for unlicensed servers (#22568) * [MM-51467] - Reduce frequency for notify install plugin job * [MM-51467] - NotifyAdmin job reports an error for unlicensed servers * . * fix imports --- server/channels/app/notify_admin.go | 6 +++--- .../channels/jobs/notify_admin/install_plugin_scheduler.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/channels/app/notify_admin.go b/server/channels/app/notify_admin.go index 908134b4b2..87a294c55f 100644 --- a/server/channels/app/notify_admin.go +++ b/server/channels/app/notify_admin.go @@ -48,12 +48,12 @@ func (a *App) SaveAdminNotification(userId string, notifyData *model.NotifyAdmin func (a *App) DoCheckForAdminNotifications(trial bool) *model.AppError { ctx := request.EmptyContext(a.Srv().Log()) + currentSKU := "starter" license := a.Srv().License() - if license == nil { - return model.NewAppError("DoCheckForAdminNotifications", "app.notify_admin.send_notification_post.app_error", nil, "No license found", http.StatusInternalServerError) + if license != nil { + currentSKU = license.SkuShortName } - currentSKU := license.SkuShortName workspaceName := "" return a.SendNotifyAdminPosts(ctx, workspaceName, currentSKU, trial) diff --git a/server/channels/jobs/notify_admin/install_plugin_scheduler.go b/server/channels/jobs/notify_admin/install_plugin_scheduler.go index 36b9818635..91ebdb79c1 100644 --- a/server/channels/jobs/notify_admin/install_plugin_scheduler.go +++ b/server/channels/jobs/notify_admin/install_plugin_scheduler.go @@ -12,7 +12,7 @@ import ( "github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog" ) -const installPluginSchedFreq = 1 * time.Minute +const installPluginSchedFreq = 24 * time.Hour func MakeInstallPluginScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler { isEnabled := func(cfg *model.Config) bool { From 3e85a9bb3ac71480ec3e8b0ffd2c322159a644e7 Mon Sep 17 00:00:00 2001 From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Date: Mon, 27 Mar 2023 13:23:05 +0530 Subject: [PATCH 06/20] Updated query to support old mysql version (#22606) * Updated query to support old mysql version * Added tests * Using foundation for tests * Removed unused override params * Removed unused override params --- .../store/sqlstore/boards_migrator.go | 3 +++ .../store/sqlstore/data_migrations.go | 6 ++--- .../store/sqlstore/data_migrations_test.go | 23 +++++++++++++++++++ ...testDeDuplicateCategoryBoardsMigration.sql | 9 ++++++++ .../{helpers_test.go => helpers.go} | 4 ++++ .../boards/services/store/sqlstore/testlib.go | 1 + 6 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 server/boards/services/store/sqlstore/fixtures/testDeDuplicateCategoryBoardsMigration.sql rename server/boards/services/store/sqlstore/migrationstests/{helpers_test.go => helpers.go} (90%) diff --git a/server/boards/services/store/sqlstore/boards_migrator.go b/server/boards/services/store/sqlstore/boards_migrator.go index 99395e9e46..79afeddc66 100644 --- a/server/boards/services/store/sqlstore/boards_migrator.go +++ b/server/boards/services/store/sqlstore/boards_migrator.go @@ -231,6 +231,9 @@ func (bm *BoardsMigrator) MigrateToStep(step int) error { func (bm *BoardsMigrator) Interceptors() map[int]foundation.Interceptor { return map[int]foundation.Interceptor{ 18: bm.store.RunDeletedMembershipBoardsMigration, + 35: func() error { + return bm.store.RunDeDuplicateCategoryBoardsMigration(35) + }, } } diff --git a/server/boards/services/store/sqlstore/data_migrations.go b/server/boards/services/store/sqlstore/data_migrations.go index a1404afac8..8e18022319 100644 --- a/server/boards/services/store/sqlstore/data_migrations.go +++ b/server/boards/services/store/sqlstore/data_migrations.go @@ -863,10 +863,8 @@ func (s *SQLStore) doesDuplicateCategoryBoardsExist() (bool, error) { } func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error { - query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " + - "FROM " + s.tablePrefix + "category_boards) " + - "DELETE " + s.tablePrefix + "category_boards FROM " + s.tablePrefix + "category_boards " + - "JOIN duplicates USING(id) WHERE duplicates.rownum > 1;" + query := "DELETE FROM " + s.tablePrefix + "category_boards WHERE id NOT IN " + + "(SELECT * FROM ( SELECT min(id) FROM " + s.tablePrefix + "category_boards GROUP BY user_id, board_id ) as data)" if _, err := s.db.Exec(query); err != nil { s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err)) } diff --git a/server/boards/services/store/sqlstore/data_migrations_test.go b/server/boards/services/store/sqlstore/data_migrations_test.go index e5aae4de52..5a44f9ca2e 100644 --- a/server/boards/services/store/sqlstore/data_migrations_test.go +++ b/server/boards/services/store/sqlstore/data_migrations_test.go @@ -7,6 +7,9 @@ import ( "testing" "time" + "github.com/mattermost/mattermost-server/v6/server/boards/services/store/sqlstore/migrationstests" + "github.com/mgdelacroix/foundation" + "github.com/mattermost/mattermost-server/v6/server/boards/model" "github.com/stretchr/testify/assert" @@ -263,3 +266,23 @@ func TestCheckForMismatchedCollation(t *testing.T) { } }) } + +func TestRunDeDuplicateCategoryBoardsMigration(t *testing.T) { + RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { + th, tearDown := migrationstests.SetupTestHelper(t, f) + defer tearDown() + + th.F().MigrateToStepSkippingLastInterceptor(35). + ExecFile("./fixtures/testDeDuplicateCategoryBoardsMigration.sql") + + th.F().RunInterceptor(35) + + // verifying count of rows + var count int + countQuery := "SELECT COUNT(*) FROM focalboard_category_boards" + row := th.F().DB().QueryRow(countQuery) + err := row.Scan(&count) + assert.NoError(t, err) + assert.Equal(t, 4, count) + }) +} diff --git a/server/boards/services/store/sqlstore/fixtures/testDeDuplicateCategoryBoardsMigration.sql b/server/boards/services/store/sqlstore/fixtures/testDeDuplicateCategoryBoardsMigration.sql new file mode 100644 index 0000000000..69a7dc9bde --- /dev/null +++ b/server/boards/services/store/sqlstore/fixtures/testDeDuplicateCategoryBoardsMigration.sql @@ -0,0 +1,9 @@ +INSERT INTO focalboard_category_boards(id, user_id, category_id, board_id, create_at, update_at, sort_order) +VALUES + ('id_1', 'user_id_1', 'category_id_1', 'board_id_1', 0, 0, 0), + ('id_2', 'user_id_1', 'category_id_2', 'board_id_1', 0, 0, 0), + ('id_3', 'user_id_1', 'category_id_3', 'board_id_1', 0, 0, 0), + ('id_4', 'user_id_2', 'category_id_4', 'board_id_2', 0, 0, 0), + ('id_5', 'user_id_2', 'category_id_5', 'board_id_2', 0, 0, 0), + ('id_6', 'user_id_3', 'category_id_6', 'board_id_3', 0, 0, 0), + ('id_7', 'user_id_4', 'category_id_6', 'board_id_4', 0, 0, 0); diff --git a/server/boards/services/store/sqlstore/migrationstests/helpers_test.go b/server/boards/services/store/sqlstore/migrationstests/helpers.go similarity index 90% rename from server/boards/services/store/sqlstore/migrationstests/helpers_test.go rename to server/boards/services/store/sqlstore/migrationstests/helpers.go index a6d4696f14..a674d5f988 100644 --- a/server/boards/services/store/sqlstore/migrationstests/helpers_test.go +++ b/server/boards/services/store/sqlstore/migrationstests/helpers.go @@ -22,6 +22,10 @@ func (th *TestHelper) IsMySQL() bool { return th.f.DB().DriverName() == "mysql" } +func (th *TestHelper) F() *foundation.Foundation { + return th.f +} + func SetupTestHelper(t *testing.T, f *foundation.Foundation) (*TestHelper, func()) { th := &TestHelper{t, f} diff --git a/server/boards/services/store/sqlstore/testlib.go b/server/boards/services/store/sqlstore/testlib.go index 9ea7de4301..a79b2a1643 100644 --- a/server/boards/services/store/sqlstore/testlib.go +++ b/server/boards/services/store/sqlstore/testlib.go @@ -50,6 +50,7 @@ func NewStoreType(name string, driver string, skipMigrations bool) *storeType { DB: sqlDB, IsPlugin: false, // ToDo: to be removed } + store, err := New(storeParams) if err != nil { panic(fmt.Sprintf("cannot create store: %s", err)) From 26c3b4668b117d2e98db10bd5b6603831a6a2e81 Mon Sep 17 00:00:00 2001 From: Kyriakos Z <3829551+koox00@users.noreply.github.com> Date: Mon, 27 Mar 2023 13:42:27 +0300 Subject: [PATCH 07/20] MM-51436: fixes broken link (#22655) --- .../src/components/post_priority/post_priority_picker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/channels/src/components/post_priority/post_priority_picker.tsx b/webapp/channels/src/components/post_priority/post_priority_picker.tsx index 568c6a1fa9..6f8ca90d30 100644 --- a/webapp/channels/src/components/post_priority/post_priority_picker.tsx +++ b/webapp/channels/src/components/post_priority/post_priority_picker.tsx @@ -155,7 +155,7 @@ function PostPriorityPicker({ } } - const feedbackLink = postAcknowledgementsEnabled ? 'https://forms.gle/noA8Azg7RdaBZtMB6' : 'https://forms.gle/XRb63s3KZqpLNyqr9'; + const feedbackLink = postAcknowledgementsEnabled ? 'https://forms.gle/noA8Azg7RdaBZtMB6' : 'https://forms.gle/mMcRFQzyKAo9Sv49A'; return ( Date: Mon, 27 Mar 2023 08:21:29 -0500 Subject: [PATCH 08/20] return 404 if the enterprise library error returned is 404 (#22628) --- server/channels/api4/hosted_customer.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/channels/api4/hosted_customer.go b/server/channels/api4/hosted_customer.go index 4792969c15..cead966f41 100644 --- a/server/channels/api4/hosted_customer.go +++ b/server/channels/api4/hosted_customer.go @@ -13,6 +13,8 @@ import ( "reflect" "time" + "github.com/pkg/errors" + "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/server/channels/utils" "github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog" @@ -250,6 +252,10 @@ func selfHostedInvoices(c *Context, w http.ResponseWriter, r *http.Request) { invoices, err := c.App.Cloud().GetSelfHostedInvoices() if err != nil { + if err.Error() == "404" { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotFound).Wrap(errors.New("invoices for license not found")) + return + } c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return } From 5215a0c30d9f027a58a2d062601cc60eb302cff0 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Mon, 27 Mar 2023 10:54:06 -0300 Subject: [PATCH 09/20] update LICENSE.txt (#22659) --- LICENSE.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 8ced25a132..eb417456e4 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -11,8 +11,8 @@ You may be licensed to use source code to create compiled versions not produced 1. Under the Free Software Foundation’s GNU AGPL v.3.0, subject to the exceptions outlined in this policy; or 2. Under a commercial license available from Mattermost, Inc. by contacting commercial@mattermost.com -You are licensed to use the source code in Admin Tools and Configuration Files (templates/, config/default.json, i18n/, model/, -plugin/ and all subdirectories thereof) under the Apache License v2.0. +You are licensed to use the source code in Admin Tools and Configuration Files (server/templates/, server/i18n/, model/, +plugin/, server/boards/, server/playbooks/, webapp/ and all subdirectories thereof) under the Apache License v2.0. We promise that we will not enforce the copyleft provisions in AGPL v3.0 against you if your application (a) does not link to the Mattermost Platform directly, but exclusively uses the Mattermost Admin Tools and Configuration Files, and From 865b3d75e7f69fa582a504f945226a41f442a950 Mon Sep 17 00:00:00 2001 From: Allan Guwatudde Date: Mon, 27 Mar 2023 17:42:07 +0300 Subject: [PATCH 10/20] [MM-50976] - Contact Support to redirect to Zendesk and pre-fill known information (#22640) --- .../cancel_subscription.tsx | 14 +-- .../cloud_trial_banner.tsx | 3 +- .../contact_sales_card.tsx | 10 +- .../billing/billing_subscriptions/index.tsx | 13 +-- .../limit_reached_banner.test.tsx | 2 +- .../limit_reached_banner.tsx | 4 +- .../billing/billing_subscriptions/limits.tsx | 4 +- .../to_yearly_nudge_banner.tsx | 3 +- .../billing/delete_workspace/result_modal.tsx | 13 +-- .../feature_discovery.test.tsx | 3 - .../feature_discovery/feature_discovery.tsx | 28 +++-- .../admin_console/feature_discovery/index.tsx | 8 +- .../enterprise_edition_right_panel.test.tsx | 93 ++++++++++++---- .../renew_license_card.test.tsx | 35 +++++- .../workspace-optimization/dashboard.data.tsx | 7 +- .../contact_sales/contact_us.tsx | 7 +- .../overage_users_banner/index.tsx | 6 +- .../overage_users_banner.test.tsx | 12 ++- .../renewal_link/renewal_link.test.tsx | 39 ++++++- .../renewal_link/renewal_link.tsx | 7 +- .../cloud_subscribe_result_modal/error.tsx | 8 +- .../common/hooks/useOpenSalesLink.ts | 34 +++++- .../common/hooks/useOpenZendeskForm.ts | 26 +++++ .../pricing_modal/contact_sales_cta.tsx | 12 +-- .../src/components/pricing_modal/content.tsx | 13 ++- .../pricing_modal/self_hosted_content.tsx | 6 +- .../src/components/purchase_modal/index.ts | 17 ++- .../purchase_modal/purchase_modal.tsx | 13 ++- .../contact_sales_link.tsx | 7 +- .../self_hosted_purchase_modal/error.tsx | 6 +- .../ad_ldap_upsell_banner.tsx | 16 +-- webapp/channels/src/selectors/cloud.ts | 41 ------- .../src/utils/contact_support_sales.ts | 100 ++++++++++++++++++ 33 files changed, 416 insertions(+), 194 deletions(-) create mode 100644 webapp/channels/src/components/common/hooks/useOpenZendeskForm.ts create mode 100644 webapp/channels/src/utils/contact_support_sales.ts diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cancel_subscription.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cancel_subscription.tsx index 68f5503d66..cf8560ed3b 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cancel_subscription.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cancel_subscription.tsx @@ -5,16 +5,12 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import {trackEvent} from 'actions/telemetry_actions'; +import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm'; import ExternalLink from 'components/external_link'; -type Props = { - cancelAccountLink: any; -} - -const CancelSubscription = (props: Props) => { - const { - cancelAccountLink, - } = props; +const CancelSubscription = () => { + const description = `I am requesting that workspace "${window.location.host}" be deleted`; + const [, contactSupportURL] = useOpenCloudZendeskSupportForm('Request workspace be deleted', description); return (
@@ -33,7 +29,7 @@ const CancelSubscription = (props: Props) => {
trackEvent('cloud_admin', 'click_contact_us')} > diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cloud_trial_banner.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cloud_trial_banner.tsx index 204495ce90..a62edc1e54 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cloud_trial_banner.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cloud_trial_banner.tsx @@ -24,7 +24,6 @@ import AlertBanner from 'components/alert_banner'; import UpgradeLink from 'components/widgets/links/upgrade_link'; import './cloud_trial_banner.scss'; -import {SalesInquiryIssue} from 'selectors/cloud'; export interface Props { trialEndDate: number; @@ -34,7 +33,7 @@ const CloudTrialBanner = ({trialEndDate}: Props): JSX.Element | null => { const endDate = new Date(trialEndDate); const DISMISSED_DAYS = 10; const {formatMessage} = useIntl(); - const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise); + const [openSalesLink] = useOpenSalesLink(); const dispatch = useDispatch(); const user = useSelector(getCurrentUser); const storedDismissedEndDate = useSelector((state: GlobalState) => getPreference(state, Preferences.CLOUD_TRIAL_BANNER, CloudBanners.UPGRADE_FROM_TRIAL)); diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx index 0687b78857..3e5bec63e9 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx @@ -10,21 +10,19 @@ import {CloudLinks, CloudProducts} from 'utils/constants'; import PrivateCloudSvg from 'components/common/svg_images_components/private_cloud_svg'; import CloudTrialSvg from 'components/common/svg_images_components/cloud_trial_svg'; import {TelemetryProps} from 'components/common/hooks/useOpenPricingModal'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import ExternalLink from 'components/external_link'; type Props = { - contactSalesLink: any; isFreeTrial: boolean; - trialQuestionsLink: any; subscriptionPlan: string | undefined; onUpgradeMattermostCloud: (telemetryProps?: TelemetryProps | undefined) => void; } const ContactSalesCard = (props: Props) => { + const [openSalesLink, contactSalesLink] = useOpenSalesLink(); const { - contactSalesLink, isFreeTrial, - trialQuestionsLink, subscriptionPlan, onUpgradeMattermostCloud, } = props; @@ -145,7 +143,7 @@ const ContactSalesCard = (props: Props) => { {(isFreeTrial || subscriptionPlan === CloudProducts.ENTERPRISE || isCloudLegacyPlan) && trackEvent('cloud_admin', 'click_contact_sales')} > @@ -163,7 +161,7 @@ const ContactSalesCard = (props: Props) => { if (subscriptionPlan === CloudProducts.STARTER) { onUpgradeMattermostCloud({trackingLocation: 'admin_console_subscription_card_upgrade_now_button'}); } else { - window.open(contactSalesLink, '_blank'); + openSalesLink(); } }} className='PrivateCloudCard__actionButton' diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/index.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/index.tsx index f4f07ab0f9..ac6bdb6ef1 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/index.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/index.tsx @@ -13,7 +13,6 @@ import FormattedAdminHeader from 'components/widgets/admin_console/formatted_adm import CloudTrialBanner from 'components/admin_console/billing/billing_subscriptions/cloud_trial_banner'; import CloudFetchError from 'components/cloud_fetch_error'; -import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud'; import { getSubscriptionProduct, getCloudSubscription as selectCloudSubscription, @@ -63,9 +62,6 @@ const BillingSubscriptions = () => { const isCardExpired = isCustomerCardExpired(useSelector(selectCloudCustomer)); - const contactSalesLink = useSelector(getCloudContactUsLink)(InquiryType.Sales); - const cancelAccountLink = useSelector(getCloudContactUsLink)(InquiryType.Sales, SalesInquiryIssue.CancelAccount); - const trialQuestionsLink = useSelector(getCloudContactUsLink)(InquiryType.Sales, SalesInquiryIssue.TrialQuestions); const trialEndDate = subscription?.trial_end_at || 0; const [showCreditCardBanner, setShowCreditCardBanner] = useState(true); @@ -159,19 +155,12 @@ const BillingSubscriptions = () => { ) : ( )} - {isAnnualProfessionalOrEnterprise && !isFreeTrial ? - : - - } + {isAnnualProfessionalOrEnterprise && !isFreeTrial ? : } }
diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.test.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.test.tsx index 0aa08eddb7..66e4356434 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.test.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.test.tsx @@ -177,7 +177,7 @@ describe('limits_reached_banner', () => { const store = mockStore(state); const spies = makeSpies(); const mockOpenSalesLink = jest.fn(); - spies.useOpenSalesLink.mockReturnValue(mockOpenSalesLink); + spies.useOpenSalesLink.mockReturnValue([mockOpenSalesLink, '']); spies.useGetUsageDeltas.mockReturnValue(someLimitReached); renderWithIntl(); screen.getByText(titleFree); diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.tsx index d006716bea..666adc3bd0 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.tsx @@ -5,8 +5,6 @@ import React from 'react'; import {useIntl, FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; -import {SalesInquiryIssue} from 'selectors/cloud'; - import {CloudProducts} from 'utils/constants'; import {anyUsageDeltaExceededLimit} from 'utils/limits'; @@ -33,7 +31,7 @@ const LimitReachedBanner = (props: Props) => { const hasDismissedBanner = useSelector(getHasDismissedSystemConsoleLimitReached); - const openSalesLink = useOpenSalesLink(props.product?.sku === CloudProducts.PROFESSIONAL ? SalesInquiryIssue.UpgradeEnterprise : undefined); + const [openSalesLink] = useOpenSalesLink(); const openPricingModal = useOpenPricingModal(); const saveBool = useSaveBool(); if (hasDismissedBanner || !someLimitExceeded || !props.product || (props.product.sku !== CloudProducts.STARTER)) { diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limits.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limits.tsx index 2798c1374a..3457fbd201 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limits.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limits.tsx @@ -11,8 +11,6 @@ import { getSubscriptionProduct, } from 'mattermost-redux/selectors/entities/cloud'; -import {SalesInquiryIssue} from 'selectors/cloud'; - import {CloudProducts} from 'utils/constants'; import {asGBString, fallbackStarterLimits, hasSomeLimits} from 'utils/limits'; @@ -32,7 +30,7 @@ const Limits = (): JSX.Element | null => { const subscriptionProduct = useSelector(getSubscriptionProduct); const [cloudLimits, limitsLoaded] = useGetLimits(); const usage = useGetUsage(); - const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise); + const [openSalesLink] = useOpenSalesLink(); const openPricingModal = useOpenPricingModal(); if (!subscriptionProduct || !limitsLoaded || !hasSomeLimits(cloudLimits)) { diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx index 1bbf9823b3..3cc1e8aa75 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx @@ -10,7 +10,6 @@ import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurch import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import AnnouncementBar from 'components/announcement_bar/default_announcement_bar'; -import {SalesInquiryIssue} from 'selectors/cloud'; 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'; @@ -79,7 +78,7 @@ const ToYearlyNudgeBannerDismissable = () => { const ToYearlyNudgeBanner = () => { const {formatMessage} = useIntl(); - const openSalesLink = useOpenSalesLink(SalesInquiryIssue.AboutPurchasing); + const [openSalesLink] = useOpenSalesLink(); const openPurchaseModal = useOpenCloudPurchaseModal({}); const product = useSelector(selectSubscriptionProduct); diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx index b84c72ee90..bc452d3c57 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx @@ -7,6 +7,7 @@ import {useDispatch, useSelector} from 'react-redux'; import IconMessage from 'components/purchase_modal/icon_message'; import FullScreenModal from 'components/widgets/modals/full_screen_modal'; +import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm'; import {closeModal} from 'actions/views/modals'; import {isModalOpen} from 'selectors/views/modals'; @@ -14,9 +15,6 @@ import {GlobalState} from 'types/store'; import './result_modal.scss'; -import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; -import {InquiryType} from 'selectors/cloud'; - type Props = { onHide?: () => void; icon: JSX.Element; @@ -33,7 +31,7 @@ type Props = { export default function ResultModal(props: Props) { const dispatch = useDispatch(); - const openContactUs = useOpenSalesLink(undefined, InquiryType.Technical); + const [openContactSupport] = useOpenCloudZendeskSupportForm('Delete workspace', ''); const isResultModalOpen = useSelector((state: GlobalState) => isModalOpen(state, props.identifier), @@ -64,14 +62,13 @@ export default function ResultModal(props: Props) { buttonHandler={props.primaryButtonHandler} className={'success'} formattedTertiaryButonText={ - props.contactSupportButtonVisible ? + props.contactSupportButtonVisible ? ( : - undefined + />) : undefined } - tertiaryButtonHandler={props.contactSupportButtonVisible ? openContactUs : undefined} + tertiaryButtonHandler={props.contactSupportButtonVisible ? openContactSupport : undefined} />
diff --git a/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx b/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx index 2b8e5d19b2..e8589a0c09 100644 --- a/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx +++ b/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx @@ -17,7 +17,6 @@ describe('components/feature_discovery', () => { { { }); } + contactSalesFunc = () => { + const {customer, isCloud} = this.props; + const customerEmail = customer?.email || ''; + const firstName = customer?.contact_first_name || ''; + const lastName = customer?.contact_last_name || ''; + const companyName = customer?.name || ''; + const utmMedium = isCloud ? 'in-product-cloud' : 'in-product'; + goToMattermostContactSalesForm(firstName, lastName, companyName, customerEmail, 'mattermost', utmMedium); + } + renderPostTrialCta = () => { const { minimumSKURequiredForFeature, @@ -110,7 +122,7 @@ export default class FeatureDiscovery extends React.PureComponent data-testid='featureDiscovery_primaryCallToAction' onClick={() => { trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery'); - window.open(LicenseLinks.CONTACT_SALES, '_blank'); + this.contactSalesFunc(); }} > hadPrevCloudTrial, isPaidSubscription, minimumSKURequiredForFeature, - contactSalesLink, } = this.props; const canRequestCloudFreeTrial = isCloud && !isCloudTrial && !hadPrevCloudTrial && !isPaidSubscription; @@ -217,11 +228,10 @@ export default class FeatureDiscovery extends React.PureComponent onClick={() => { if (isCloud) { trackEvent(TELEMETRY_CATEGORIES.CLOUD_ADMIN, 'click_enterprise_contact_sales_feature_discovery'); - window.open(contactSalesLink, '_blank'); } else { trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery'); - window.open(LicenseLinks.CONTACT_SALES, '_blank'); } + this.contactSalesFunc(); }} > { const license = { IsLicensed: 'true', @@ -28,8 +61,11 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris } as EnterpriseEditionProps; test('should render for no Gov no Trial no Enterprise', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Upgrade to the Enterprise Plan'); @@ -43,11 +79,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris }); test('should render for Gov no Trial no Enterprise', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Upgrade to the Enterprise Gov Plan'); @@ -61,11 +100,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris }); test('should render for Enterprise no Trial', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Need to increase your headcount?'); @@ -73,11 +115,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris }); test('should render for E20 no Trial', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Need to increase your headcount?'); @@ -85,11 +130,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris }); test('should render for Trial no Gov', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Purchase the Enterprise Plan'); @@ -97,11 +145,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris }); test('should render for Trial Gov', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Purchase the Enterprise Gov Plan'); diff --git a/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx b/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx index eb43a3cf0f..c52e8272d7 100644 --- a/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx @@ -12,6 +12,37 @@ import mockStore from 'tests/test_store'; import RenewalLicenseCard from './renew_license_card'; +const initialState = { + views: { + announcementBar: { + announcementBarState: { + announcementBarCount: 1, + }, + }, + }, + entities: { + general: { + config: { + CWSURL: '', + }, + license: { + IsLicensed: 'true', + Cloud: 'true', + }, + }, + users: { + currentUserId: 'current_user_id', + profiles: { + current_user_id: {roles: 'system_user'}, + }, + }, + preferences: { + myPreferences: {}, + }, + cloud: {}, + }, +}; + const actImmediate = (wrapper: ReactWrapper) => act( () => @@ -47,7 +78,7 @@ describe('components/RenewalLicenseCard', () => { }); }); getRenewalLinkSpy.mockImplementation(() => promise); - const store = mockStore({}); + const store = mockStore(initialState); const wrapper = mountWithIntl(); // wait for the promise to resolve and component to update @@ -64,7 +95,7 @@ describe('components/RenewalLicenseCard', () => { reject(new Error('License cannot be renewed from portal')); }); getRenewalLinkSpy.mockImplementation(() => promise); - const store = mockStore({}); + const store = mockStore(initialState); const wrapper = mountWithIntl(); // wait for the promise to resolve and component to update diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx index b9477f36de..dd5ee9b565 100644 --- a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx @@ -18,8 +18,9 @@ import {getLicense} from 'mattermost-redux/selectors/entities/general'; import {GlobalState} from '@mattermost/types/store'; -import {CloudLinks, ConsolePages, DocLinks, LicenseLinks} from 'utils/constants'; +import {CloudLinks, ConsolePages, DocLinks} from 'utils/constants'; import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; export type DataModel = { [key: string]: { @@ -84,8 +85,10 @@ const useMetricsData = () => { const isEnterpriseLicense = isEnterpriseOrE20License(license); const isStarterLicense = getIsStarterLicense(license); + const [, contactSalesLink] = useOpenSalesLink(); + const trialOrEnterpriseCtaConfig = { - configUrl: canStartTrial ? ConsolePages.LICENSE : LicenseLinks.CONTACT_SALES, + 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'}), }; diff --git a/webapp/channels/src/components/announcement_bar/contact_sales/contact_us.tsx b/webapp/channels/src/components/announcement_bar/contact_sales/contact_us.tsx index 48abe4a4dd..74b576192d 100644 --- a/webapp/channels/src/components/announcement_bar/contact_sales/contact_us.tsx +++ b/webapp/channels/src/components/announcement_bar/contact_sales/contact_us.tsx @@ -6,8 +6,9 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import {trackEvent} from 'actions/telemetry_actions'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; + import './contact_us.scss'; -import {LicenseLinks} from '../../../utils/constants'; export interface Props { buttonTextElement?: JSX.Element; @@ -16,10 +17,12 @@ export interface Props { } const ContactUsButton: React.FC = (props: Props) => { + const [openContactSales] = useOpenSalesLink(); + const handleContactUsLinkClick = async (e: React.MouseEvent) => { e.preventDefault(); trackEvent('admin', props.eventID || 'in_trial_contact_sales'); - window.open(LicenseLinks.CONTACT_SALES, '_blank'); + openContactSales(); }; return ( diff --git a/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx b/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx index 176af1dbd2..d22fe6389f 100644 --- a/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx +++ b/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx @@ -15,7 +15,8 @@ import {savePreferences} from 'mattermost-redux/actions/preferences'; import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; import {PreferenceType} from '@mattermost/types/preferences'; import {useExpandOverageUsersCheck} from 'components/common/hooks/useExpandOverageUsersCheck'; -import {LicenseLinks, StatTypes, Preferences, AnnouncementBarTypes} from 'utils/constants'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; +import {StatTypes, Preferences, AnnouncementBarTypes} from 'utils/constants'; import './overage_users_banner.scss'; @@ -34,6 +35,7 @@ const adminHasDismissed = ({preferenceName, overagePreferences, isWarningBanner} }; const OverageUsersBanner = () => { + const [openContactSales] = useOpenSalesLink(); const dispatch = useDispatch(); const stats = useSelector((state: GlobalState) => state.entities.admin.analytics) || {}; const isAdmin = useSelector(isCurrentUserSystemAdmin); @@ -90,7 +92,7 @@ const OverageUsersBanner = () => { const handleContactSalesClick = (e: React.MouseEvent) => { e.preventDefault(); trackEventFn('Contact Sales'); - window.open(LicenseLinks.CONTACT_SALES, '_blank'); + openContactSales(); }; const handleClick = isExpandable ? handleUpdateSeatsSelfServeClick : handleContactSalesClick; diff --git a/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx b/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx index 212c9dadae..dedd30a7c9 100644 --- a/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx +++ b/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx @@ -7,7 +7,7 @@ import {fireEvent, screen} from '@testing-library/react'; import {DeepPartial} from '@mattermost/types/utilities'; import {GlobalState} from 'types/store'; import {General} from 'mattermost-redux/constants'; -import {LicenseLinks, OverActiveUserLimits, Preferences, StatTypes} from 'utils/constants'; +import {OverActiveUserLimits, Preferences, StatTypes} from 'utils/constants'; import {renderWithIntlAndStore} from 'tests/react_testing_utils'; import {savePreferences} from 'mattermost-redux/actions/preferences'; import {trackEvent} from 'actions/telemetry_actions'; @@ -249,7 +249,10 @@ describe('components/overage_users_banner', () => { fireEvent.click(screen.getByText(contactSalesTextLink)); expect(windowSpy).toBeCalledTimes(1); - expect(windowSpy).toBeCalledWith(LicenseLinks.CONTACT_SALES, '_blank'); + + // only the email is encoded and other params are empty. See logic for useOpenSalesLink hook + const salesLinkWithEncodedParams = 'https://mattermost.com/contact-sales/?qk=&qp=&qw=&qx=dGVzdEBtYXR0ZXJtb3N0LmNvbQ==&utm_source=mattermost&utm_medium=in-product'; + expect(windowSpy).toBeCalledWith(salesLinkWithEncodedParams, '_blank'); expect(trackEvent).toBeCalledTimes(1); expect(trackEvent).toBeCalledWith('insights', 'click_true_up_warning', { cta: 'Contact Sales', @@ -368,7 +371,10 @@ describe('components/overage_users_banner', () => { fireEvent.click(screen.getByText(contactSalesTextLink)); expect(windowSpy).toBeCalledTimes(1); - expect(windowSpy).toBeCalledWith(LicenseLinks.CONTACT_SALES, '_blank'); + + // only the email is encoded and other params are empty. See logic for useOpenSalesLink hook + const salesLinkWithEncodedParams = 'https://mattermost.com/contact-sales/?qk=&qp=&qw=&qx=dGVzdEBtYXR0ZXJtb3N0LmNvbQ==&utm_source=mattermost&utm_medium=in-product'; + expect(windowSpy).toBeCalledWith(salesLinkWithEncodedParams, '_blank'); expect(trackEvent).toBeCalledTimes(1); expect(trackEvent).toBeCalledWith('insights', 'click_true_up_error', { cta: 'Contact Sales', diff --git a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx index 7582add168..9dae6a38d0 100644 --- a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx +++ b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx @@ -3,13 +3,46 @@ import React from 'react'; import {ReactWrapper} from 'enzyme'; +import {Provider} from 'react-redux'; import {act} from 'react-dom/test-utils'; import {Client4} from 'mattermost-redux/client'; import {mountWithIntl} from 'tests/helpers/intl-test-helper'; +import mockStore from 'tests/test_store'; import RenewalLink from './renewal_link'; +const initialState = { + views: { + announcementBar: { + announcementBarState: { + announcementBarCount: 1, + }, + }, + }, + entities: { + general: { + config: { + CWSURL: '', + }, + license: { + IsLicensed: 'true', + Cloud: 'true', + }, + }, + users: { + currentUserId: 'current_user_id', + profiles: { + current_user_id: {roles: 'system_user'}, + }, + }, + preferences: { + myPreferences: {}, + }, + cloud: {}, + }, +}; + const actImmediate = (wrapper: ReactWrapper) => act( () => @@ -40,7 +73,8 @@ describe('components/RenewalLink', () => { }); }); getRenewalLinkSpy.mockImplementation(() => promise); - const wrapper = mountWithIntl(); + const store = mockStore(initialState); + const wrapper = mountWithIntl(); // wait for the promise to resolve and component to update await actImmediate(wrapper); @@ -54,7 +88,8 @@ describe('components/RenewalLink', () => { reject(new Error('License cannot be renewed from portal')); }); getRenewalLinkSpy.mockImplementation(() => promise); - const wrapper = mountWithIntl(); + const store = mockStore(initialState); + const wrapper = mountWithIntl(); // wait for the promise to resolve and component to update await actImmediate(wrapper); diff --git a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx index 3ca79b1e11..dfae9f3ea1 100644 --- a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx +++ b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx @@ -11,9 +11,9 @@ import {trackEvent} from 'actions/telemetry_actions'; import {ModalData} from 'types/actions'; import { - LicenseLinks, ModalIdentifiers, } from 'utils/constants'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import NoInternetConnection from '../no_internet_connection/no_internet_connection'; @@ -31,6 +31,9 @@ export interface RenewalLinkProps { const RenewalLink = (props: RenewalLinkProps) => { const [renewalLink, setRenewalLink] = useState(''); const [manualInterventionRequired, setManualInterventionRequired] = useState(false); + + const [openContactSales] = useOpenSalesLink(); + useEffect(() => { Client4.getRenewalLink().then(({renewal_link: renewalLinkParam}) => { try { @@ -55,7 +58,7 @@ const RenewalLink = (props: RenewalLinkProps) => { } window.open(renewalLink, '_blank'); } else if (manualInterventionRequired) { - window.open(LicenseLinks.CONTACT_SALES, '_blank'); + openContactSales(); } else { showConnectionErrorModal(); } diff --git a/webapp/channels/src/components/cloud_subscribe_result_modal/error.tsx b/webapp/channels/src/components/cloud_subscribe_result_modal/error.tsx index 0b61d3598e..2340430c02 100644 --- a/webapp/channels/src/components/cloud_subscribe_result_modal/error.tsx +++ b/webapp/channels/src/components/cloud_subscribe_result_modal/error.tsx @@ -13,9 +13,8 @@ import PaymentFailedSvg from 'components/common/svg_images_components/payment_fa import IconMessage from 'components/purchase_modal/icon_message'; import FullScreenModal from 'components/widgets/modals/full_screen_modal'; -import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; +import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm'; -import {InquiryType} from 'selectors/cloud'; import {closeModal} from 'actions/views/modals'; import {ModalIdentifiers} from 'utils/constants'; import {isModalOpen} from 'selectors/views/modals'; @@ -31,7 +30,8 @@ type Props = { function ErrorModal(props: Props) { const dispatch = useDispatch(); const subscriptionProduct = useSelector(getSubscriptionProduct); - const openContactUs = useOpenSalesLink(undefined, InquiryType.Technical); + + const [openContactSupport] = useOpenCloudZendeskSupportForm('Cloud Subscription', ''); const isSuccessModalOpen = useSelector((state: GlobalState) => isModalOpen(state, ModalIdentifiers.ERROR_MODAL), @@ -97,7 +97,7 @@ function ErrorModal(props: Props) { } /> } - tertiaryButtonHandler={openContactUs} + tertiaryButtonHandler={openContactSupport} buttonHandler={onBackButtonPress} className={'success'} /> diff --git a/webapp/channels/src/components/common/hooks/useOpenSalesLink.ts b/webapp/channels/src/components/common/hooks/useOpenSalesLink.ts index aecb58844f..4dbe44d0ca 100644 --- a/webapp/channels/src/components/common/hooks/useOpenSalesLink.ts +++ b/webapp/channels/src/components/common/hooks/useOpenSalesLink.ts @@ -3,11 +3,35 @@ import {useSelector} from 'react-redux'; -import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud'; +import {getCloudCustomer, isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {buildMMURL, goToMattermostContactSalesForm} from 'utils/contact_support_sales'; +import {LicenseLinks} from 'utils/constants'; -export default function useOpenSalesLink(issue?: SalesInquiryIssue, inquireType: InquiryType = InquiryType.Sales) { - const contactSalesLink = useSelector(getCloudContactUsLink)(inquireType, issue); +export default function useOpenSalesLink(): [() => void, string] { + const isCloud = useSelector(isCurrentLicenseCloud); + const customer = useSelector(getCloudCustomer); + const currentUser = useSelector(getCurrentUser); + let customerEmail = ''; + let firstName = ''; + let lastName = ''; + let companyName = ''; + const utmSource = 'mattermost'; + let utmMedium = 'in-product'; - return () => window.open(contactSalesLink, '_blank'); + if (isCloud && customer) { + customerEmail = customer.email || ''; + firstName = customer.contact_first_name || ''; + lastName = customer.contact_last_name || ''; + companyName = customer.name || ''; + utmMedium = 'in-product-cloud'; + } else { + customerEmail = currentUser.email || ''; + } + + const contactSalesLink = buildMMURL(LicenseLinks.CONTACT_SALES, firstName, lastName, companyName, customerEmail, utmSource, utmMedium); + const goToSalesLinkFunc = () => { + goToMattermostContactSalesForm(firstName, lastName, companyName, customerEmail, utmSource, utmMedium); + }; + return [goToSalesLinkFunc, contactSalesLink]; } - diff --git a/webapp/channels/src/components/common/hooks/useOpenZendeskForm.ts b/webapp/channels/src/components/common/hooks/useOpenZendeskForm.ts new file mode 100644 index 0000000000..16df9d9a02 --- /dev/null +++ b/webapp/channels/src/components/common/hooks/useOpenZendeskForm.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useSelector} from 'react-redux'; + +import {getCloudCustomer} from 'mattermost-redux/selectors/entities/cloud'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {getCloudSupportLink, getSelfHostedSupportLink, goToCloudSupportForm, goToSelfHostedSupportForm} from 'utils/contact_support_sales'; + +export function useOpenCloudZendeskSupportForm(subject: string, description: string): [() => void, string] { + const customer = useSelector(getCloudCustomer); + const customerEmail = customer?.email || ''; + + const url = getCloudSupportLink(customerEmail, subject, description, window.location.host); + + return [() => goToCloudSupportForm(customerEmail, subject, description, window.location.host), url]; +} + +export function useOpenSelfHostedZendeskSupportForm(subject: string): [() => void, string] { + const currentUser = useSelector(getCurrentUser); + const customerEmail = currentUser.email || ''; + + const url = getSelfHostedSupportLink(customerEmail, subject); + + return [() => goToSelfHostedSupportForm(customerEmail, subject), url]; +} diff --git a/webapp/channels/src/components/pricing_modal/contact_sales_cta.tsx b/webapp/channels/src/components/pricing_modal/contact_sales_cta.tsx index 1a0c47ed2a..66e05d0947 100644 --- a/webapp/channels/src/components/pricing_modal/contact_sales_cta.tsx +++ b/webapp/channels/src/components/pricing_modal/contact_sales_cta.tsx @@ -11,8 +11,7 @@ import {trackEvent} from 'actions/telemetry_actions'; import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; -import {LicenseLinks, TELEMETRY_CATEGORIES} from 'utils/constants'; -import {SalesInquiryIssue} from 'selectors/cloud'; +import {TELEMETRY_CATEGORIES} from 'utils/constants'; const StyledA = styled.a` color: var(--denim-button-bg); @@ -27,11 +26,7 @@ text-align: center; function ContactSalesCTA() { const {formatMessage} = useIntl(); - const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise); - - const openSelfHostedLink = () => { - window.open(LicenseLinks.CONTACT_SALES, '_blank'); - }; + const [openSalesLink] = useOpenSalesLink(); const isCloud = useSelector(isCurrentLicenseCloud); @@ -42,11 +37,10 @@ function ContactSalesCTA() { e.preventDefault(); if (isCloud) { trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales'); - openSalesLink(); } else { trackEvent('self_hosted_pricing', 'click_enterprise_contact_sales'); - openSelfHostedLink(); } + openSalesLink(); }} > {formatMessage({id: 'pricing_modal.btn.contactSalesForQuote', defaultMessage: 'Contact Sales'})} diff --git a/webapp/channels/src/components/pricing_modal/content.tsx b/webapp/channels/src/components/pricing_modal/content.tsx index 7dc7d8dcfc..e0f55999fe 100644 --- a/webapp/channels/src/components/pricing_modal/content.tsx +++ b/webapp/channels/src/components/pricing_modal/content.tsx @@ -10,8 +10,6 @@ import {CloudLinks, CloudProducts, LicenseSkus, ModalIdentifiers, MattermostFeat import {fallbackStarterLimits, asGBString, hasSomeLimits} from 'utils/limits'; import {findOnlyYearlyProducts, findProductBySku} from 'utils/products'; -import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud'; - import {trackEvent} from 'actions/telemetry_actions'; import {closeModal, openModal} from 'actions/views/modals'; import {subscribeCloudSubscription} from 'actions/cloud'; @@ -38,6 +36,8 @@ import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurch import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal'; import useOpenDowngradeModal from 'components/common/hooks/useOpenDowngradeModal'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; +import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm'; import ExternalLink from 'components/external_link'; import DowngradeTeamRemovalModal from './downgrade_team_removal_modal'; @@ -64,15 +64,12 @@ function Content(props: ContentProps) { const openPricingModalBackAction = useOpenPricingModal(); const isAdmin = useSelector(isCurrentUserSystemAdmin); - const contactSalesLink = useSelector(getCloudContactUsLink)(InquiryType.Sales, SalesInquiryIssue.UpgradeEnterprise); const subscription = useSelector(selectCloudSubscription); const currentProduct = useSelector(selectSubscriptionProduct); const products = useSelector(selectCloudProducts); const yearlyProducts = findOnlyYearlyProducts(products || {}); // pricing modal should now only show yearly products - const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical); - const currentSubscriptionIsMonthly = currentProduct?.recurring_interval === RecurringIntervals.MONTH; const isEnterprise = currentProduct?.sku === CloudProducts.ENTERPRISE; const isEnterpriseTrial = subscription?.is_free_trial === 'true'; @@ -124,6 +121,8 @@ function Content(props: ContentProps) { const freeTierText = (!isStarter && !currentSubscriptionIsMonthly) ? formatMessage({id: 'pricing_modal.btn.contactSupport', defaultMessage: 'Contact Support'}) : formatMessage({id: 'pricing_modal.btn.downgrade', defaultMessage: 'Downgrade'}); const adminProfessionalTierText = currentSubscriptionIsMonthlyProfessional ? formatMessage({id: 'pricing_modal.btn.switch_to_annual', defaultMessage: 'Switch to annual billing'}) : formatMessage({id: 'pricing_modal.btn.upgrade', defaultMessage: 'Upgrade'}); + const [openContactSales] = useOpenSalesLink(); + const [openContactSupport] = useOpenCloudZendeskSupportForm('Workspace downgrade', ''); const openCloudPurchaseModal = useOpenCloudPurchaseModal({}); const openCloudDelinquencyModal = useOpenCloudPurchaseModal({ isDelinquencyModal: true, @@ -239,7 +238,7 @@ function Content(props: ContentProps) { return { action: () => { trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales'); - window.open(contactSalesLink, '_blank'); + openContactSales(); }, text: formatMessage({id: 'pricing_modal.btn.contactSales', defaultMessage: 'Contact Sales'}), customClass: ButtonCustomiserClasses.active, @@ -350,7 +349,7 @@ function Content(props: ContentProps) { buttonDetails={{ action: () => { if (!isStarter && !currentSubscriptionIsMonthly) { - window.open(contactSupportLink, '_blank'); + openContactSupport(); return; } diff --git a/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx b/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx index c573b10c4a..9edb86d824 100644 --- a/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx +++ b/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx @@ -6,7 +6,7 @@ import {Modal} from 'react-bootstrap'; import {useIntl} from 'react-intl'; import {useDispatch, useSelector} from 'react-redux'; -import {CloudLinks, LicenseLinks, ModalIdentifiers, SelfHostedProducts, LicenseSkus, TELEMETRY_CATEGORIES, RecurringIntervals} from 'utils/constants'; +import {CloudLinks, ModalIdentifiers, SelfHostedProducts, LicenseSkus, TELEMETRY_CATEGORIES, RecurringIntervals} from 'utils/constants'; import {findSelfHostedProductBySku} from 'utils/hosted_customer'; import {trackEvent} from 'actions/telemetry_actions'; @@ -27,6 +27,7 @@ import StartTrialBtn from 'components/learn_more_trial_modal/start_trial_btn'; import ExternalLink from 'components/external_link'; import useCanSelfHostedSignup from 'components/common/hooks/useCanSelfHostedSignup'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import { useControlAirGappedSelfHostedPurchaseModal, @@ -89,6 +90,7 @@ function SelfHostedContent(props: ContentProps) { const isEnterprise = license.SkuShortName === LicenseSkus.Enterprise; const isPostSelfHostedEnterpriseTrial = prevSelfHostedTrialLicense.IsLicensed === 'true'; + const [openContactSales] = useOpenSalesLink(); const controlScreeningInProgressModal = useControlScreeningInProgressModal(); const controlAirgappedModal = useControlAirGappedSelfHostedPurchaseModal(); @@ -287,7 +289,7 @@ function SelfHostedContent(props: ContentProps) { buttonDetails={(isPostSelfHostedEnterpriseTrial || !isAdmin) ? { action: () => { trackEvent('self_hosted_pricing', 'click_enterprise_contact_sales'); - window.open(LicenseLinks.CONTACT_SALES, '_blank'); + openContactSales(); }, text: formatMessage({id: 'pricing_modal.btn.contactSales', defaultMessage: 'Contact Sales'}), customClass: ButtonCustomiserClasses.active, diff --git a/webapp/channels/src/components/purchase_modal/index.ts b/webapp/channels/src/components/purchase_modal/index.ts index be38d6167d..0285722adf 100644 --- a/webapp/channels/src/components/purchase_modal/index.ts +++ b/webapp/channels/src/components/purchase_modal/index.ts @@ -19,7 +19,7 @@ import {GlobalState} from 'types/store'; import {BillingDetails} from 'types/cloud/sku'; import {isModalOpen} from 'selectors/views/modals'; -import {getCloudContactUsLink, InquiryType, getCloudDelinquentInvoices, isCloudDelinquencyGreaterThan90Days} from 'selectors/cloud'; +import {getCloudDelinquentInvoices, isCloudDelinquencyGreaterThan90Days} from 'selectors/cloud'; import {isDevModeEnabled} from 'selectors/general'; import {ModalIdentifiers} from 'utils/constants'; @@ -29,6 +29,7 @@ import {completeStripeAddPaymentMethod, subscribeCloudSubscription} from 'action import {ModalData} from 'types/actions'; import withGetCloudSubscription from 'components/common/hocs/cloud/with_get_cloud_subscription'; import {findOnlyYearlyProducts} from 'utils/products'; +import {getCloudContactSalesLink, getCloudSupportLink} from 'utils/contact_support_sales'; const PurchaseModal = makeAsyncComponent('PurchaseModal', React.lazy(() => import('./purchase_modal'))); @@ -39,19 +40,27 @@ function mapStateToProps(state: GlobalState) { const products = state.entities.cloud!.products; const yearlyProducts = findOnlyYearlyProducts(products || {}); + const customer = state.entities.cloud.customer; + const customerEmail = customer?.email || ''; + const firstName = customer?.contact_first_name || ''; + const lastName = customer?.contact_last_name || ''; + const companyName = customer?.name || ''; + const contactSalesLink = getCloudContactSalesLink(firstName, lastName, companyName, customerEmail, 'mattermost', 'in-product-cloud'); + const contactSupportLink = getCloudSupportLink(customerEmail, 'Cloud purchase', '', window.location.host); + return { show: isModalOpen(state, ModalIdentifiers.CLOUD_PURCHASE), products, yearlyProducts, isDevMode: isDevModeEnabled(state), - contactSupportLink: getCloudContactUsLink(state)(InquiryType.Technical), + contactSupportLink, invoices: getCloudDelinquentInvoices(state), isCloudDelinquencyGreaterThan90Days: isCloudDelinquencyGreaterThan90Days(state), isFreeTrial: subscription?.is_free_trial === 'true', isComplianceBlocked: subscription?.compliance_blocked === 'true', - contactSalesLink: getCloudContactUsLink(state)(InquiryType.Sales), + contactSalesLink, productId: subscription?.product_id, - customer: state.entities.cloud.customer, + customer, currentTeam: getCurrentTeam(state), theme: getTheme(state), isDelinquencyModal, diff --git a/webapp/channels/src/components/purchase_modal/purchase_modal.tsx b/webapp/channels/src/components/purchase_modal/purchase_modal.tsx index 36b39a3ab0..3fdfe6f319 100644 --- a/webapp/channels/src/components/purchase_modal/purchase_modal.tsx +++ b/webapp/channels/src/components/purchase_modal/purchase_modal.tsx @@ -18,7 +18,6 @@ import ComplianceScreenFailedSvg from 'components/common/svg_images_components/a import AddressForm from 'components/payment_form/address_form'; import {t} from 'utils/i18n'; -import {Address, CloudCustomer, Product, Invoice, areShippingDetailsValid, Feedback} from '@mattermost/types/cloud'; import {ActionResult} from 'mattermost-redux/types/actions'; import {localizeMessage, getNextBillingDate, getBlankAddressWithCountry} from 'utils/utils'; @@ -34,6 +33,7 @@ import { ModalIdentifiers, RecurringIntervals, } from 'utils/constants'; +import {goToMattermostContactSalesForm} from 'utils/contact_support_sales'; import PaymentDetails from 'components/admin_console/billing/payment_details'; import {STRIPE_CSS_SRC, STRIPE_PUBLIC_KEY} from 'components/payment_form/stripe'; @@ -54,6 +54,8 @@ import {ModalData} from 'types/actions'; import {Theme} from 'mattermost-redux/selectors/entities/preferences'; +import {Address, CloudCustomer, Product, Invoice, areShippingDetailsValid, Feedback} from '@mattermost/types/cloud'; + import {areBillingDetailsValid, BillingDetails} from '../../types/cloud/sku'; import {Team} from '@mattermost/types/teams'; @@ -463,6 +465,7 @@ class PurchaseModal extends React.PureComponent { } confirmSwitchToAnnual = () => { + const {customer} = this.props; this.props.actions.openModal({ modalId: ModalIdentifiers.CONFIRM_SWITCH_TO_YEARLY, dialogType: SwitchToYearlyPlanConfirmModal, @@ -476,7 +479,11 @@ class PurchaseModal extends React.PureComponent { TELEMETRY_CATEGORIES.CLOUD_ADMIN, 'confirm_switch_to_annual_click_contact_sales', ); - window.open(this.props.contactSalesLink, '_blank'); + const customerEmail = customer?.email || ''; + const firstName = customer?.contact_first_name || ''; + const lastName = customer?.contact_last_name || ''; + const companyName = customer?.name || ''; + goToMattermostContactSalesForm(firstName, lastName, companyName, customerEmail, 'mattermost', 'in-product-cloud'); }, }, }); @@ -1013,7 +1020,7 @@ class PurchaseModal extends React.PureComponent { }); }} contactSupportLink={ - this.props.contactSalesLink + this.props.contactSupportLink } currentTeam={this.props.currentTeam} onSuccess={() => { diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/contact_sales_link.tsx b/webapp/channels/src/components/self_hosted_purchase_modal/contact_sales_link.tsx index 350e44cb9f..a34e11df60 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/contact_sales_link.tsx +++ b/webapp/channels/src/components/self_hosted_purchase_modal/contact_sales_link.tsx @@ -4,18 +4,17 @@ import React from 'react'; import {useIntl} from 'react-intl'; -import {useSelector} from 'react-redux'; import {trackEvent} from 'actions/telemetry_actions'; -import {getCloudContactUsLink, InquiryType} from 'selectors/cloud'; import { TELEMETRY_CATEGORIES, } from 'utils/constants'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import ExternalLink from 'components/external_link'; export default function ContactSalesLink() { - const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical); + const [, contactSalesLink] = useOpenSalesLink(); const intl = useIntl(); return ( {intl.formatMessage({id: 'self_hosted_signup.contact_sales', defaultMessage: 'Contact Sales'})} diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/error.tsx b/webapp/channels/src/components/self_hosted_purchase_modal/error.tsx index 10da32432d..3813f9efda 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/error.tsx +++ b/webapp/channels/src/components/self_hosted_purchase_modal/error.tsx @@ -4,13 +4,11 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; -import {useSelector} from 'react-redux'; - -import {getCloudContactUsLink, InquiryType} from 'selectors/cloud'; import PaymentFailedSvg from 'components/common/svg_images_components/payment_failed_svg'; import AccessDeniedHappySvg from 'components/common/svg_images_components/access_denied_happy_svg'; import IconMessage from 'components/purchase_modal/icon_message'; +import {useOpenSelfHostedZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm'; import ExternalLink from 'components/external_link'; interface Props { @@ -20,7 +18,7 @@ interface Props { } export default function ErrorPage(props: Props) { - const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical); + const [, contactSupportLink] = useOpenSelfHostedZendeskSupportForm('Purchase error'); let formattedTitle = ( { dispatch(getPrevTrialLicense()); @@ -54,14 +54,6 @@ function ADLDAPUpsellBanner() { const currentLicenseEndDate = new Date(parseInt(currentLicense?.ExpiresAt, 10)); - const openLink = () => { - if (isCloud) { - openSalesLink(); - } else { - window.open(LicenseLinks.CONTACT_SALES, '_blank'); - } - }; - const confirmBanner = (
@@ -71,7 +63,7 @@ function ADLDAPUpsellBanner() {
@@ -122,7 +114,7 @@ function ADLDAPUpsellBanner() { btn = ( diff --git a/webapp/channels/src/selectors/cloud.ts b/webapp/channels/src/selectors/cloud.ts index 9786b1f5e0..684a4e6c25 100644 --- a/webapp/channels/src/selectors/cloud.ts +++ b/webapp/channels/src/selectors/cloud.ts @@ -4,51 +4,10 @@ import {Invoice, Subscription} from '@mattermost/types/cloud'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {createSelector} from 'reselect'; import {GlobalState} from 'types/store'; -export enum InquiryType { - Technical = 'technical', - Sales = 'sales', - Billing = 'billing', -} - -export enum TechnicalInquiryIssue { - AdminConsole = 'admin_console', - MattermostMessaging = 'mm_messaging', - DataExport = 'data_export', - Other = 'other', -} - -export enum SalesInquiryIssue { - AboutPurchasing = 'about_purchasing', - CancelAccount = 'cancel_account', - PurchaseNonprofit = 'purchase_nonprofit', - TrialQuestions = 'trial_questions', - UpgradeEnterprise = 'upgrade_enterprise', - SomethingElse = 'something_else', -} - -type Issue = SalesInquiryIssue | TechnicalInquiryIssue - -export const getCloudContactUsLink: (state: GlobalState) => (inquiry: InquiryType, inquiryIssue?: Issue) => string = createSelector( - 'getCloudContactUsLink', - getConfig, - getCurrentUser, - (config, user) => { - // cloud/contact-us with query params for name, email and inquiry - const cwsUrl = config.CWSURL; - const fullName = `${user.first_name} ${user.last_name}`; - return (inquiry: InquiryType, inquiryIssue?: Issue) => { - const inquiryIssueQuery = inquiryIssue ? `&inquiry-issue=${inquiryIssue}` : ''; - - return `${cwsUrl}/cloud/contact-us?email=${encodeURIComponent(user.email)}&name=${encodeURIComponent(fullName)}&inquiry=${inquiry}${inquiryIssueQuery}`; - }; - }, -); - export const getExpandSeatsLink: (state: GlobalState) => (licenseId: string) => string = createSelector( 'getExpandSeatsLink', getConfig, diff --git a/webapp/channels/src/utils/contact_support_sales.ts b/webapp/channels/src/utils/contact_support_sales.ts new file mode 100644 index 0000000000..1899d599a6 --- /dev/null +++ b/webapp/channels/src/utils/contact_support_sales.ts @@ -0,0 +1,100 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Buffer} from 'buffer'; + +import {LicenseLinks} from './constants'; + +const baseZendeskFormURL = 'https://support.mattermost.com/hc/en-us/requests/new'; + +export enum ZendeskSupportForm { + SELF_HOSTED_SUPPORT_FORM = '11184911962004', + CLOUD_SUPPORT_FORM = '11184929555092', +} + +export enum ZendeskFormFieldIDs { + CLOUD_WORKSPACE_URL = '5245314479252', + SELF_HOSTED_ENVIRONMENT = '360026980452', + BILLING_SALES_CATEGORY = '360031056451', + EMAIL = 'anonymous_requester_email', + SUBJECT = 'subject', + DESCRIPTION = 'description' +} + +export type PrefillFieldFormFieldIDs = { + id: ZendeskFormFieldIDs; + val: string; +} + +export const buildZendeskSupportForm = (form: ZendeskSupportForm, formFieldIDs: PrefillFieldFormFieldIDs[]): string => { + let formUrl = `${baseZendeskFormURL}?ticket_form_id=${form}`; + + formFieldIDs.forEach((formPrefill) => { + formUrl = formUrl.concat(`&tf_${formPrefill.id}=${formPrefill.val}`); + }); + + if (form === ZendeskSupportForm.SELF_HOSTED_SUPPORT_FORM) { + formUrl = formUrl.concat(`&tf_${ZendeskFormFieldIDs.SELF_HOSTED_ENVIRONMENT}=production`); + } + + return formUrl; +}; + +export const goToSelfHostedSupportForm = (email: string, subject: string) => { + const form = ZendeskSupportForm.SELF_HOSTED_SUPPORT_FORM; + const url = buildZendeskSupportForm(form, [ + {id: ZendeskFormFieldIDs.EMAIL, val: email}, + {id: ZendeskFormFieldIDs.SUBJECT, val: subject}, + ]); + window.open(url, '_blank'); +}; + +export const getSelfHostedSupportLink = (email: string, subject: string) => { + const form = ZendeskSupportForm.SELF_HOSTED_SUPPORT_FORM; + const url = buildZendeskSupportForm(form, [ + {id: ZendeskFormFieldIDs.EMAIL, val: email}, + {id: ZendeskFormFieldIDs.SUBJECT, val: subject}, + ]); + return url; +}; + +export const goToCloudSupportForm = (email: string, subject: string, description: string, workspaceURL: string) => { + const form = ZendeskSupportForm.CLOUD_SUPPORT_FORM; + let url = buildZendeskSupportForm(form, [ + {id: ZendeskFormFieldIDs.EMAIL, val: email}, + {id: ZendeskFormFieldIDs.SUBJECT, val: subject}, + {id: ZendeskFormFieldIDs.DESCRIPTION, val: description}, + ]); + url = url.concat(`&tf_${ZendeskFormFieldIDs.CLOUD_WORKSPACE_URL}=${workspaceURL}`); + window.open(url, '_blank'); +}; + +export const getCloudSupportLink = (email: string, subject: string, description: string, workspaceURL: string) => { + const form = ZendeskSupportForm.CLOUD_SUPPORT_FORM; + let url = buildZendeskSupportForm(form, [ + {id: ZendeskFormFieldIDs.EMAIL, val: email}, + {id: ZendeskFormFieldIDs.SUBJECT, val: subject}, + {id: ZendeskFormFieldIDs.DESCRIPTION, val: description}, + ]); + url = url.concat(`&tf_${ZendeskFormFieldIDs.CLOUD_WORKSPACE_URL}=${workspaceURL}`); + return url; +}; + +const encodeString = (s: string) => { + return Buffer.from(s).toString('base64'); +}; + +export const buildMMURL = (baseURL: string, firstName: string, lastName: string, companyName: string, businessEmail: string, source: string, medium: string) => { + const mmURL = `${baseURL}?qk=${encodeString(firstName)}&qp=${encodeString(lastName)}&qw=${encodeString(companyName)}&qx=${encodeString(businessEmail)}&utm_source=${source}&utm_medium=${medium}`; + return mmURL; +}; + +export const goToMattermostContactSalesForm = (firstName: string, lastName: string, companyName: string, businessEmail: string, source: string, medium: string) => { + const url = buildMMURL(LicenseLinks.CONTACT_SALES, firstName, lastName, companyName, businessEmail, source, medium); + window.open(url, '_blank'); +}; + +export const getCloudContactSalesLink = (firstName: string, lastName: string, companyName: string, businessEmail: string, source: string, medium: string) => { + const url = buildMMURL(LicenseLinks.CONTACT_SALES, firstName, lastName, companyName, businessEmail, source, medium); + return url; +}; From 884e8e280a97d457ec768c8f7ca914af3276a528 Mon Sep 17 00:00:00 2001 From: Pantelis Vratsalis Date: Mon, 27 Mar 2023 12:42:55 +0200 Subject: [PATCH 11/20] Translated using Weblate (Spanish) Currently translated at 93.6% (2384 of 2546 strings) Translation: mattermost-languages-shipped/mattermost-server-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server-monorepo/es/ --- server/i18n/es.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/i18n/es.json b/server/i18n/es.json index 891462edb8..0e1791f125 100644 --- a/server/i18n/es.json +++ b/server/i18n/es.json @@ -9546,5 +9546,9 @@ { "id": "api.admin.syncables_error", "translation": "Error al agregar usuario a grupo-equipos y grupo-canales" + }, + { + "id": "api.command_templates.name", + "translation": "plantillas" } ] From d6dab948473e5d04f8370d6c71d6aff1c1d7747e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Vay=C3=A1?= Date: Mon, 27 Mar 2023 12:42:56 +0200 Subject: [PATCH 12/20] Translated using Weblate (Spanish) Currently translated at 89.4% (5152 of 5758 strings) Translation: mattermost-languages-shipped/mattermost-webapp-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp-monorepo/es/ Translated using Weblate (Spanish) Currently translated at 89.4% (5152 of 5758 strings) Translation: mattermost-languages-shipped/mattermost-webapp-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp-monorepo/es/ --- webapp/channels/src/i18n/es.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/webapp/channels/src/i18n/es.json b/webapp/channels/src/i18n/es.json index 4e349c4a1c..28de93f5e2 100644 --- a/webapp/channels/src/i18n/es.json +++ b/webapp/channels/src/i18n/es.json @@ -255,6 +255,7 @@ "admin.billing.company_info_edit.sameAsBillingAddress": "Igual que la dirección de facturación", "admin.billing.company_info_edit.save": "Guardar información", "admin.billing.company_info_edit.title": "Editar información de la empresa", + "admin.billing.deleteWorkspace.failureModal.buttonText": "Prueba de nuevo", "admin.billing.history.allPaymentsShowHere": "Todos sus pagos mensuales se mostrarán aquí", "admin.billing.history.date": "Fecha", "admin.billing.history.description": "Descripción", @@ -3898,9 +3899,6 @@ "modal.manual_status.title_ooo": "Tu estado actual es \"Fuera de Oficina\"", "more.details": "Más detalles", "more_channels.create": "Crear Canal", - "more_channels.createClick": "Haz clic en 'Crear Nuevo Canal' para crear uno nuevo", - "more_channels.join": "Unirse", - "more_channels.joining": "Uniendo...", "more_channels.next": "Siguiente", "more_channels.noMore": "No hay más canales para unirse", "more_channels.prev": "Anterior", From 1a01dedfeebe712fac9d8b68d6224d103059bcc1 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 27 Mar 2023 12:42:57 +0200 Subject: [PATCH 13/20] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: mattermost-languages-shipped/mattermost-webapp-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp-monorepo/ Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: mattermost-languages-shipped/mattermost-webapp-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp-monorepo/ --- webapp/channels/src/i18n/bg.json | 2 -- webapp/channels/src/i18n/de.json | 3 --- webapp/channels/src/i18n/en_AU.json | 3 --- webapp/channels/src/i18n/fa.json | 3 --- webapp/channels/src/i18n/fr.json | 3 --- webapp/channels/src/i18n/hu.json | 3 --- webapp/channels/src/i18n/it.json | 2 -- webapp/channels/src/i18n/ja.json | 3 --- webapp/channels/src/i18n/ko.json | 2 -- webapp/channels/src/i18n/nl.json | 3 --- webapp/channels/src/i18n/pl.json | 3 --- webapp/channels/src/i18n/pt-BR.json | 2 -- webapp/channels/src/i18n/ro.json | 2 -- webapp/channels/src/i18n/ru.json | 3 --- webapp/channels/src/i18n/sv.json | 3 --- webapp/channels/src/i18n/tr.json | 3 --- webapp/channels/src/i18n/uk.json | 1 - webapp/channels/src/i18n/zh-CN.json | 3 --- webapp/channels/src/i18n/zh-TW.json | 2 -- 19 files changed, 49 deletions(-) diff --git a/webapp/channels/src/i18n/bg.json b/webapp/channels/src/i18n/bg.json index ac169a02b8..c5331d8e9c 100644 --- a/webapp/channels/src/i18n/bg.json +++ b/webapp/channels/src/i18n/bg.json @@ -3484,8 +3484,6 @@ "modal.manual_status.title_offline": "Вашето състояние е зададено на \"Извън линия\"", "modal.manual_status.title_ooo": "Вашето състояние е зададено на \"Извън офиса\"", "more_channels.create": "Създайте канал", - "more_channels.createClick": "Кликнете върху \"Създаване на нов канал\", за да създадете канал", - "more_channels.joining": "Присъединявне ...", "more_channels.next": "Следващ", "more_channels.noMore": "Няма повече канали за присъединяване", "more_channels.prev": "Предишен", diff --git a/webapp/channels/src/i18n/de.json b/webapp/channels/src/i18n/de.json index 91cf9e29ab..a9ab534958 100644 --- a/webapp/channels/src/i18n/de.json +++ b/webapp/channels/src/i18n/de.json @@ -4157,9 +4157,6 @@ "modal.manual_status.title_ooo": "Dein Status ist auf \"Nicht im Büro\" gesetzt", "more.details": "Mehr Details", "more_channels.create": "Kanal erstellen", - "more_channels.createClick": "Klicke auf 'Neuen Kanal erstellen' um einen Neuen zu erzeugen", - "more_channels.join": "Beitreten", - "more_channels.joining": "Betrete...", "more_channels.next": "Weiter", "more_channels.noMore": "Keine weiteren Kanäle, denen beigetreten werden kann", "more_channels.prev": "Zurück", diff --git a/webapp/channels/src/i18n/en_AU.json b/webapp/channels/src/i18n/en_AU.json index a1efa880c5..a8a38f34e8 100644 --- a/webapp/channels/src/i18n/en_AU.json +++ b/webapp/channels/src/i18n/en_AU.json @@ -4153,9 +4153,6 @@ "modal.manual_status.title_ooo": "Your Status is Set to 'Out of Office'", "more.details": "More details", "more_channels.create": "Create Channel", - "more_channels.createClick": "Click 'Create New Channel' to make a new one", - "more_channels.join": "Join", - "more_channels.joining": "Joining...", "more_channels.next": "Next", "more_channels.noMore": "No more channels to join", "more_channels.prev": "Previous", diff --git a/webapp/channels/src/i18n/fa.json b/webapp/channels/src/i18n/fa.json index ade499d8b7..90132c93a1 100644 --- a/webapp/channels/src/i18n/fa.json +++ b/webapp/channels/src/i18n/fa.json @@ -3707,9 +3707,6 @@ "modal.manual_status.title_ooo": "وضعیت شما روی \"خارج از دفتر\" تنظیم شده است", "more.details": "جزئیات بیشتر", "more_channels.create": "ایجاد کانال", - "more_channels.createClick": "برای ایجاد کانال جدید روی \"ایجاد کانال جدید\" کلیک کنید", - "more_channels.join": "پیوستن", - "more_channels.joining": "پیوستن...", "more_channels.next": "بعد", "more_channels.noMore": "کانال دیگری برای پیوستن وجود ندارد", "more_channels.prev": "قبلی", diff --git a/webapp/channels/src/i18n/fr.json b/webapp/channels/src/i18n/fr.json index 64a3dcf932..0b4cc9471e 100644 --- a/webapp/channels/src/i18n/fr.json +++ b/webapp/channels/src/i18n/fr.json @@ -3736,9 +3736,6 @@ "modal.manual_status.title_offline": "Votre statut est défini sur « Hors ligne »", "modal.manual_status.title_ooo": "Votre statut est défini sur « Absent du bureau »", "more_channels.create": "Créer un canal", - "more_channels.createClick": "Veuillez cliquer sur « Créer un nouveau canal » pour en créer un nouveau", - "more_channels.join": "Rejoindre", - "more_channels.joining": "Accès en cours...", "more_channels.next": "Suivant", "more_channels.noMore": "Il n'y a plus d'autre canal que vous pouvez rejoindre", "more_channels.prev": "Précédent", diff --git a/webapp/channels/src/i18n/hu.json b/webapp/channels/src/i18n/hu.json index 5eef371fe0..cf69f79c4c 100644 --- a/webapp/channels/src/i18n/hu.json +++ b/webapp/channels/src/i18n/hu.json @@ -3920,9 +3920,6 @@ "modal.manual_status.title_ooo": "Az Ön állapota \"Irodán kívül\" -re van állítva", "more.details": "További információ", "more_channels.create": "Csatorna létrehozása", - "more_channels.createClick": "Kattintson az \"Új csatorna létrehozása\" gombra egy új létrehozásához", - "more_channels.join": "Csatlakozás", - "more_channels.joining": "Csatlakozás...", "more_channels.next": "Következő", "more_channels.noMore": "Nincs több beszélgetés amelyhez csatlakozni lehetne", "more_channels.prev": "Előző", diff --git a/webapp/channels/src/i18n/it.json b/webapp/channels/src/i18n/it.json index 475ca8ab90..b64ecd9cd4 100644 --- a/webapp/channels/src/i18n/it.json +++ b/webapp/channels/src/i18n/it.json @@ -2997,8 +2997,6 @@ "modal.manual_status.title_offline": "Il tuo stato è \"Non in linea\"", "modal.manual_status.title_ooo": "Il tuo stato è \"Fuori sede\"", "more_channels.create": "Crea canale", - "more_channels.createClick": "Click 'Crea un nuovo canale' per crearne uno nuovo", - "more_channels.joining": "Accoppiamento...", "more_channels.next": "Prossimo", "more_channels.noMore": "Nessun altro canale in cui entrare", "more_channels.prev": "Precedente", diff --git a/webapp/channels/src/i18n/ja.json b/webapp/channels/src/i18n/ja.json index 6a60d919b3..01d621261e 100644 --- a/webapp/channels/src/i18n/ja.json +++ b/webapp/channels/src/i18n/ja.json @@ -4155,9 +4155,6 @@ "modal.manual_status.title_ooo": "ステータスが \"外出中\" になりました", "more.details": "もっと詳しく", "more_channels.create": "チャンネルを作成する", - "more_channels.createClick": "新しいチャンネルを作成するには「チャンネルを作成する」をクリックしてください", - "more_channels.join": "参加", - "more_channels.joining": "参加しています....", "more_channels.next": "次へ", "more_channels.noMore": "参加できるチャンネルがありません", "more_channels.prev": "前へ", diff --git a/webapp/channels/src/i18n/ko.json b/webapp/channels/src/i18n/ko.json index 9f36cadb38..a73022b548 100644 --- a/webapp/channels/src/i18n/ko.json +++ b/webapp/channels/src/i18n/ko.json @@ -2883,8 +2883,6 @@ "modal.manual_status.title_offline": "상태가 \"오프라인\"이 되셨습니다", "modal.manual_status.title_ooo": "상태가 \"오프라인\"이 되셨습니다", "more_channels.create": "채널 만들기", - "more_channels.createClick": "'새로 만들기'를 클릭하여 새로운 채널을 만드세요", - "more_channels.joining": "참가 중...", "more_channels.next": "다음", "more_channels.noMore": "가입할 수 있는 채널이 없습니다", "more_channels.prev": "이전", diff --git a/webapp/channels/src/i18n/nl.json b/webapp/channels/src/i18n/nl.json index d9b49b646b..2b0c2deccf 100644 --- a/webapp/channels/src/i18n/nl.json +++ b/webapp/channels/src/i18n/nl.json @@ -4155,9 +4155,6 @@ "modal.manual_status.title_ooo": "Je status is ingesteld op \"Out of Office\"", "more.details": "Meer details", "more_channels.create": "Kanaal aanmaken", - "more_channels.createClick": "Klik 'Maak nieuw kanaal' om een nieuw kanaal te maken", - "more_channels.join": "Deelnemen", - "more_channels.joining": "Lid worden...", "more_channels.next": "Volgende", "more_channels.noMore": "Geen kanalen beschikbaar waar aan deelgenomen kan worden", "more_channels.prev": "Vorige", diff --git a/webapp/channels/src/i18n/pl.json b/webapp/channels/src/i18n/pl.json index 3ce35e3fed..7786f19479 100644 --- a/webapp/channels/src/i18n/pl.json +++ b/webapp/channels/src/i18n/pl.json @@ -4157,9 +4157,6 @@ "modal.manual_status.title_ooo": "Twój status został ustawiony na \"Poza biurem\"", "more.details": "Więcej informacji", "more_channels.create": "Stwórz kanał", - "more_channels.createClick": "Kliknij przycisk 'Utwórz nowy kanał', aby dodać nowy", - "more_channels.join": "Dołącz do", - "more_channels.joining": "Dołączanie...", "more_channels.next": "Dalej", "more_channels.noMore": "Brak kanałów", "more_channels.prev": "Wstecz", diff --git a/webapp/channels/src/i18n/pt-BR.json b/webapp/channels/src/i18n/pt-BR.json index 43c012f677..7c727fa2d3 100644 --- a/webapp/channels/src/i18n/pt-BR.json +++ b/webapp/channels/src/i18n/pt-BR.json @@ -3216,8 +3216,6 @@ "modal.manual_status.title_offline": "Seu Status está configurado para \"Desconectado\"", "modal.manual_status.title_ooo": "Seu Status está configurado para \"Fora do Escritório\"", "more_channels.create": "Criar Canal", - "more_channels.createClick": "Clique em 'Criar Novo Canal' para fazer um novo", - "more_channels.joining": "Juntando...", "more_channels.next": "Próximo", "more_channels.noMore": "Não há mais canais para participar", "more_channels.prev": "Anterior", diff --git a/webapp/channels/src/i18n/ro.json b/webapp/channels/src/i18n/ro.json index 9d643ffde9..1f71d191a4 100644 --- a/webapp/channels/src/i18n/ro.json +++ b/webapp/channels/src/i18n/ro.json @@ -3306,8 +3306,6 @@ "modal.manual_status.title_offline": "Starea dvs. este setată la \"Offline\"", "modal.manual_status.title_ooo": "Starea dvs. este setată la \"Plecat din birou\"", "more_channels.create": "Creați un nou canal", - "more_channels.createClick": "Dați clic pe \"Creați un nou canal\" pentru a crea unul nou", - "more_channels.joining": "Aderarea...", "more_channels.next": "Următor", "more_channels.noMore": "Nu mai există canale care să se alăture", "more_channels.prev": "Anterior", diff --git a/webapp/channels/src/i18n/ru.json b/webapp/channels/src/i18n/ru.json index f5260c57cf..bfca5442bf 100644 --- a/webapp/channels/src/i18n/ru.json +++ b/webapp/channels/src/i18n/ru.json @@ -4157,9 +4157,6 @@ "modal.manual_status.title_ooo": "Ваш статус установлен на \"Не на работе\"", "more.details": "Подробнее", "more_channels.create": "Создать канал", - "more_channels.createClick": "Нажмите 'Создать канал' для создания нового канала", - "more_channels.join": "Присоединиться", - "more_channels.joining": "Присоединяемся...", "more_channels.next": "Далее", "more_channels.noMore": "Доступных каналов не найдено", "more_channels.prev": "Предыдущая", diff --git a/webapp/channels/src/i18n/sv.json b/webapp/channels/src/i18n/sv.json index f9280f8c21..9ed2615d87 100644 --- a/webapp/channels/src/i18n/sv.json +++ b/webapp/channels/src/i18n/sv.json @@ -4157,9 +4157,6 @@ "modal.manual_status.title_ooo": "Din status är satt till \"Inte på kontoret\"", "more.details": "Mer information", "more_channels.create": "Skapa kanal", - "more_channels.createClick": "Tryck 'Skapa ny kanal' för att skapa en ny", - "more_channels.join": "Gå med", - "more_channels.joining": "Ansluter...", "more_channels.next": "Nästa", "more_channels.noMore": "Det finns inga fler kanaler att gå med i", "more_channels.prev": "Föregående", diff --git a/webapp/channels/src/i18n/tr.json b/webapp/channels/src/i18n/tr.json index 91700905d0..4c4e6c1fed 100644 --- a/webapp/channels/src/i18n/tr.json +++ b/webapp/channels/src/i18n/tr.json @@ -4093,9 +4093,6 @@ "modal.manual_status.title_ooo": "Durumunuz \"Ofis dışında\" olarak değiştirildi", "more.details": "Ayrıntılı bilgi", "more_channels.create": "Kanal ekle", - "more_channels.createClick": "Yeni bir kanal eklemek için 'Yeni kanal ekle' üzerine tıklayın", - "more_channels.join": "Katıl", - "more_channels.joining": "Katılınıyor...", "more_channels.next": "Sonraki", "more_channels.noMore": "Katılabileceğiniz başka bir kanal yok", "more_channels.prev": "Önceki", diff --git a/webapp/channels/src/i18n/uk.json b/webapp/channels/src/i18n/uk.json index be24267d9c..4afba9192e 100644 --- a/webapp/channels/src/i18n/uk.json +++ b/webapp/channels/src/i18n/uk.json @@ -2246,7 +2246,6 @@ "modal.manual_status.title_offline": "Ваш статус встановлено на \"Offline\"", "modal.manual_status.title_ooo": "Ваш статус встановлено на \"За межами офісу\"", "more_channels.create": "Створити канал", - "more_channels.createClick": "Натисніть 'Створити новий канал' для створення нового каналу", "more_channels.next": "Далі", "more_channels.noMore": "Більше немає каналів для входу", "more_channels.prev": "Попередній", diff --git a/webapp/channels/src/i18n/zh-CN.json b/webapp/channels/src/i18n/zh-CN.json index f7dc1ff425..34642cf7a3 100644 --- a/webapp/channels/src/i18n/zh-CN.json +++ b/webapp/channels/src/i18n/zh-CN.json @@ -3581,9 +3581,6 @@ "modal.manual_status.title_offline": "您的状态已设置为 \"离线\"", "modal.manual_status.title_ooo": "您的状态已设置为 \"离开办公室\"", "more_channels.create": "创建频道", - "more_channels.createClick": "点击'创建新频道'创建一个新的频道", - "more_channels.join": "加入", - "more_channels.joining": "加入中...", "more_channels.next": "下一页", "more_channels.noMore": "没有更多可加入的频道", "more_channels.prev": "上一页", diff --git a/webapp/channels/src/i18n/zh-TW.json b/webapp/channels/src/i18n/zh-TW.json index 67076d9959..63c968580a 100644 --- a/webapp/channels/src/i18n/zh-TW.json +++ b/webapp/channels/src/i18n/zh-TW.json @@ -2913,8 +2913,6 @@ "modal.manual_status.title_offline": "狀態已設為\"離線\"", "modal.manual_status.title_ooo": "狀態已設為\"不在辦公室\"", "more_channels.create": "建立頻道", - "more_channels.createClick": "按下'建立頻道'來建立新頻道", - "more_channels.joining": "加入中...", "more_channels.next": "下一頁", "more_channels.noMore": "沒有可參加的頻道", "more_channels.prev": "上一頁", From ecf800edc11183559a509f022c9c68f120bf608d Mon Sep 17 00:00:00 2001 From: Frank Tang Date: Mon, 27 Mar 2023 12:42:57 +0200 Subject: [PATCH 14/20] Translated using Weblate (Chinese (Simplified)) Currently translated at 79.6% (4587 of 5759 strings) Translation: mattermost-languages-shipped/mattermost-webapp-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp-monorepo/zh_Hans/ Translated using Weblate (Chinese (Simplified)) Currently translated at 79.5% (4583 of 5758 strings) Translation: mattermost-languages-shipped/mattermost-webapp-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp-monorepo/zh_Hans/ Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (2546 of 2546 strings) Translation: mattermost-languages-shipped/mattermost-server-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server-monorepo/zh_Hans/ --- server/i18n/zh-CN.json | 348 ++++++++++++++++++++++++++++ webapp/channels/src/i18n/zh-CN.json | 17 +- 2 files changed, 363 insertions(+), 2 deletions(-) diff --git a/server/i18n/zh-CN.json b/server/i18n/zh-CN.json index 143659ad85..3cc21656bd 100644 --- a/server/i18n/zh-CN.json +++ b/server/i18n/zh-CN.json @@ -9858,5 +9858,353 @@ { "id": "api.command_templates.unsupported.app_error", "translation": "您的设备不支持模板命令。" + }, + { + "id": "worktemplate.product_teams.sprint_planning.integration", + "translation": "项目面板可以使迭代计划前所未来的容易。频道可以用来对话和保证问题的被关注。迭代计划面板可以使所以有本周对任务的关注,回顾面板让团队作为一个整体不断改进。" + }, + { + "id": "worktemplate.product_teams.sprint_planning.channel", + "translation": "项目面板可以使迭代计划前所未来的容易。频道可以用来对话和保证问题的被关注。迭代计划面板可以使所以有本周对任务的关注,回顾面板让团队作为一个整体不断改进。" + }, + { + "id": "worktemplate.product_teams.sprint_planning.board", + "translation": "项目面板可以使迭代计划前所未来的容易。频道可以用来对话和保证问题的被关注。迭代计划面板可以使所以有本周对任务的关注,回顾面板让团队作为一个整体不断改进。" + }, + { + "id": "worktemplate.product_teams.product_roadmap.channel", + "translation": "这里描述了为什么需要面板" + }, + { + "id": "worktemplate.product_teams.product_roadmap.board", + "translation": "这里描述了为什么需要面板" + }, + { + "id": "worktemplate.product_teams.goals_and_okrs.integration", + "translation": "清晰的目标对团队的成功至关重要,在此项目里你可以在文档里写下团队的目标和OKR,并在相关的频道里会有消息提醒。" + }, + { + "id": "worktemplate.product_teams.goals_and_okrs.channel", + "translation": "清晰的目标对团队的成功至关重要,在此项目里你可以在文档里写下团队的目标和OKR,并在相关的频道里会有消息提醒。" + }, + { + "id": "worktemplate.product_teams.goals_and_okrs.board", + "translation": "清晰的目标对团队的成功至关重要,在此项目里你可以在文档里写下团队的目标和OKR,并在相关的频道里会有消息提醒。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "通过建立透明的跨越整个研发团队的工作流程确保你的功能开发过程完美流畅。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "在你的频道通过集成Jira和Gtihub机器人提高效率。这些会自动下载安装。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Boards,Playbooks和应用Bot可以很容易地接入功能发布频道并且和你的团队进行相关互动和讨论。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "使用我们的会议日程模板安排像站立会议这样的定期会议,使用我们的项目任务面板在一路上管理任务的进度。" + }, + { + "id": "worktemplate.product_teams.bug_bash.playbook", + "translation": "把事情安排好并且干掉此项目里的所有bug!用包含的Playbook, Board, and Channel推动项目并评估进度。" + }, + { + "id": "worktemplate.product_teams.bug_bash.integration", + "translation": "把事情安排好并且干掉此项目里的所有bug!用包含的Playbook, Board, and Channel推动项目并评估进度。" + }, + { + "id": "worktemplate.product_teams.bug_bash.channel", + "translation": "把事情安排好并且干掉此项目里的所有bug!用包含的Playbook, Board, and Channel推动项目并评估进度。" + }, + { + "id": "worktemplate.product_teams.bug_bash.board", + "translation": "把事情安排好并且干掉此项目里的所有bug!用包含的Playbook, Board, and Channel推动项目并评估进度。" + }, + { + "id": "worktemplate.leadership.goals_and_okrs.integration", + "translation": "清晰的目标对团队的成功至关重要,在此项目里你可以在文档里写下团队的目标和OKR,并在相关的频道里会有消息提醒。" + }, + { + "id": "worktemplate.leadership.goals_and_okrs.channel", + "translation": "清晰的目标对团队的成功至关重要,在此项目里你可以在文档里写下团队的目标和OKR,并在相关的频道里会有消息提醒。" + }, + { + "id": "worktemplate.leadership.goals_and_okrs.board", + "translation": "清晰的目标对团队的成功至关重要,在此项目里你可以在文档里写下团队的目标和OKR,并在相关的频道里会有消息提醒。" + }, + { + "id": "worktemplate.devops.product_release.playbook", + "translation": "不要丢失此项目的任何一个步骤。从Playbook的检验清单分离成任务部署并达到项目面板的里程碑。用频道来保持所有人对事情的理解一致。" + }, + { + "id": "worktemplate.devops.product_release.channel", + "translation": "不要丢失此项目的任何一个步骤。从Playbook的检验清单分离成任务部署并达到项目面板的里程碑。用频道来保持所有人对事情的理解一致。" + }, + { + "id": "worktemplate.devops.product_release.board", + "translation": "不要丢失此项目的任何一个步骤。从Playbook的检验清单分离成任务部署并达到项目面板的里程碑。用频道来保持所有人对事情的理解一致。" + }, + { + "id": "worktemplate.devops.incident_resolution.description.playbook", + "translation": "当到处都是问题的时候,有一个能够确保一切都尽快回归正确的可重复流程是关键。此项目使用Mattermost提供的一切功能保证火被一步步扑灭以及利益相关者被告知。" + }, + { + "id": "worktemplate.devops.incident_resolution.description.channel", + "translation": "当到处都是问题的时候,有一个能够确保一切都尽快回归正确的可重复流程是关键。此项目使用Mattermost提供的一切功能保证火被一步步扑灭以及利益相关者被告知。" + }, + { + "id": "worktemplate.devops.incident_resolution.description.board", + "translation": "当到处都是问题的时候,有一个能够确保一切都尽快回归正确的可重复流程是关键。此项目使用Mattermost提供的一切功能保证火被一步步扑灭以及利益相关者被告知。" + }, + { + "id": "worktemplate.companywide.goals_and_okrs.integration", + "translation": "清晰的目标对团队的成功至关重要,在此项目里你可以在文档里写下团队的目标和OKR,并在相关的频道里会有消息提醒。" + }, + { + "id": "worktemplate.companywide.goals_and_okrs.channel", + "translation": "清晰的目标对团队的成功至关重要,在此项目里你可以在文档里写下团队的目标和OKR,并在相关的频道里会有消息提醒。" + }, + { + "id": "worktemplate.companywide.goals_and_okrs.board", + "translation": "清晰的目标对团队的成功至关重要,在此项目里你可以在文档里写下团队的目标和OKR,并在相关的频道里会有消息提醒。" + }, + { + "id": "worktemplate.companywide.create_project.integration", + "translation": "使用此项目面板设计一个路线图,并在产生的频道里就相应话题进行探讨合作。" + }, + { + "id": "worktemplate.companywide.create_project.channel", + "translation": "使用此项目面板设计一个路线图,并在产生的频道里就相应话题进行探讨合作。" + }, + { + "id": "worktemplate.companywide.create_project.board", + "translation": "使用此项目面板设计一个路线图,并在产生的频道里就相应话题进行探讨合作。" + }, + { + "id": "app.user.run.update_status.title", + "translation": "状态更新" + }, + { + "id": "app.user.run.update_status.submit_label", + "translation": "更新状态" + }, + { + "id": "app.user.run.update_status.reminder_for_next_update", + "translation": "下次更新的提醒" + }, + { + "id": "app.user.run.update_status.num_channel", + "translation": { + "other": "为利益相关者提供一次更新提醒。这条提醒将被广播到{{.Count}} 个频道。" + } + }, + { + "id": "app.user.run.update_status.finish_run.placeholder", + "translation": "并且标记此运行为已结束" + }, + { + "id": "app.user.run.update_status.finish_run", + "translation": "结束运行" + }, + { + "id": "app.user.run.update_status.change_since_last_update", + "translation": "对比上次存在更改" + }, + { + "id": "app.user.run.status_enable", + "translation": "@{{.Username}} 启用了对 [{{.RunName}}]({{.RunURL}})的状态更新" + }, + { + "id": "app.user.run.status_disable", + "translation": "@{{.Username}} 停止用了 [{{.RunName}}]({{.RunURL}})的状态更新。" + }, + { + "id": "app.user.run.request_update", + "translation": "@here — @{{.Name}} 请求对 [{{.RunName}}]({{.RunURL}}) 进行状态更新。 \n" + }, + { + "id": "app.user.run.request_join_channel", + "translation": "@{{.Name}} 是一个运行的参与者,并且希望要参加这个频道。任何的频道成员都可以邀请他们。\n" + }, + { + "id": "app.user.run.confirm_finish.title", + "translation": "确认完成运行" + }, + { + "id": "app.user.run.confirm_finish.submit_label", + "translation": "完成运行" + }, + { + "id": "app.user.run.confirm_finish.num_outstanding", + "translation": { + "other": "一共有 **{{.Count}} 未完成的任务**. 您确定想为所有的参与者结束*{{.RunName}}*吗?" + } + }, + { + "id": "app.user.run.add_to_timeline.title", + "translation": "添加到运行队列" + }, + { + "id": "app.user.run.add_to_timeline.summary.placeholder", + "translation": "时间表里显示的简单概要" + }, + { + "id": "app.user.run.add_to_timeline.summary.help", + "translation": "最大64个字符" + }, + { + "id": "app.user.run.add_to_timeline.summary", + "translation": "概要" + }, + { + "id": "app.user.run.add_to_timeline.submit_label", + "translation": "添加到运行队列" + }, + { + "id": "app.user.run.add_to_timeline.playbook_run", + "translation": "Playbook运行" + }, + { + "id": "app.user.run.add_checklist_item.title", + "translation": "添加新任务" + }, + { + "id": "app.user.run.add_checklist_item.submit_label", + "translation": "添加任务" + }, + { + "id": "app.user.run.add_checklist_item.name", + "translation": "名字" + }, + { + "id": "app.user.run.add_checklist_item.description", + "translation": "描述" + }, + { + "id": "app.user.new_run.title", + "translation": "运行playbook" + }, + { + "id": "app.user.new_run.submit_label", + "translation": "开始运行" + }, + { + "id": "app.user.new_run.run_name", + "translation": "运行名" + }, + { + "id": "app.user.new_run.playbook", + "translation": "Playbook" + }, + { + "id": "app.user.new_run.intro", + "translation": "**所有者** {{.Username}}" + }, + { + "id": "app.user.digest.tasks.zero_assigned", + "translation": "您没有任务。" + }, + { + "id": "app.user.digest.tasks.num_assigned_due_until_today", + "translation": { + "other": "您有 {{.Count}} 个任务现在已经逾期:" + } + }, + { + "id": "app.user.digest.tasks.num_assigned", + "translation": { + "other": "您一共有{{.Count}}个任务:" + } + }, + { + "id": "app.user.digest.tasks.heading", + "translation": "您的任务" + }, + { + "id": "app.user.digest.tasks.due_yesterday", + "translation": "于昨天过期" + }, + { + "id": "app.user.digest.tasks.due_x_days_ago", + "translation": "{{.Count}}天已过期" + }, + { + "id": "app.user.digest.tasks.due_today", + "translation": "将于今天过期" + }, + { + "id": "app.user.digest.tasks.due_in_x_days", + "translation": { + "other": "{{.Count}}天后过期" + } + }, + { + "id": "app.user.digest.tasks.due_after_today", + "translation": { + "other": "您有 **{{.Count}} 项目任务今天之后将要过期**." + } + }, + { + "id": "app.user.digest.tasks.all_tasks_command", + "translation": "请使用`/playbook todo`来查看您所有的任务。" + }, + { + "id": "app.user.digest.runs_in_progress.zero_in_progress", + "translation": "您有0项正在运行。" + }, + { + "id": "app.user.digest.runs_in_progress.num_in_progress", + "translation": { + "other": "您有{{.Count}}正在运行:" + } + }, + { + "id": "app.user.digest.runs_in_progress.heading", + "translation": "正在运行" + }, + { + "id": "app.user.digest.overdue_status_updates.zero_overdue", + "translation": "您没有逾期。" + }, + { + "id": "app.user.digest.overdue_status_updates.num_overdue", + "translation": { + "other": "您有 {{.Count}} 过期需要状态更新:" + } + }, + { + "id": "app.user.digest.overdue_status_updates.heading", + "translation": "过期状态更新" + }, + { + "id": "app.oauth.remove_auth_data_by_client_id.app_error", + "translation": "不能删除oauth认证信息。" + }, + { + "id": "app.command.execute.error", + "translation": "无法执行命令。" + }, + { + "id": "api.templates.license_up_for_renewal_contact_sales", + "translation": "联系销售" + }, + { + "id": "api.license.true_up_review.not_allowed_for_cloud", + "translation": "云实例不允许真实性评估" + }, + { + "id": "api.license.true_up_review.license_required", + "translation": "真实性评估需要许可证" + }, + { + "id": "api.license.true_up_review.get_status_error", + "translation": "无法获取真实的状态记录" + }, + { + "id": "api.license.true_up_review.create_error", + "translation": "无法创建真实的状态记录" } ] diff --git a/webapp/channels/src/i18n/zh-CN.json b/webapp/channels/src/i18n/zh-CN.json index 34642cf7a3..2db89a3255 100644 --- a/webapp/channels/src/i18n/zh-CN.json +++ b/webapp/channels/src/i18n/zh-CN.json @@ -54,6 +54,7 @@ "accessibility.sidebar.types.unread": "未读", "activityAndInsights.sidebarLink": "见解", "activityAndInsights.title": "活动与见解 - {displayName} {siteName}", + "activityAndInsights.tutorialTip.description": "查看新加入工作区的洞见功能。了解最热的内容,了解你和你的队友在怎么样使用你的工作区间。", "activityAndInsights.tutorialTip.title": "介绍:见解", "activityAndInsights.tutorial_tip.notNow": "现在不要", "activityAndInsights.tutorial_tip.viewInsights": "查看见解", @@ -255,6 +256,12 @@ "admin.billing.company_info_edit.save": "保存信息", "admin.billing.company_info_edit.title": "编辑公司信息", "admin.billing.deleteWorkspace.failureModal.buttonText": "重试", + "admin.billing.deleteWorkspace.failureModal.subtitle": "我们在删除你的工作空间时遇到了问题。请再试一次或联系技术支持。", + "admin.billing.deleteWorkspace.failureModal.title": "工作区删除失败", + "admin.billing.deleteWorkspace.progressModal.title": "删除你的工作区", + "admin.billing.deleteWorkspace.resultModal.ContactSupport": "联系客服", + "admin.billing.deleteWorkspace.successModal.subtitle": "您的工作区现在已被删除。谢谢您的惠顾。", + "admin.billing.deleteWorkspace.successModal.title": "您的工作区已被删除", "admin.billing.history.allPaymentsShowHere": "您所有的发票都将显示在这里", "admin.billing.history.date": "日期", "admin.billing.history.description": "描述", @@ -293,6 +300,7 @@ "admin.billing.purchaseModal.savedPaymentDetailsTitle": "您保存的付款详情", "admin.billing.subscription.LearnMore": "了解更多", "admin.billing.subscription.billedFrom": "您将从 {beginDate} 开始计费", + "admin.billing.subscription.byClickingYouAgree": "点击{buttonContent} ,您将同意{legalText}", "admin.billing.subscription.cancelSubscriptionSection.contactUs": "联系我们", "admin.billing.subscription.cancelSubscriptionSection.description": "目前,只能在客户支持代表的帮助下删除工作空间。", "admin.billing.subscription.cancelSubscriptionSection.title": "取消订阅", @@ -303,10 +311,15 @@ "admin.billing.subscription.cloudTrial.subscribeButton": "立刻升级", "admin.billing.subscription.cloudTrialBadge.daysLeftOnTrial": "试用期还剩 {daysLeftOnTrial} 天", "admin.billing.subscription.cloudYearlyBadge": "年度", + "admin.billing.subscription.complianceScreenFailed.button": "继续使用云免费版", + "admin.billing.subscription.complianceScreenFailed.subtitle": "一旦您的云订阅升级被批准,我们将检查相关事宜,并在3天内回复您。在此期间,请继续使用免费版本。", + "admin.billing.subscription.complianceScreenFailed.title": "您的交易正在审核中", + "admin.billing.subscription.complianceScreenShippingSameAsBilling": "我的送货地址和我的账单地址是一样的", "admin.billing.subscription.constCloudCard.contactSupport": "联系支持", "admin.billing.subscription.creditCardExpired": "您的信用卡已过期。请更新您的付款信息,以免造成任何中断。", "admin.billing.subscription.creditCardHasExpired": "您的信用卡已过期", "admin.billing.subscription.creditCardHasExpired.description": "请更新您的 付款信息 以避免任何中断服务。", + "admin.billing.subscription.deleteWorkspaceModal.usageDetails": "{messageCount} 条消息和 {fileSize} 的文件", "admin.billing.subscription.downgradedSuccess": "您现在订阅了 {productName}", "admin.billing.subscription.downgrading": "降级您的工作区", "admin.billing.subscription.featuresAvailable": "{productName} 功能现在已可以使用。", @@ -2963,7 +2976,7 @@ "emoji_picker.header": "表情选择器", "emoji_picker.objects": "物品", "emoji_picker.people-body": "人与身体", - "emoji_picker.recent": "最近使用", + "emoji_picker.recent": "最近使用过", "emoji_picker.search": "搜索表情符", "emoji_picker.searchResults": "搜索结果", "emoji_picker.search_emoji": "搜索表情符", @@ -3383,7 +3396,7 @@ "integrations.successful": "设置成功", "interactive_dialog.cancel": "取消", "interactive_dialog.element.optional": "(可选)", - "interactive_dialog.submit": "提交", + "interactive_dialog.submit": "发送", "interactive_dialog.submitting": "提交中...", "intro_messages.DM": "这是您和{teammate}私信记录的开端。\n此区域外的人不能看到这里共享的私信和文件。", "intro_messages.GM": "这是您和{names}团体消息的起端。\n此区域外的人不能看到这里共享的消息和文件。", From a6f5f0619c858e37eec43bc2b74ed890cc562991 Mon Sep 17 00:00:00 2001 From: kaakaa Date: Mon, 27 Mar 2023 12:42:58 +0200 Subject: [PATCH 15/20] Translated using Weblate (Japanese) Currently translated at 100.0% (5759 of 5759 strings) Translation: mattermost-languages-shipped/mattermost-webapp-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp-monorepo/ja/ Translated using Weblate (Japanese) Currently translated at 100.0% (2546 of 2546 strings) Translation: mattermost-languages-shipped/mattermost-server-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server-monorepo/ja/ --- server/i18n/ja.json | 208 +++++++++++++++++++++++++++++++ webapp/channels/src/i18n/ja.json | 45 ++++++- 2 files changed, 248 insertions(+), 5 deletions(-) diff --git a/server/i18n/ja.json b/server/i18n/ja.json index 2f9c97f888..f42157caa1 100644 --- a/server/i18n/ja.json +++ b/server/i18n/ja.json @@ -9998,5 +9998,213 @@ { "id": "api.command_templates.unsupported.app_error", "translation": "あなたのデバイスではテンプレートコマンドはサポートされていません。" + }, + { + "id": "app.user.run.update_status.title", + "translation": "ステータスの更新" + }, + { + "id": "app.user.run.update_status.submit_label", + "translation": "ステータスを更新" + }, + { + "id": "app.user.run.update_status.reminder_for_next_update", + "translation": "次回更新のリマインド" + }, + { + "id": "app.user.run.update_status.num_channel", + "translation": { + "other": "関係者に更新内容を提供します。この投稿は {{.Count}} チャンネルに配信されます。" + } + }, + { + "id": "app.user.run.update_status.finish_run.placeholder", + "translation": "また、実行を終了としてマークする" + }, + { + "id": "app.user.run.update_status.finish_run", + "translation": "実行を終了する" + }, + { + "id": "app.user.run.update_status.change_since_last_update", + "translation": "前回更新時からの変更点" + }, + { + "id": "app.user.run.status_enable", + "translation": "@{{.Username}} は [{{.RunName}}]({{.RunURL}}) のステータス更新を有効化しました" + }, + { + "id": "app.user.run.status_disable", + "translation": "@{{.Username}} は [{{.RunName}}]({{.RunURL}}) のステータス更新を無効化しました" + }, + { + "id": "app.user.run.request_update", + "translation": "@here — @{{.Name}} は [{{.RunName}}]({{.RunURL}}) のステータスの更新を要求しました。 \n" + }, + { + "id": "app.user.run.request_join_channel", + "translation": "@{{.Name}} は実行の参加者で、このチャンネルへの参加を希望しています。チャンネルのメンバーなら誰でも招待できます。\n" + }, + { + "id": "app.user.run.confirm_finish.title", + "translation": "実行終了の確認" + }, + { + "id": "app.user.run.confirm_finish.submit_label", + "translation": "実行を終了する" + }, + { + "id": "app.user.run.confirm_finish.num_outstanding", + "translation": { + "other": "**{{.Count}} 個の未解決タスク**があります。 本当に実行 *{{.RunName}}* を終了してもよろしいですか?" + } + }, + { + "id": "app.user.run.add_to_timeline.title", + "translation": "実行のタイムラインに追加する" + }, + { + "id": "app.user.run.add_to_timeline.summary.placeholder", + "translation": "タイムラインに表示される短い要約" + }, + { + "id": "app.user.run.add_to_timeline.summary.help", + "translation": "最大 64 文字" + }, + { + "id": "app.user.run.add_to_timeline.summary", + "translation": "概要" + }, + { + "id": "app.user.run.add_to_timeline.submit_label", + "translation": "実行のタイムラインに追加する" + }, + { + "id": "app.user.run.add_to_timeline.playbook_run", + "translation": "Playbookを実行" + }, + { + "id": "app.user.run.add_checklist_item.title", + "translation": "新しいタスクを追加" + }, + { + "id": "app.user.run.add_checklist_item.submit_label", + "translation": "タスクを追加" + }, + { + "id": "app.user.run.add_checklist_item.name", + "translation": "名前" + }, + { + "id": "app.user.run.add_checklist_item.description", + "translation": "説明" + }, + { + "id": "app.user.new_run.title", + "translation": "Playbookを実行する" + }, + { + "id": "app.user.new_run.submit_label", + "translation": "実行開始" + }, + { + "id": "app.user.new_run.run_name", + "translation": "実行名" + }, + { + "id": "app.user.new_run.playbook", + "translation": "Playbook" + }, + { + "id": "app.user.new_run.intro", + "translation": "**オーナー** {{.Username}}" + }, + { + "id": "app.user.digest.tasks.zero_assigned", + "translation": "割り当てられたタスクがありません。" + }, + { + "id": "app.user.digest.tasks.num_assigned_due_until_today", + "translation": { + "other": "対応期限を迎えている{{.Count}}タスクが割り当てられています:" + } + }, + { + "id": "app.user.digest.tasks.num_assigned", + "translation": { + "other": "{{.Count}}タスクが割り当てられています:" + } + }, + { + "id": "app.user.digest.tasks.heading", + "translation": "あなたに割り当てられたタスク" + }, + { + "id": "app.user.digest.tasks.due_yesterday", + "translation": "昨日で期限切れ" + }, + { + "id": "app.user.digest.tasks.due_x_days_ago", + "translation": "{{.Count}}日前に期限切れ" + }, + { + "id": "app.user.digest.tasks.due_today", + "translation": "今日が期限です" + }, + { + "id": "app.user.digest.tasks.due_in_x_days", + "translation": { + "other": "期限は{{.Count}}日後です" + } + }, + { + "id": "app.user.digest.tasks.due_after_today", + "translation": { + "other": "**今日で期限切れとなるタスクが {{.Count}} 件**割り当てられています。" + } + }, + { + "id": "app.user.digest.tasks.all_tasks_command", + "translation": "`/playbook todo`を使用すると、あなたのすべてのタスクを確認することができます。" + }, + { + "id": "app.user.digest.runs_in_progress.zero_in_progress", + "translation": "進行中の実行はありません。" + }, + { + "id": "app.user.digest.runs_in_progress.num_in_progress", + "translation": { + "other": "現在、 {{.Count}} の実行が進行中です:" + } + }, + { + "id": "app.user.digest.runs_in_progress.heading", + "translation": "進行中の実行" + }, + { + "id": "app.user.digest.overdue_status_updates.zero_overdue", + "translation": "期限切れの実行は 0 です。" + }, + { + "id": "app.user.digest.overdue_status_updates.num_overdue", + "translation": { + "other": "ステータス更新の期日が過ぎた実行が {{.Count}} あります:" + } + }, + { + "id": "app.user.digest.overdue_status_updates.heading", + "translation": "期限切れステータスの更新" + }, + { + "id": "app.oauth.remove_auth_data_by_client_id.app_error", + "translation": "oauth データを削除することができませんでした。" + }, + { + "id": "app.command.execute.error", + "translation": "コマンドを実行できませんでした。" + }, + { + "id": "api.templates.license_up_for_renewal_contact_sales", + "translation": "営業に問い合わせる" } ] diff --git a/webapp/channels/src/i18n/ja.json b/webapp/channels/src/i18n/ja.json index 01d621261e..fee7cc2e97 100644 --- a/webapp/channels/src/i18n/ja.json +++ b/webapp/channels/src/i18n/ja.json @@ -387,7 +387,7 @@ "admin.billing.subscription.privateCloudCard.contactSalesy": "営業担当に問い合わせる", "admin.billing.subscription.privateCloudCard.contactSupport": "サポートに連絡する", "admin.billing.subscription.privateCloudCard.freeTrial.description": "私たちは、お客様のニーズにお応えすることを大切にしています。サブスクリプション、請求書作成、トライアルに関するご質問は、営業までお問い合わせください。", - "admin.billing.subscription.privateCloudCard.freeTrial.title": "トライアルに関する質問はこちら", + "admin.billing.subscription.privateCloudCard.freeTrial.title": "トライアルに関する質問がありますか?", "admin.billing.subscription.privateCloudCard.upgradeNow": "今すぐアップグレード", "admin.billing.subscription.proratedPayment.substitle": "{selectedProductName}にアップグレードしていただきありがとうございます。このプランのすべての機能にアクセスするには、数分後にワークスペースをチェックしてください。現在ご利用中の{currentProductName}プランと{selectedProductName}プランの料金については、請求サイクルの残り日数とユーザー数に応じた額を請求させていただきます。", "admin.billing.subscription.proratedPayment.title": "現在、{selectedProductName} を利用しています", @@ -1989,6 +1989,8 @@ "admin.requestButton.requestFailure": "テストが失敗しました: {error}", "admin.requestButton.requestSuccess": "テストが成功しました", "admin.reset_email.cancel": "キャンセル", + "admin.reset_email.currentPassword": "現在のパスワード", + "admin.reset_email.missing_current_password": "現在のパスワードを入力してください。", "admin.reset_email.newEmail": "新しい電子メールアドレス", "admin.reset_email.reset": "リセット", "admin.reset_email.titleReset": "電子メールを更新する", @@ -2125,7 +2127,7 @@ "admin.service.corsExposedHeadersTitle": "CORS Exposedヘッダ:", "admin.service.corsHeadersEx": "X-My-Header", "admin.service.corsTitle": "クロスオリジンリクエストを許可する:", - "admin.service.developerDesc": "有効な場合、JavaScriptのエラーはユーザーインターフェイス上部の紫色のバーに表示されます。本番環境での使用はお勧めできません。 ", + "admin.service.developerDesc": "有効な場合、JavaScriptのエラーはユーザーインターフェイス上部の紫色のバーに表示されます。本番環境での使用はお勧めできません。", "admin.service.developerTitle": "開発者モードを有効にする: ", "admin.service.disableBotOwnerDeactivatedTitle": "オーナーが無効化された際にBotアカウントを無効化する:", "admin.service.disableBotWhenOwnerIsDeactivated": "ユーザーが無効化された際、そのユーザーが管理していたすべてのBotアカウントを無効化します。Botアカウントを再び有効にするには、[統合機能 > Botアカウント]({siteURL}/_redirect/integrations/bots) から設定してください。", @@ -3128,7 +3130,7 @@ "create_post.deactivated": "**無効化されたユーザー** のいるアーカイブされたチャンネルを見ています。新しいメッセージは投稿できません。", "create_post.error_message": "メッセージが長すぎます。文字数: {length}/{limit}", "create_post.fileProcessing": "処理しています...", - "create_post.file_limit_sticky_banner.admin_message": "新たにアップロードすると古いファイルから自動的にアーカイブされます。古いファイルを削除するか、有料プランにアップグレードすることで、再度表示できるようになります。", + "create_post.file_limit_sticky_banner.admin_message": "新たにアップロードすると古いファイルから自動的にアーカイブされます。古いファイルを削除するか、有料プランにアップグレードすることで、再度表示できるようになります", "create_post.file_limit_sticky_banner.messageTitle": "無料プランではファイル容量が {storageGB} に制限されます。", "create_post.file_limit_sticky_banner.non_admin_message": "新たにアップロードすると古いファイルから自動的にアーカイブされます。再度表示するには、管理者に有料プランへアップグレードするよう通知してください。", "create_post.file_limit_sticky_banner.snooze_tooltip": "{snoozeDays}日間スヌーズする", @@ -4154,10 +4156,22 @@ "modal.manual_status.title_offline": "ステータスが \"オフライン\" になりました", "modal.manual_status.title_ooo": "ステータスが \"外出中\" になりました", "more.details": "もっと詳しく", + "more_channels.channel_purpose": "チャンネル情報: メンバーシップ状況: 加入済, メンバー数 {memberCount} , 目的: {channelPurpose}", + "more_channels.count": "{count}件", + "more_channels.count_one": "1件", + "more_channels.count_zero": "0件", "more_channels.create": "チャンネルを作成する", + "more_channels.hide_joined": "参加したことを表示しない", + "more_channels.hide_joined_checked": "チャンネルに参加したことを表示しないチェックボックスがチェック済です", + "more_channels.hide_joined_not_checked": "チャンネルに参加したことを表示しないチェックボックスがチェックされていません", + "more_channels.joined": "参加済", + "more_channels.membership_indicator": "メンバーシップ状況: 参加済", "more_channels.next": "次へ", - "more_channels.noMore": "参加できるチャンネルがありません", + "more_channels.noArchived": "アーカイブされたチャンネルはありません", + "more_channels.noMore": "\"{text}\"の結果はありません", + "more_channels.noPublic": "公開チャンネルはありません", "more_channels.prev": "前へ", + "more_channels.searchError": "違うキーワードで検索してみたり、入力ミスを確認したり、フィルター設定を変更して再度お試しください。", "more_channels.show_archived_channels": "表示: アーカイブチャンネル", "more_channels.show_public_channels": "表示: 公開チャンネル", "more_channels.title": "他のチャンネル", @@ -4367,6 +4381,7 @@ "payment_form.no_billing_address": "請求先住所が追加されませんでした", "payment_form.no_credit_card": "クレジットカードが追加されませんでした", "payment_form.saved_payment_method": "支払い方法を保存する", + "payment_form.shipping_address": "配送先住所", "payment_form.zipcode": "郵便番号", "pending_post_actions.cancel": "キャンセル", "pending_post_actions.retry": "再試行", @@ -4382,6 +4397,14 @@ "plan.self_serve": "セルフサービス", "pluggable.errorOccurred": "プラグイン {pluginId} でエラーが発生しました。", "pluggable.errorRefresh": "更新しますか?", + "pluggable_rhs.tourtip.boards.access": "右側のApp barの Boards アイコンから、リンクされた boards にアクセスできます。", + "pluggable_rhs.tourtip.boards.click": "この右側のパネルから boards をクリックします。", + "pluggable_rhs.tourtip.boards.review": "チャンネルから board の更新を確認します。", + "pluggable_rhs.tourtip.boards.title": "{count} 個のリンクされた {num, plural, one {board} other {boards}} にアクセスしましょう!", + "pluggable_rhs.tourtip.playbooks.access": "右側のApp barの Playbooks アイコンから、リンクされた playbooks にアクセスできます。", + "pluggable_rhs.tourtip.playbooks.click": "この右側のパネルから playbooks をクリックします。", + "pluggable_rhs.tourtip.playbooks.review": "チャンネルから playbook の更新を確認します。", + "pluggable_rhs.tourtip.playbooks.title": "{count} 個のリンクされた {num, plural, one {playbook} other {playbooks}} にアクセスしましょう。", "post.ariaLabel.attachment": ", 1 添付ファイル", "post.ariaLabel.attachmentMultiple": ", {attachmentCount} 添付ファイル", "post.ariaLabel.message": "{time} {date}, {authorName} が, {message} を書きました", @@ -4391,6 +4414,8 @@ "post.ariaLabel.reaction": ", 1 リアクション", "post.ariaLabel.reactionMultiple": ", {reactionCount} リアクション", "post.ariaLabel.replyMessage": "{time} {date}, {authorName} が, {message} と返信しました", + "post.reminder.acknowledgement": "{username} からのこのメッセージについて、{reminderDate}, {reminderTime} にリマインドされます: {permaLink}", + "post.reminder.systemBot": "{username} からのこのメッセージについてのリマインドです: {permaLink}", "post_body.check_for_out_of_channel_groups_mentions.message": "彼らはチャンネルにいないため、このメンションによる通知は行われませんでした。また、彼らはリンクされたグループのメンバーではないため、チャンネルに追加することもできません。彼らをこのチャンネルに追加するには、リンクされたグループに追加しなければなりません。", "post_body.check_for_out_of_channel_mentions.link.and": " と ", "post_body.check_for_out_of_channel_mentions.link.private": "彼らを非公開チャンネルに追加しますか", @@ -4431,6 +4456,13 @@ "post_info.message.visible.compact": " (あなただけが見ることができます)", "post_info.permalink": "リンクをコピーする", "post_info.pin": "チャンネルにピン留め", + "post_info.post_reminder.menu": "リマインダー", + "post_info.post_reminder.sub_menu.custom": "カスタム", + "post_info.post_reminder.sub_menu.header": "リマインダーを設定する:", + "post_info.post_reminder.sub_menu.one_hour": "1時間", + "post_info.post_reminder.sub_menu.thirty_minutes": "30分", + "post_info.post_reminder.sub_menu.tomorrow": "明日", + "post_info.post_reminder.sub_menu.two_hours": "2時間", "post_info.reply": "返信する", "post_info.submenu.icon": "サブメニューアイコン", "post_info.submenu.mobile": "モバイルサブメニュー", @@ -4459,6 +4491,9 @@ "post_priority.requested_ack.description": "メッセージに確認ボタンが表示されます", "post_priority.requested_ack.text": "確認を要求する", "post_priority.you.acknowledge": "(あなた)", + "post_reminder.custom_time_picker_modal.header": "リマインダーを設定する", + "post_reminder.custom_time_picker_modal.submit_button": "リマインダーを設定する", + "post_reminder_custom_time_picker_modal.defaultMsg": "リマインダーを設定する", "postlist.toast.history": "メッセージの履歴を確認しています", "postlist.toast.newMessages": "新しい {count, number} {count, plural, one {メッセージ} other {メッセージ}}", "postlist.toast.newMessagesSince": "{date} {isToday, select, true {} other {以降}} に投稿された新しい {count, number} {count, plural, one {メッセージ} other {メッセージ}}", @@ -5385,7 +5420,7 @@ "user.settings.notifications.email.disabled": "電子メール通知は有効化されていません", "user.settings.notifications.email.disabled_long": "電子メール通知はシステム管理者によって有効化されていません。", "user.settings.notifications.email.everyHour": "1時間毎", - "user.settings.notifications.email.everyXMinutes": "{count}分ごと", + "user.settings.notifications.email.everyXMinutes": "{count, plural, one {分} other {{count, number} 分}}ごと", "user.settings.notifications.email.immediately": "すぐに", "user.settings.notifications.email.never": "通知しない", "user.settings.notifications.email.send": "電子メール通知を送信する", From 5c4f1b5bde8c022668d51741f064b75ed63fabda Mon Sep 17 00:00:00 2001 From: jprusch Date: Mon, 27 Mar 2023 12:42:58 +0200 Subject: [PATCH 16/20] Translated using Weblate (German) Currently translated at 100.0% (5759 of 5759 strings) Translation: mattermost-languages-shipped/mattermost-webapp-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp-monorepo/de/ Translated using Weblate (German) Currently translated at 100.0% (2546 of 2546 strings) Translation: mattermost-languages-shipped/mattermost-server-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server-monorepo/de/ --- server/i18n/de.json | 208 +++++++++++++++++++++++++++++++ webapp/channels/src/i18n/de.json | 19 ++- 2 files changed, 224 insertions(+), 3 deletions(-) diff --git a/server/i18n/de.json b/server/i18n/de.json index b5e9251adc..7d7cefc91d 100644 --- a/server/i18n/de.json +++ b/server/i18n/de.json @@ -10013,5 +10013,213 @@ { "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "Oauth-Daten können nicht entfernt werden." + }, + { + "id": "app.user.run.update_status.title", + "translation": "Aktueller Stand" + }, + { + "id": "app.user.run.update_status.submit_label", + "translation": "Status aktualisieren" + }, + { + "id": "app.user.run.update_status.reminder_for_next_update", + "translation": "Erinnerung an das nächste Update" + }, + { + "id": "app.user.run.update_status.num_channel", + "translation": { + "one": "Bringe die Beteiligten auf den neuesten Stand. Dieser Beitrag wird in einem Kanal veröffentlicht.", + "other": "Bringe die Beteiligten auf den neuesten Stand. Dieser Beitrag wird in {{.Count}} Kanälen veröffentlicht." + } + }, + { + "id": "app.user.run.update_status.finish_run.placeholder", + "translation": "Markiere den Durchlauf auch als beendet" + }, + { + "id": "app.user.run.update_status.finish_run", + "translation": "Durchlauf beenden" + }, + { + "id": "app.user.run.update_status.change_since_last_update", + "translation": "Änderung seit der letzten Aktualisierung" + }, + { + "id": "app.user.run.status_enable", + "translation": "@{{.Username}} hat die Statusaktualisierungen für [{{.RunName}}]({{.RunURL}}) aktiviert" + }, + { + "id": "app.user.run.status_disable", + "translation": "@{{.Username}} hat die Statusaktualisierungen für [{{.RunName}}]({{.RunURL}}) deaktiviert" + }, + { + "id": "app.user.run.request_update", + "translation": "@here — @{{.Name}} hat eine Statusaktualisierung für [{{.RunName}}]({{.RunURL}}) angefordert. \n" + }, + { + "id": "app.user.run.request_join_channel", + "translation": "@{{.Name}} ist ein Teilnehmer und möchte diesem Kanal beitreten. Jedes Mitglied des Kanals kann ihn einladen.\n" + }, + { + "id": "app.user.run.confirm_finish.title", + "translation": "Beenden des Durchlaufs bestätigen" + }, + { + "id": "app.user.run.confirm_finish.submit_label", + "translation": "Durchlauf beenden" + }, + { + "id": "app.user.run.confirm_finish.num_outstanding", + "translation": { + "one": "Es gibt **eine offene Aufgabe**. Bist du sicher, dass du den Durchlauf *{{.RunName}}* für alle Teilnehmer beenden willst?", + "other": "Es gibt **{{.Count}} offene Aufgaben**. Bist du sicher, dass du den Durchlauf *{{.RunName}}* für alle Teilnehmer beenden willst?" + } + }, + { + "id": "app.user.run.add_to_timeline.title", + "translation": "Zur Zeitleiste hinzufügen" + }, + { + "id": "app.user.run.add_to_timeline.summary.placeholder", + "translation": "Kurze Zusammenfassung auf der Zeitachse" + }, + { + "id": "app.user.run.add_to_timeline.summary.help", + "translation": "Max. 64 Zeichen" + }, + { + "id": "app.user.run.add_to_timeline.summary", + "translation": "Zusammenfassung" + }, + { + "id": "app.user.run.add_to_timeline.submit_label", + "translation": "Zur Zeitleiste hinzufügen" + }, + { + "id": "app.user.run.add_to_timeline.playbook_run", + "translation": "Playbook-Durchlauf" + }, + { + "id": "app.user.run.add_checklist_item.title", + "translation": "Neue Aufgabe hinzufügen" + }, + { + "id": "app.user.run.add_checklist_item.submit_label", + "translation": "Aufgabe hinzufügen" + }, + { + "id": "app.user.run.add_checklist_item.name", + "translation": "Name" + }, + { + "id": "app.user.run.add_checklist_item.description", + "translation": "Beschreibung" + }, + { + "id": "app.user.new_run.title", + "translation": "Playbook starten" + }, + { + "id": "app.user.new_run.submit_label", + "translation": "Starte Durchlauf" + }, + { + "id": "app.user.new_run.run_name", + "translation": "Name des Durchlaufs" + }, + { + "id": "app.user.new_run.playbook", + "translation": "Playbook" + }, + { + "id": "app.user.new_run.intro", + "translation": "**Eigentümer** {{.Username}}" + }, + { + "id": "app.user.digest.tasks.zero_assigned", + "translation": "Du hast keine zugewiesene Aufgabe." + }, + { + "id": "app.user.digest.tasks.num_assigned_due_until_today", + "translation": { + "one": "Du hast eine zugewiesene Aufgabe, die jetzt fällig ist:", + "other": "Du hast {{.Count}} zugewiesene Aufgaben, die jetzt fällig sind:" + } + }, + { + "id": "app.user.digest.tasks.num_assigned", + "translation": { + "one": "Du hast eine zugewiesene Aufgabe:", + "other": "Du hast {{.Count}} zugewiesene Aufgaben:" + } + }, + { + "id": "app.user.digest.tasks.heading", + "translation": "Deine zugewiesenen Aufgaben" + }, + { + "id": "app.user.digest.tasks.due_yesterday", + "translation": "Gestern fällig" + }, + { + "id": "app.user.digest.tasks.due_x_days_ago", + "translation": "Fällig vor {{.Count}} Tagen" + }, + { + "id": "app.user.digest.tasks.due_today", + "translation": "Heute fällig" + }, + { + "id": "app.user.digest.tasks.due_in_x_days", + "translation": { + "one": "Fällig in einem Tag", + "other": "Fällig in {{.Count}} Tagen" + } + }, + { + "id": "app.user.digest.tasks.due_after_today", + "translation": { + "one": "Du hast **eine zugewiesene Aufgabe, die nach dem heutige Tag fällig ist**.", + "other": "Du hast **{{.Count}} zugewiesene Aufgaben, die nach dem heutige Tag fällig sind**." + } + }, + { + "id": "app.user.digest.tasks.all_tasks_command", + "translation": "Bitte benutze `/playbook todo` um alle deine Aufgaben anzuzeigen." + }, + { + "id": "app.user.digest.runs_in_progress.zero_in_progress", + "translation": "Du hast keinen aktiven Durchlauf." + }, + { + "id": "app.user.digest.runs_in_progress.num_in_progress", + "translation": { + "one": "Du hast einen aktiven Durchlauf:", + "other": "Du hast {{.Count}} aktive Durchläufe:" + } + }, + { + "id": "app.user.digest.runs_in_progress.heading", + "translation": "Aktive Durchläufe" + }, + { + "id": "app.user.digest.overdue_status_updates.zero_overdue", + "translation": "Du hast keine überfälligen Durchläufe." + }, + { + "id": "app.user.digest.overdue_status_updates.num_overdue", + "translation": { + "one": "Du hast einen überfälligen Durchlauf für ein Statusupdate:", + "other": "Du hast {{.Count}} überfällige Durchläufe für ein Statusupdate:" + } + }, + { + "id": "app.user.digest.overdue_status_updates.heading", + "translation": "Überfällige Statusaktualisierungen" + }, + { + "id": "app.command.execute.error", + "translation": "Kann Befehl nicht ausführen." } ] diff --git a/webapp/channels/src/i18n/de.json b/webapp/channels/src/i18n/de.json index a9ab534958..01990e3010 100644 --- a/webapp/channels/src/i18n/de.json +++ b/webapp/channels/src/i18n/de.json @@ -2127,7 +2127,7 @@ "admin.service.corsExposedHeadersTitle": "CORS-Exposed-Headers:", "admin.service.corsHeadersEx": "X-My-Header", "admin.service.corsTitle": "Erlaube Cross Origin Requests von:", - "admin.service.developerDesc": "Wenn wahr, werden Javascript Fehler in einer roten Zeile im oberen Bereich des Interfaces angezeigt. Nicht empfohlen für Produktionsumgebungen. ", + "admin.service.developerDesc": "Wenn wahr, werden Javascript Fehler in einer roten Zeile im oberen Bereich des Interfaces angezeigt. Nicht empfohlen für Produktionsumgebungen. Das Ändern dieser Einstellung erfordert einen Neustart des Servers, bevor sie wirksam wird.", "admin.service.developerTitle": "Aktiviere Entwickler-Modus: ", "admin.service.disableBotOwnerDeactivatedTitle": "Deaktiviere Bot-Konten, wenn der Besitzer deaktiviert ist:", "admin.service.disableBotWhenOwnerIsDeactivated": "Wenn ein Benutzer deaktiviert ist, werden alle vom Benutzer verwalteten Bot-Konten deaktiviert. Um Bot-Konten wieder zu aktivieren, gehe zu [Integrationen > Bot-Konten]({siteURL}/_redirect/integrations/bots).", @@ -3603,7 +3603,7 @@ "help.attaching.pasting.title": "Kopieren und Einfügen von Dateien", "help.attaching.previewer.description": "Mattermost verfügt über eine integrierte Dateivorschau, die zum Anzeigen von Medien, Herunterladen von Dateien und zum Teilen öffentlicher Links verwendet wird. Wähle die Miniaturansicht einer angehängten Datei, um sie in der Dateivorschau zu öffnen.", "help.attaching.previewer.title": "Dateivorschau", - "help.attaching.publicLinks.description": "Mit öffentlichen Links kannst du Dateianhänge mit Personen außerhalb deines Mattermost-Teams teilen. Öffne die Dateivorschau, indem du die Miniaturansicht eines Anhangs auswählen, und wähle dann **Öffentlichen Link erhalten**. Kopiere den angegebenen Link. Wenn der Link freigegeben und von einem anderen Benutzer geöffnet wird, wird die Datei automatisch heruntergeladen.", + "help.attaching.publicLinks.description": "Mit öffentlichen Links kannst du Dateianhänge mit Personen außerhalb deines Mattermost-Teams teilen. Öffne die Dateivorschau, indem du die Miniaturansicht eines Anhangs auswählen, und wähle dann **Öffentlichen Link erhalten**. Kopiere den angegebenen Link. Wenn der Link geteilt und von einem anderen Benutzer geöffnet wird, erfolgt ein automatischer Download der Datei.", "help.attaching.publicLinks.title": "Links öffentlich teilen", "help.attaching.publicLinks2": "Wenn die Option **Öffentlichen Link abrufen** in der Dateivorschau nicht sichtbar ist und du diese Funktion aktivieren möchtest, bitten deinen Systemadministrator, diese Funktion in der Systemkonsole unter **Site-Konfiguration > Öffentliche Links** zu aktivieren.", "help.attaching.supported.description": "Wenn du versuchst, eine Vorschau eines nicht unterstützten Medientyps anzuzeigen, öffnet die Dateivorschau ein Standardsymbol für Medienanhänge. Die unterstützten Medienformate hängen stark von deinem Browser und Betriebssystem ab. Die folgenden Formate werden von Mattermost in den meisten Browsern unterstützt:", @@ -4156,10 +4156,22 @@ "modal.manual_status.title_offline": "Dein Status wurde auf \"Offline\" gesetzt", "modal.manual_status.title_ooo": "Dein Status ist auf \"Nicht im Büro\" gesetzt", "more.details": "Mehr Details", + "more_channels.channel_purpose": "Kanal-Informationen: Mitgliedschaftsindikator: Beigetreten, Mitglieder {memberCount}, Zweck: {channelPurpose}", + "more_channels.count": "{count} Ergebnisse", + "more_channels.count_one": "1 Ergebnis", + "more_channels.count_zero": "Keine Ergebnisse", "more_channels.create": "Kanal erstellen", + "more_channels.hide_joined": "Verbundene Kanäle ausblenden", + "more_channels.hide_joined_checked": "Kontrollkästchen Verbundene Kanäle ausblenden, aktiviert", + "more_channels.hide_joined_not_checked": "Kontrollkästchen Verbundene Kanäle ausblenden, deaktiviert", + "more_channels.joined": "Verknüpft", + "more_channels.membership_indicator": "Mitgliedschaftsindikator: Beigetreten", "more_channels.next": "Weiter", - "more_channels.noMore": "Keine weiteren Kanäle, denen beigetreten werden kann", + "more_channels.noArchived": "Keine archivierten Kanäle", + "more_channels.noMore": "Keine Ergebnisse für \"{text}\"", + "more_channels.noPublic": "Keine öffentlichen Kanäle", "more_channels.prev": "Zurück", + "more_channels.searchError": "Versuche, nach anderen Stichworten zu suchen, auf Tippfehlern zu prüfen oder die Filter anzupassen.", "more_channels.show_archived_channels": "Anzeigen: Archivierte Kanäle", "more_channels.show_public_channels": "Anzeigen: Öffentliche Kanäle", "more_channels.title": "Weitere Kanäle", @@ -4369,6 +4381,7 @@ "payment_form.no_billing_address": "Keine Rechnungsadresse hinzugefügt", "payment_form.no_credit_card": "Keine Kreditkarte hinzugefügt", "payment_form.saved_payment_method": "Zahlungsmethode speichern", + "payment_form.shipping_address": "Lieferadresse", "payment_form.zipcode": "Postleitzahl/Zip", "pending_post_actions.cancel": "Abbrechen", "pending_post_actions.retry": "Erneut versuchen", From d62244aebb79ea487851510e67fc01066c9a6ea8 Mon Sep 17 00:00:00 2001 From: master7 Date: Mon, 27 Mar 2023 12:42:59 +0200 Subject: [PATCH 17/20] Translated using Weblate (Polish) Currently translated at 100.0% (5759 of 5759 strings) Translation: mattermost-languages-shipped/mattermost-webapp-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/webapp-monorepo/pl/ Translated using Weblate (Polish) Currently translated at 98.3% (2505 of 2546 strings) Translation: mattermost-languages-shipped/mattermost-server-monorepo Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server-monorepo/pl/ --- server/i18n/pl.json | 24 ++++++++++++++++++++++++ webapp/channels/src/i18n/pl.json | 17 +++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/server/i18n/pl.json b/server/i18n/pl.json index 31a6bc7aeb..9049ed5e57 100644 --- a/server/i18n/pl.json +++ b/server/i18n/pl.json @@ -10014,5 +10014,29 @@ { "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "Nie można usunąć danych oauth." + }, + { + "id": "app.user.digest.runs_in_progress.heading", + "translation": "Uruchomienia w Trakcie" + }, + { + "id": "app.user.digest.overdue_status_updates.zero_overdue", + "translation": "Masz 0 zaległych uruchomień." + }, + { + "id": "app.user.digest.overdue_status_updates.num_overdue", + "translation": { + "few": "Masz {{.Count}} zaległości w aktualizacji statusu:", + "many": "Masz {{.Count}} zaległości w aktualizacji statusu:", + "one": "Masz {{.Count}} zaległość w aktualizacji statusu:" + } + }, + { + "id": "app.user.digest.overdue_status_updates.heading", + "translation": "Zaległe aktualizacje statusu" + }, + { + "id": "app.command.execute.error", + "translation": "Nie można wykonać polecenia." } ] diff --git a/webapp/channels/src/i18n/pl.json b/webapp/channels/src/i18n/pl.json index 7786f19479..96eda0f3b4 100644 --- a/webapp/channels/src/i18n/pl.json +++ b/webapp/channels/src/i18n/pl.json @@ -2127,7 +2127,7 @@ "admin.service.corsExposedHeadersTitle": "Eksponowane nagłówki CORS:", "admin.service.corsHeadersEx": "X-Mój-Header", "admin.service.corsTitle": "Pozwól na zapytania Cross-domain z:", - "admin.service.developerDesc": "Gdy włączone, błędy JavaScript wyświetlane są na czerwonym pasku u góry interfejsu użytkownika. Nie zalecane w wersji produkcyjnej. ", + "admin.service.developerDesc": "Gdy włączone, błędy JavaScript wyświetlane są na czerwonym pasku u góry interfejsu użytkownika. Nie zalecane w wersji produkcyjnej. Zmiana tego wymaga restartu serwera zanim zacznie działać.", "admin.service.developerTitle": "Włączyć Tryb Dewelopera: ", "admin.service.disableBotOwnerDeactivatedTitle": "Wyłącz konta botów jeśli właściciel jest dezaktywowany:", "admin.service.disableBotWhenOwnerIsDeactivated": "Kiedy użytkownik jest dezaktywowany, wyłącza wszystkie konta bot zarządzane przez użytkownika. Aby ponownie włączyć konta botów, przejdź do [Integracje > Konta Botów]({siteURL}/_redirect/integrations/bots).", @@ -4156,10 +4156,22 @@ "modal.manual_status.title_offline": "Twój status został ustawiony na \"Offline\"", "modal.manual_status.title_ooo": "Twój status został ustawiony na \"Poza biurem\"", "more.details": "Więcej informacji", + "more_channels.channel_purpose": "Informacje o kanale: Wskaźnik członkostwa: Dołączyło, liczba członków {memberCount}, Propozycje: {channelPurpose}", + "more_channels.count": "{count} Wyników", + "more_channels.count_one": "1 Wynik", + "more_channels.count_zero": "0 Wyników", "more_channels.create": "Stwórz kanał", + "more_channels.hide_joined": "Ukryj dołączonych", + "more_channels.hide_joined_checked": "Pole wyboru Ukryj połączone kanały, zaznaczone", + "more_channels.hide_joined_not_checked": "Pole wyboru Ukryj połączone kanały, nie zaznaczone", + "more_channels.joined": "Dołączył", + "more_channels.membership_indicator": "Wskaźnik członkostwa: Dołączył", "more_channels.next": "Dalej", - "more_channels.noMore": "Brak kanałów", + "more_channels.noArchived": "Brak zarchiwizowanych kanałów", + "more_channels.noMore": "Brak wyników dla \"{text}\"", + "more_channels.noPublic": "Brak kanałów publicznych", "more_channels.prev": "Wstecz", + "more_channels.searchError": "Spróbuj wyszukać inne słowa kluczowe, sprawdzić literówki lub dostosować filtry.", "more_channels.show_archived_channels": "Pokaż: Archiwizowane kanały", "more_channels.show_public_channels": "Pokaż: Publiczne kanały", "more_channels.title": "Więcej Kanałów", @@ -4369,6 +4381,7 @@ "payment_form.no_billing_address": "Nie dodano adresu rozliczeniowego", "payment_form.no_credit_card": "Nie dodano karty kredytowej", "payment_form.saved_payment_method": "Zapisana Metoda Płatności", + "payment_form.shipping_address": "Adres do wysyłki", "payment_form.zipcode": "Kod Pocztowy", "pending_post_actions.cancel": "Anuluj", "pending_post_actions.retry": "Ponów", From 80c14319838bc6d8a83c3e44d9292a6982d3be38 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Mon, 27 Mar 2023 13:19:29 -0300 Subject: [PATCH 18/20] Revert testing workarounds (#22630) * Revert "fix store issue take two" This reverts commit 59f943c2c7ff7d88f7b36cc29e242042746e959e. * Revert "fix store override issue" This reverts commit 29c346757aa627c07d357c54991f9188c927dd1a. * Revert "Fix TestPushNotificationRace" This reverts commit 6d62dddf8679e82b02e8ad9fe7513217eef5b4ee. * Revert "fix default DSN for CI" This reverts commit e0e69cdbb0645bb50434f6b5bbd12bead1e7ce1d. * Revert "disable playbooks from more unit tests" This reverts commit a1e97a9e96bdd16537f5b6dbdc8335762617a9e0. * Revert "disable playbooks for more tests" This reverts commit 4d2dc74f05339f0b3cd28b997a2e35ae20f898be. * Revert "disable playbooks for TestSAMLSettings" This reverts commit 35c1a4312d0c6a0a64991520fa5b0892c083e6a1. * Revert "disable playbooks for more unit tests" This reverts commit c049631a1474cddf168b2be8feb24140e0dcfd48. * Revert "disable playbooks for mocked enterprise tests" This reverts commit 829317fddbd0e84866534a5a75e52dcffd2dbde7. * Partially revert "disable playbooks for channel/apps mocked tests" This reverts commit 52b4a0a6cf135d26f53298294ed23734aafae0d2. * Revert "fix TestUnitUpdateConfig" This reverts commit 8f134f2a8ae9765aa2b6f66d6827e10ef1f5109f. * Revert "add plugin mock to TestUnitUpdateConfig" This reverts commit 3ec5419092135f494fd04701b5cbbd15920e667b. * Revert "disable Boards for more test helpers" This reverts commit 5d4d0d02d9cf6f872f0304098c68a01a3aab0fbe. * Revert "disable boards at correct place in test helpers" This reverts commit 0c9e175f79293c8be4289c7424930388f207dc75. * Partially revert "disable boards for slash cmd tests" This reverts commit fad8d9de93f5ce351d2e50fd6448662e75f597ae. * Revert "disable Boards for channels web tests" This reverts commit 15540fdfc09cf927071af718d4ad0b2c58308328. * Revert "Adds a teardown function to playbook server tests to disable and reenable boards" This reverts commit 9a46e3d0f43f66d548994986b8c4029d58ad022f. * Revert "Test disable boards through feature flag" This reverts commit 787044add8ba8e2680a2c3c6ba11e709cebc8705. * TestUnitUpdateConfig: restore callback check * Revert "Revert "fix default DSN for CI"" This reverts commit 01b879d55ad1249265f23c6fd9ceb5d7730ddb3d. --- server/channels/api4/apitestlib.go | 44 ++++-------------- server/channels/app/app_test.go | 21 ++++++++- server/channels/app/helper_test.go | 46 ++++++------------- server/channels/app/notification_push_test.go | 10 ++-- server/channels/app/product.go | 6 +-- .../channels/app/slashcommands/helper_test.go | 40 +++------------- server/channels/web/web_test.go | 39 +++------------- server/playbooks/server/api_actions_test.go | 9 ++-- server/playbooks/server/api_bot_test.go | 3 +- server/playbooks/server/api_general_test.go | 3 +- .../server/api_graphql_playbooks_test.go | 12 ++--- .../playbooks/server/api_graphql_runs_test.go | 27 ++++------- server/playbooks/server/api_playbooks_test.go | 45 ++++++------------ server/playbooks/server/api_runs_test.go | 39 ++++++---------- server/playbooks/server/api_settings_test.go | 3 +- server/playbooks/server/api_stats_test.go | 6 +-- server/playbooks/server/api_telemetry_test.go | 3 +- server/playbooks/server/main_test.go | 16 ++----- 18 files changed, 113 insertions(+), 259 deletions(-) diff --git a/server/channels/api4/apitestlib.go b/server/channels/api4/apitestlib.go index 92ef4a9221..921ceb0a2c 100644 --- a/server/channels/api4/apitestlib.go +++ b/server/channels/api4/apitestlib.go @@ -71,10 +71,8 @@ type TestHelper struct { IncludeCacheLayer bool - LogBuffer *mlog.Buffer - TestLogger *mlog.Logger - boardsProductEnvValue string - playbooksDisableEnvValue string + LogBuffer *mlog.Buffer + TestLogger *mlog.Logger } var mainHelper *testlib.MainHelper @@ -104,17 +102,6 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent *memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false *memoryConfig.AnnouncementSettings.UserNoticesEnabled = false *memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false - - // disable Boards through the feature flag - boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct") - os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct") - memoryConfig.FeatureFlags.BoardsProduct = false - - // disable Playbooks (temporarily) as it causes many more mocked methods to get - // called, and cannot receieve a mocked database. - playbooksDisableEnvValue := os.Getenv("MM_DISABLE_PLAYBOOKS") - os.Setenv("MM_DISABLE_PLAYBOOKS", "true") - if updateConfig != nil { updateConfig(memoryConfig) } @@ -153,15 +140,13 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent } th := &TestHelper{ - App: app.New(app.ServerConnector(s.Channels())), - Server: s, - ConfigStore: configStore, - IncludeCacheLayer: includeCache, - Context: request.EmptyContext(testLogger), - TestLogger: testLogger, - LogBuffer: buffer, - boardsProductEnvValue: boardsProductEnvValue, - playbooksDisableEnvValue: playbooksDisableEnvValue, + App: app.New(app.ServerConnector(s.Channels())), + Server: s, + ConfigStore: configStore, + IncludeCacheLayer: includeCache, + Context: request.EmptyContext(testLogger), + TestLogger: testLogger, + LogBuffer: buffer, } th.Context.SetLogger(testLogger) @@ -386,17 +371,6 @@ func (th *TestHelper) ShutdownApp() { } func (th *TestHelper) TearDown() { - // reset board and playbooks product setting to original - if th.boardsProductEnvValue != "" { - os.Setenv("MM_FEATUREFLAGS_BoardsProduct", th.boardsProductEnvValue) - } - - if th.playbooksDisableEnvValue != "" { - os.Setenv("MM_DISABLE_PLAYBOOKS", th.playbooksDisableEnvValue) - } else { - os.Unsetenv("MM_DISABLE_PLAYBOOKS") - } - if th.IncludeCacheLayer { // Clean all the caches th.App.Srv().InvalidateAllCaches() diff --git a/server/channels/app/app_test.go b/server/channels/app/app_test.go index 70a29f3985..ff725fecdf 100644 --- a/server/channels/app/app_test.go +++ b/server/channels/app/app_test.go @@ -11,8 +11,10 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks" ) /* Temporarily comment out until MM-11108 @@ -37,9 +39,26 @@ func init() { } func TestUnitUpdateConfig(t *testing.T) { - th := Setup(t) + th := SetupWithStoreMock(t) defer th.TearDown() + mockStore := th.App.Srv().Store().(*mocks.Store) + mockUserStore := mocks.UserStore{} + mockUserStore.On("Count", mock.Anything).Return(int64(10), nil) + mockPostStore := mocks.PostStore{} + mockPostStore.On("GetMaxPostSize").Return(65535, nil) + mockSystemStore := mocks.SystemStore{} + mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil) + mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil) + mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil) + mockLicenseStore := mocks.LicenseStore{} + mockLicenseStore.On("Get", "").Return(&model.LicenseRecord{}, nil) + mockStore.On("User").Return(&mockUserStore) + mockStore.On("Post").Return(&mockPostStore) + mockStore.On("System").Return(&mockSystemStore) + mockStore.On("License").Return(&mockLicenseStore) + mockStore.On("GetDBSchemaVersion").Return(1, nil) + prev := *th.App.Config().ServiceSettings.SiteURL var called int32 diff --git a/server/channels/app/helper_test.go b/server/channels/app/helper_test.go index 4b5cf1aa0b..a1b8340f66 100644 --- a/server/channels/app/helper_test.go +++ b/server/channels/app/helper_test.go @@ -42,9 +42,7 @@ type TestHelper struct { TestLogger *mlog.Logger IncludeCacheLayer bool - tempWorkspace string - boardsProductEnvValue string - playbooksDisableEnvValue string + tempWorkspace string } func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, options []Option, tb testing.TB) *TestHelper { @@ -62,17 +60,6 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo *memoryConfig.LogSettings.EnableSentry = false // disable error reporting during tests *memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false *memoryConfig.AnnouncementSettings.UserNoticesEnabled = false - - // disable Boards through the feature flag - boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct") - os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct") - memoryConfig.FeatureFlags.BoardsProduct = false - - // disable Playbooks (temporarily) as it causes many more mocked methods to get - // called, and cannot receieve a mocked database. - playbooksDisableEnvValue := os.Getenv("MM_DISABLE_PLAYBOOKS") - os.Setenv("MM_DISABLE_PLAYBOOKS", "true") - configStore.Set(memoryConfig) buffer := &mlog.Buffer{} @@ -103,14 +90,12 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo } th := &TestHelper{ - App: New(ServerConnector(s.Channels())), - Context: request.EmptyContext(testLogger), - Server: s, - LogBuffer: buffer, - TestLogger: testLogger, - IncludeCacheLayer: includeCacheLayer, - boardsProductEnvValue: boardsProductEnvValue, - playbooksDisableEnvValue: playbooksDisableEnvValue, + App: New(ServerConnector(s.Channels())), + Context: request.EmptyContext(testLogger), + Server: s, + LogBuffer: buffer, + TestLogger: testLogger, + IncludeCacheLayer: includeCacheLayer, } th.Context.SetLogger(testLogger) @@ -184,10 +169,16 @@ func SetupWithStoreMock(tb testing.TB) *TestHelper { statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil) statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil) statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil) + + pluginMock := mocks.PluginStore{} + pluginMock.On("Get", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&model.PluginKeyValue{}, nil) + emptyMockStore := mocks.Store{} emptyMockStore.On("Close").Return(nil) emptyMockStore.On("Status").Return(&statusMock) + emptyMockStore.On("Plugin").Return(&pluginMock).Maybe() th.App.Srv().SetStore(&emptyMockStore) + return th } @@ -553,17 +544,6 @@ func (th *TestHelper) ShutdownApp() { } func (th *TestHelper) TearDown() { - // reset board and playbooks product setting to original - if th.boardsProductEnvValue != "" { - os.Setenv("MM_FEATUREFLAGS_BoardsProduct", th.boardsProductEnvValue) - } - - if th.playbooksDisableEnvValue != "" { - os.Setenv("MM_DISABLE_PLAYBOOKS", th.playbooksDisableEnvValue) - } else { - os.Unsetenv("MM_DISABLE_PLAYBOOKS") - } - if th.IncludeCacheLayer { // Clean all the caches th.App.Srv().InvalidateAllCaches() diff --git a/server/channels/app/notification_push_test.go b/server/channels/app/notification_push_test.go index ed22c833eb..8c42bdbbca 100644 --- a/server/channels/app/notification_push_test.go +++ b/server/channels/app/notification_push_test.go @@ -1445,13 +1445,9 @@ func TestPushNotificationRace(t *testing.T) { Router: mux.NewRouter(), } var err error - s.platform, err = platform.New( - platform.ServiceConfig{ - ConfigStore: memoryStore, - }, - platform.SetFileStore(&fmocks.FileBackend{}), - platform.StoreOverride(th.GetSqlStore()), - ) + s.platform, err = platform.New(platform.ServiceConfig{ + ConfigStore: memoryStore, + }, platform.SetFileStore(&fmocks.FileBackend{})) s.SetStore(mockStore) require.NoError(t, err) serviceMap := map[product.ServiceKey]any{ diff --git a/server/channels/app/product.go b/server/channels/app/product.go index af515fe290..37d4af8c5b 100644 --- a/server/channels/app/product.go +++ b/server/channels/app/product.go @@ -73,17 +73,15 @@ func (s *Server) initializeProducts( func (s *Server) shouldStart(product string) bool { if product == "boards" { if !s.Config().FeatureFlags.BoardsProduct { - s.Log().Info("Skipping Boards init; disabled via feature flag") + s.Log().Warn("Skipping boards start: not enabled via feature flag") return false } - s.Log().Info("Allowing Boards init; enabled via feature flag") } if product == "playbooks" { if os.Getenv("MM_DISABLE_PLAYBOOKS") == "true" { - s.Log().Info("Skipping Playbooks init; disabled via env var") + s.Log().Warn("Skipping playbooks start: disabled via env var") return false } - s.Log().Info("Allowing Playbooks init; enabled via env var") } return true diff --git a/server/channels/app/slashcommands/helper_test.go b/server/channels/app/slashcommands/helper_test.go index cc0960cee6..af574c5cce 100644 --- a/server/channels/app/slashcommands/helper_test.go +++ b/server/channels/app/slashcommands/helper_test.go @@ -36,9 +36,7 @@ type TestHelper struct { TestLogger *mlog.Logger IncludeCacheLayer bool - tempWorkspace string - boardsProductEnvValue string - playbooksDisableEnvValue string + tempWorkspace string } func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, tb testing.TB, configSet func(*model.Config)) *TestHelper { @@ -53,17 +51,6 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo if configSet != nil { configSet(memoryConfig) } - - // disable Boards through the feature flag - boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct") - os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct") - memoryConfig.FeatureFlags.BoardsProduct = false - - // disable Playbooks (temporarily) as it causes many more mocked methods to get - // called, and cannot receieve a mocked database. - playbooksDisableEnvValue := os.Getenv("MM_DISABLE_PLAYBOOKS") - os.Setenv("MM_DISABLE_PLAYBOOKS", "true") - *memoryConfig.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins") *memoryConfig.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp") *memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false @@ -95,14 +82,12 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo } th := &TestHelper{ - App: app.New(app.ServerConnector(s.Channels())), - Context: request.EmptyContext(testLogger), - Server: s, - LogBuffer: buffer, - TestLogger: testLogger, - IncludeCacheLayer: includeCacheLayer, - boardsProductEnvValue: boardsProductEnvValue, - playbooksDisableEnvValue: playbooksDisableEnvValue, + App: app.New(app.ServerConnector(s.Channels())), + Context: request.EmptyContext(testLogger), + Server: s, + LogBuffer: buffer, + TestLogger: testLogger, + IncludeCacheLayer: includeCacheLayer, } th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 }) @@ -389,17 +374,6 @@ func (th *TestHelper) shutdownApp() { } func (th *TestHelper) tearDown() { - // reset board and playbooks product setting to original - if th.boardsProductEnvValue != "" { - os.Setenv("MM_FEATUREFLAGS_BoardsProduct", th.boardsProductEnvValue) - } - - if th.playbooksDisableEnvValue != "" { - os.Setenv("MM_DISABLE_PLAYBOOKS", th.playbooksDisableEnvValue) - } else { - os.Unsetenv("MM_DISABLE_PLAYBOOKS") - } - if th.IncludeCacheLayer { // Clean all the caches th.App.Srv().InvalidateAllCaches() diff --git a/server/channels/web/web_test.go b/server/channels/web/web_test.go index 704c2dd0c7..a4458d39f1 100644 --- a/server/channels/web/web_test.go +++ b/server/channels/web/web_test.go @@ -48,9 +48,6 @@ type TestHelper struct { IncludeCacheLayer bool TestLogger *mlog.Logger - - boardsProductEnvValue string - playbooksDisableEnvValue string } func SetupWithStoreMock(tb testing.TB) *TestHelper { @@ -80,17 +77,6 @@ func setupTestHelper(tb testing.TB, includeCacheLayer bool) *TestHelper { *newConfig.AnnouncementSettings.AdminNoticesEnabled = false *newConfig.AnnouncementSettings.UserNoticesEnabled = false *newConfig.PluginSettings.AutomaticPrepackagedPlugins = false - - // disable Boards through the feature flag - boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct") - os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct") - newConfig.FeatureFlags.BoardsProduct = false - - // disable Playbooks (temporarily) as it causes many more mocked methods to get - // called, and cannot receieve a mocked database. - playbooksDisableEnvValue := os.Getenv("MM_DISABLE_PLAYBOOKS") - os.Setenv("MM_DISABLE_PLAYBOOKS", "true") - memoryStore.Set(newConfig) var options []app.Option options = append(options, app.ConfigStore(memoryStore)) @@ -148,14 +134,12 @@ func setupTestHelper(tb testing.TB, includeCacheLayer bool) *TestHelper { }) th := &TestHelper{ - App: a, - Context: request.EmptyContext(testLogger), - Server: s, - Web: web, - IncludeCacheLayer: includeCacheLayer, - TestLogger: testLogger, - boardsProductEnvValue: boardsProductEnvValue, - playbooksDisableEnvValue: playbooksDisableEnvValue, + App: a, + Context: request.EmptyContext(testLogger), + Server: s, + Web: web, + IncludeCacheLayer: includeCacheLayer, + TestLogger: testLogger, } th.Context.SetLogger(testLogger) @@ -194,17 +178,6 @@ func (th *TestHelper) InitBasic() *TestHelper { } func (th *TestHelper) TearDown() { - // reset board and playbooks product setting to original - if th.boardsProductEnvValue != "" { - os.Setenv("MM_FEATUREFLAGS_BoardsProduct", th.boardsProductEnvValue) - } - - if th.playbooksDisableEnvValue != "" { - os.Setenv("MM_DISABLE_PLAYBOOKS", th.playbooksDisableEnvValue) - } else { - os.Unsetenv("MM_DISABLE_PLAYBOOKS") - } - if th.IncludeCacheLayer { // Clean all the caches th.App.Srv().InvalidateAllCaches() diff --git a/server/playbooks/server/api_actions_test.go b/server/playbooks/server/api_actions_test.go index 10672f9308..26ba51d31b 100644 --- a/server/playbooks/server/api_actions_test.go +++ b/server/playbooks/server/api_actions_test.go @@ -15,8 +15,7 @@ import ( ) func TestActionCreation(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() createNewChannel := func(t *testing.T, name string) *model.Channel { @@ -201,8 +200,7 @@ func TestActionCreation(t *testing.T) { } func TestActionList(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() // Create three valid actions @@ -294,8 +292,7 @@ func TestActionList(t *testing.T) { } func TestActionUpdate(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() // Create a valid action diff --git a/server/playbooks/server/api_bot_test.go b/server/playbooks/server/api_bot_test.go index e5ff029bfa..4ab0bd0196 100644 --- a/server/playbooks/server/api_bot_test.go +++ b/server/playbooks/server/api_bot_test.go @@ -16,8 +16,7 @@ func TestTrialLicences(t *testing.T) { // This test is flaky due to upstream connectivity issues. t.Skip() - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("request trial license without permissions", func(t *testing.T) { diff --git a/server/playbooks/server/api_general_test.go b/server/playbooks/server/api_general_test.go index 380959325e..b3052eb649 100644 --- a/server/playbooks/server/api_general_test.go +++ b/server/playbooks/server/api_general_test.go @@ -11,8 +11,7 @@ import ( ) func TestAPI(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateClients() t.Run("404", func(t *testing.T) { diff --git a/server/playbooks/server/api_graphql_playbooks_test.go b/server/playbooks/server/api_graphql_playbooks_test.go index 8212c3dd8b..bcb78c08a3 100644 --- a/server/playbooks/server/api_graphql_playbooks_test.go +++ b/server/playbooks/server/api_graphql_playbooks_test.go @@ -21,8 +21,7 @@ import ( ) func TestGraphQLPlaybooks(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("basic get", func(t *testing.T) { @@ -206,8 +205,7 @@ func TestGraphQLPlaybooks(t *testing.T) { } func TestGraphQLUpdatePlaybookFails(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("update playbook fails because size constraints.", func(t *testing.T) { @@ -370,8 +368,7 @@ func TestGraphQLUpdatePlaybookFails(t *testing.T) { } func TestUpdatePlaybookFavorite(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("favorite", func(t *testing.T) { @@ -493,8 +490,7 @@ func gqlTestPlaybookUpdate(e *TestEnvironment, t *testing.T, playbookID string, } func TestGraphQLPlaybooksMetrics(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("metrics get", func(t *testing.T) { diff --git a/server/playbooks/server/api_graphql_runs_test.go b/server/playbooks/server/api_graphql_runs_test.go index 92897393b9..7a97b3be53 100644 --- a/server/playbooks/server/api_graphql_runs_test.go +++ b/server/playbooks/server/api_graphql_runs_test.go @@ -20,8 +20,7 @@ import ( ) func TestGraphQLRunList(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("list by participantOrFollower", func(t *testing.T) { @@ -206,8 +205,7 @@ func TestGraphQLRunList(t *testing.T) { } func TestGraphQLChangeRunParticipants(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() user3, _, err := e.ServerAdminClient.CreateUser(&model.User{ @@ -669,8 +667,7 @@ func TestGraphQLChangeRunParticipants(t *testing.T) { } func TestGraphQLChangeRunOwner(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() // create a third user to test change owner @@ -713,8 +710,7 @@ func TestGraphQLChangeRunOwner(t *testing.T) { } func TestSetRunFavorite(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() createRun := func() *client.PlaybookRun { @@ -800,8 +796,7 @@ func TestSetRunFavorite(t *testing.T) { } func TestResolverFavorites(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() createRun := func() *client.PlaybookRun { @@ -833,8 +828,7 @@ func TestResolverFavorites(t *testing.T) { } func TestResolverPlaybooks(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() createRun := func() *client.PlaybookRun { @@ -860,8 +854,7 @@ func TestResolverPlaybooks(t *testing.T) { } func TestUpdateRun(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() createRun := func() *client.PlaybookRun { @@ -977,8 +970,7 @@ func TestUpdateRun(t *testing.T) { } func TestUpdateRunTaskActions(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("task actions mutation create and update", func(t *testing.T) { @@ -1071,8 +1063,7 @@ func TestUpdateRunTaskActions(t *testing.T) { } func TestBadGraphQLRequest(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() testRunsQuery := ` diff --git a/server/playbooks/server/api_playbooks_test.go b/server/playbooks/server/api_playbooks_test.go index 67f70fe8b2..d1c9e8ebed 100644 --- a/server/playbooks/server/api_playbooks_test.go +++ b/server/playbooks/server/api_playbooks_test.go @@ -22,8 +22,7 @@ import ( ) func TestPlaybooks(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateClients() e.CreateBasicServer() @@ -267,8 +266,7 @@ func TestPlaybooks(t *testing.T) { } func TestCreateInvalidPlaybook(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateClients() e.CreateBasicServer() @@ -369,8 +367,7 @@ func TestCreateInvalidPlaybook(t *testing.T) { } func TestPlaybooksRetrieval(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("get playbook", func(t *testing.T) { @@ -387,8 +384,7 @@ func TestPlaybooksRetrieval(t *testing.T) { } func TestPlaybookUpdate(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("update playbook properties", func(t *testing.T) { @@ -521,8 +517,7 @@ func TestPlaybookUpdate(t *testing.T) { } func TestPlaybookUpdateCrossTeam(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("update playbook properties not in team public playbook", func(t *testing.T) { @@ -552,8 +547,7 @@ func TestPlaybookUpdateCrossTeam(t *testing.T) { } func TestPlaybooksSort(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateClients() e.CreateBasicServer() e.SetE20Licence() @@ -795,8 +789,7 @@ func TestPlaybooksSort(t *testing.T) { } func TestPlaybooksPaging(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateClients() e.CreateBasicServer() e.SetE20Licence() @@ -935,8 +928,7 @@ func getPlaybookIDsList(playbooks []client.Playbook) []string { } func TestPlaybooksPermissions(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("test no permissions to create", func(t *testing.T) { @@ -1148,8 +1140,7 @@ func TestPlaybooksPermissions(t *testing.T) { } func TestPlaybooksConversions(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("public to private conversion", func(t *testing.T) { @@ -1208,8 +1199,7 @@ func TestPlaybooksConversions(t *testing.T) { } func TestPlaybooksImportExport(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateClients() e.CreateBasicServer() e.CreateBasicPublicPlaybook() @@ -1237,8 +1227,7 @@ func TestPlaybooksImportExport(t *testing.T) { } func TestPlaybooksDuplicate(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateClients() e.CreateBasicServer() e.SetE20Licence() @@ -1259,8 +1248,7 @@ func TestPlaybooksDuplicate(t *testing.T) { } func TestAddPostToTimeline(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() dialogRequest := model.SubmitDialogRequest{ @@ -1307,8 +1295,7 @@ func TestAddPostToTimeline(t *testing.T) { } func TestPlaybookStats(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateClients() e.CreateBasicServer() e.SetE20Licence() @@ -1343,8 +1330,7 @@ func TestPlaybookStats(t *testing.T) { } func TestPlaybookGetAutoFollows(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() p1ID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ @@ -1450,8 +1436,7 @@ func TestPlaybookGetAutoFollows(t *testing.T) { } func TestPlaybookChecklistCleanup(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("update playbook", func(t *testing.T) { diff --git a/server/playbooks/server/api_runs_test.go b/server/playbooks/server/api_runs_test.go index 780c0f8bb8..2cac09e2b3 100644 --- a/server/playbooks/server/api_runs_test.go +++ b/server/playbooks/server/api_runs_test.go @@ -19,8 +19,7 @@ import ( ) func TestRunCreation(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() incompletePlaybookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ @@ -314,8 +313,7 @@ func TestRunCreation(t *testing.T) { } func TestCreateRunInExistingChannel(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() // create playbook @@ -410,8 +408,7 @@ func TestCreateRunInExistingChannel(t *testing.T) { } func TestCreateInvalidRuns(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("fails if description is longer than 4096", func(t *testing.T) { @@ -428,8 +425,7 @@ func TestCreateInvalidRuns(t *testing.T) { } func TestRunRetrieval(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("by channel id", func(t *testing.T) { @@ -510,8 +506,7 @@ func TestRunRetrieval(t *testing.T) { } func TestRunPostStatusUpdate(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("post an update", func(t *testing.T) { @@ -571,8 +566,7 @@ func TestRunPostStatusUpdate(t *testing.T) { } func TestChecklistManagement(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() createNewRunWithNoChecklists := func(t *testing.T) *client.PlaybookRun { @@ -1188,8 +1182,7 @@ func TestChecklistManagement(t *testing.T) { } func TestChecklisFailTooLarge(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("checklist creation - failure: too large checklist", func(t *testing.T) { @@ -1213,8 +1206,7 @@ func TestChecklisFailTooLarge(t *testing.T) { } func TestRunGetStatusUpdates(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("public - get no updates", func(t *testing.T) { @@ -1343,8 +1335,7 @@ func TestRunGetStatusUpdates(t *testing.T) { } func TestRequestUpdate(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("private - no viewer access ", func(t *testing.T) { @@ -1437,8 +1428,7 @@ func TestRequestUpdate(t *testing.T) { } func TestReminderReset(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("reminder reset - timeline event created", func(t *testing.T) { @@ -1485,8 +1475,7 @@ func TestReminderReset(t *testing.T) { } func TestChecklisItem_SetAssignee(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() addSimpleChecklistToTun := func(t *testing.T, runID string) *client.PlaybookRun { @@ -1597,8 +1586,7 @@ func TestChecklisItem_SetAssignee(t *testing.T) { } func TestChecklisItem_SetCommand(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ @@ -1699,8 +1687,7 @@ func TestChecklisItem_SetCommand(t *testing.T) { } func TestGetOwners(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() ownerFromUser := func(u *model.User) client.OwnerInfo { diff --git a/server/playbooks/server/api_settings_test.go b/server/playbooks/server/api_settings_test.go index c768866469..4ef06b42c4 100644 --- a/server/playbooks/server/api_settings_test.go +++ b/server/playbooks/server/api_settings_test.go @@ -14,8 +14,7 @@ import ( ) func TestSettings(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("get settings", func(t *testing.T) { diff --git a/server/playbooks/server/api_stats_test.go b/server/playbooks/server/api_stats_test.go index 068c8979a6..a005ca7212 100644 --- a/server/playbooks/server/api_stats_test.go +++ b/server/playbooks/server/api_stats_test.go @@ -16,8 +16,7 @@ import ( ) func TestGetSiteStats(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("get sites stats", func(t *testing.T) { @@ -50,8 +49,7 @@ func TestGetSiteStats(t *testing.T) { } func TestPlaybookKeyMetricsStats(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("3 runs with published metrics, 2 runs without publishing", func(t *testing.T) { diff --git a/server/playbooks/server/api_telemetry_test.go b/server/playbooks/server/api_telemetry_test.go index ad7cfb11e9..7a3c9ab10f 100644 --- a/server/playbooks/server/api_telemetry_test.go +++ b/server/playbooks/server/api_telemetry_test.go @@ -11,8 +11,7 @@ import ( ) func TestCreateEvent(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() t.Run("create an event with bad type fails", func(t *testing.T) { diff --git a/server/playbooks/server/main_test.go b/server/playbooks/server/main_test.go index e41d6be5e1..c63f5e8b00 100644 --- a/server/playbooks/server/main_test.go +++ b/server/playbooks/server/main_test.go @@ -97,7 +97,7 @@ func getEnvWithDefault(name, defaultValue string) string { return defaultValue } -func Setup(t *testing.T) (*TestEnvironment, func()) { +func Setup(t *testing.T) *TestEnvironment { // Ignore any locally defined SiteURL as we intend to host our own. os.Unsetenv("MM_SERVICESETTINGS_SITEURL") os.Unsetenv("MM_SERVICESETTINGS_LISTENADDRESS") @@ -126,11 +126,6 @@ func Setup(t *testing.T) (*TestEnvironment, func()) { config.LogSettings.EnableFile = model.NewBool(false) config.LogSettings.ConsoleLevel = model.NewString("INFO") - // disable Boards through the feature flag - boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct") - os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct") - config.FeatureFlags.BoardsProduct = false - // override config with e2etest.config.json if it exists textConfig, err := os.ReadFile("./e2etest.config.json") if err == nil { @@ -169,10 +164,6 @@ func Setup(t *testing.T) (*TestEnvironment, func()) { ap := sapp.New(sapp.ServerConnector(server.Channels())) - teardown := func() { - os.Setenv("MM_FEATUREFLAGS_BoardsProduct", boardsProductEnvValue) - } - return &TestEnvironment{ T: t, Srv: server, @@ -184,7 +175,7 @@ func Setup(t *testing.T) (*TestEnvironment, func()) { }, }, logger: testLogger, - }, teardown + } } func (e *TestEnvironment) CreateClients() { @@ -478,8 +469,7 @@ func (e *TestEnvironment) CreateBasic() { // TestTestFramework If this is failing you know the break is not exclusively in your test. func TestTestFramework(t *testing.T) { - e, teardown := Setup(t) - defer teardown() + e := Setup(t) e.CreateBasic() } From e755ae8635f1f647e391ae5f89192a03cdeb179b Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Mon, 27 Mar 2023 13:19:58 -0300 Subject: [PATCH 19/20] Adopt placeholder_ semantics for telemetry key (#22621) Update playbooks to use the `placeholder_*` semantics in use by channels in boards. While debugging this, I realized the telemetry service isn't available until after `Start()` is called, so move most of the Playbooks initialization logic there. --- server/playbooks/product/playbooks_product.go | 453 +++++++++--------- 1 file changed, 227 insertions(+), 226 deletions(-) diff --git a/server/playbooks/product/playbooks_product.go b/server/playbooks/product/playbooks_product.go index e6b468741b..77360db7cd 100644 --- a/server/playbooks/product/playbooks_product.go +++ b/server/playbooks/product/playbooks_product.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "os" + "strings" "time" "github.com/mattermost/mattermost-server/v6/model" @@ -53,12 +54,10 @@ const ( const ServerKey product.ServiceKey = "server" -// These credentials for Rudder need to be populated at build-time, -// passing the following flags to the go build command: -// -ldflags "-X main.rudderDataplaneURL= -X main.rudderWriteKey=" -var ( - rudderDataplaneURL string - rudderWriteKey string +// These credentials for Rudder need to be replaced at build-time. +const ( + rudderDataplaneURL = "placeholder_rudder_dataplane_url" + rudderWriteKey = "placeholder_playbooks_rudder_key" ) var errServiceTypeAssert = errors.New("type assertion failed") @@ -157,229 +156,9 @@ func newPlaybooksProduct(services map[product.ServiceKey]interface{}) (product.P return nil, err } - logger := logrus.StandardLogger() - ConfigureLogrus(logger, playbooks.logger) - playbooks.server = services[ServerKey].(*mmapp.Server) playbooks.serviceAdapter = newServiceAPIAdapter(playbooks) - botID, err := playbooks.serviceAdapter.EnsureBot(&model.Bot{ - Username: "playbooks", - DisplayName: "Playbooks", - Description: "Playbooks bot.", - OwnerId: "playbooks", - }) - if err != nil { - return nil, errors.Wrapf(err, "failed to ensure bot") - } - - playbooks.config = config.NewConfigService(playbooks.serviceAdapter) - err = playbooks.config.UpdateConfiguration(func(c *config.Configuration) { - c.BotUserID = botID - c.AdminLogLevel = "debug" - }) - if err != nil { - return nil, errors.Wrapf(err, "failed save bot to config") - } - - playbooks.handler = api.NewHandler(playbooks.config) - - if rudderDataplaneURL == "" || rudderWriteKey == "" { - logrus.Warn("Rudder credentials are not set. Disabling analytics.") - playbooks.telemetryClient = &telemetry.NoopTelemetry{} - } else { - diagnosticID := playbooks.serviceAdapter.GetDiagnosticID() - serverVersion := playbooks.serviceAdapter.GetServerVersion() - playbooks.telemetryClient, err = telemetry.NewRudder(rudderDataplaneURL, rudderWriteKey, diagnosticID, model.BuildHashPlaybooks, serverVersion) - if err != nil { - return nil, errors.Wrapf(err, "failed init telemetry client") - } - } - - toggleTelemetry := func() { - diagnosticsFlag := playbooks.serviceAdapter.GetConfig().LogSettings.EnableDiagnostics - telemetryEnabled := diagnosticsFlag != nil && *diagnosticsFlag - - if telemetryEnabled { - if err = playbooks.telemetryClient.Enable(); err != nil { - logrus.WithError(err).Error("Telemetry could not be enabled") - } - return - } - - if err = playbooks.telemetryClient.Disable(); err != nil { - logrus.WithError(err).Error("Telemetry could not be disabled") - } - } - - toggleTelemetry() - playbooks.config.RegisterConfigChangeListener(toggleTelemetry) - - apiClient := sqlstore.NewClient(playbooks.serviceAdapter) - playbooks.bot = bot.New(playbooks.serviceAdapter, playbooks.config.GetConfiguration().BotUserID, playbooks.config, playbooks.telemetryClient) - scheduler := cluster.GetJobOnceScheduler(playbooks.serviceAdapter) - - sqlStore, err := sqlstore.New(apiClient, scheduler) - if err != nil { - return nil, errors.Wrapf(err, "failed creating the SQL store") - } - - playbooks.playbookRunStore = sqlstore.NewPlaybookRunStore(apiClient, sqlStore) - playbooks.playbookStore = sqlstore.NewPlaybookStore(apiClient, sqlStore) - statsStore := sqlstore.NewStatsStore(apiClient, sqlStore) - playbooks.userInfoStore = sqlstore.NewUserInfoStore(sqlStore) - channelActionStore := sqlstore.NewChannelActionStore(apiClient, sqlStore) - categoryStore := sqlstore.NewCategoryStore(apiClient, sqlStore) - - playbooks.handler = api.NewHandler(playbooks.config) - - playbooks.playbookService = app.NewPlaybookService(playbooks.playbookStore, playbooks.bot, playbooks.telemetryClient, playbooks.serviceAdapter, playbooks.metricsService) - - keywordsThreadIgnorer := app.NewKeywordsThreadIgnorer() - playbooks.channelActionService = app.NewChannelActionsService(playbooks.serviceAdapter, playbooks.bot, playbooks.config, channelActionStore, playbooks.playbookService, keywordsThreadIgnorer, playbooks.telemetryClient) - playbooks.categoryService = app.NewCategoryService(categoryStore, playbooks.serviceAdapter, playbooks.telemetryClient) - - playbooks.licenseChecker = enterprise.NewLicenseChecker(playbooks.serviceAdapter) - - playbooks.playbookRunService = app.NewPlaybookRunService( - playbooks.playbookRunStore, - playbooks.bot, - playbooks.config, - scheduler, - playbooks.telemetryClient, - playbooks.telemetryClient, - playbooks.serviceAdapter, - playbooks.playbookService, - playbooks.channelActionService, - playbooks.licenseChecker, - playbooks.metricsService, - ) - - if err = scheduler.SetCallback(playbooks.playbookRunService.HandleReminder); err != nil { - logrus.WithError(err).Error("JobOnceScheduler could not add the playbookRunService's HandleReminder") - } - if err = scheduler.Start(); err != nil { - logrus.WithError(err).Error("JobOnceScheduler could not start") - } - - // Migrations use the scheduler, so they have to be run after playbookRunService and scheduler have started - mutex, err := cluster.NewMutex(playbooks.serviceAdapter, "IR_dbMutex") - if err != nil { - return nil, errors.Wrapf(err, "failed creating cluster mutex") - } - mutex.Lock() - if err = sqlStore.RunMigrations(); err != nil { - mutex.Unlock() - return nil, errors.Wrapf(err, "failed to run migrations") - } - mutex.Unlock() - - playbooks.permissions = app.NewPermissionsService( - playbooks.playbookService, - playbooks.playbookRunService, - playbooks.serviceAdapter, - playbooks.config, - playbooks.licenseChecker, - ) - - // register collections and topics. - // TODO bump the minimum server version - if err = playbooks.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeStatus); err != nil { - logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeStatus).Warnf("failed to register collection and topic") - } - if err = playbooks.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeTask); err != nil { - logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeTask).Warnf("failed to register collection and topic") - } - - api.NewGraphQLHandler( - playbooks.handler.APIRouter, - playbooks.playbookService, - playbooks.playbookRunService, - playbooks.categoryService, - playbooks.serviceAdapter, - playbooks.config, - playbooks.permissions, - playbooks.playbookStore, - playbooks.licenseChecker, - ) - api.NewPlaybookHandler( - playbooks.handler.APIRouter, - playbooks.playbookService, - playbooks.serviceAdapter, - playbooks.config, - playbooks.permissions, - ) - api.NewPlaybookRunHandler( - playbooks.handler.APIRouter, - playbooks.playbookRunService, - playbooks.playbookService, - playbooks.permissions, - playbooks.licenseChecker, - playbooks.serviceAdapter, - playbooks.bot, - playbooks.config, - ) - api.NewStatsHandler( - playbooks.handler.APIRouter, - playbooks.serviceAdapter, - statsStore, - playbooks.playbookService, - playbooks.permissions, - playbooks.licenseChecker, - ) - api.NewBotHandler( - playbooks.handler.APIRouter, - playbooks.serviceAdapter, playbooks.bot, - playbooks.config, - playbooks.playbookRunService, - playbooks.userInfoStore, - ) - api.NewTelemetryHandler( - playbooks.handler.APIRouter, - playbooks.playbookRunService, - playbooks.serviceAdapter, - playbooks.telemetryClient, - playbooks.playbookService, - playbooks.telemetryClient, - playbooks.telemetryClient, - playbooks.telemetryClient, - playbooks.permissions, - ) - api.NewSignalHandler( - playbooks.handler.APIRouter, - playbooks.serviceAdapter, - playbooks.playbookRunService, - playbooks.playbookService, - keywordsThreadIgnorer, - ) - api.NewSettingsHandler( - playbooks.handler.APIRouter, - playbooks.serviceAdapter, - playbooks.config, - ) - api.NewActionsHandler( - playbooks.handler.APIRouter, - playbooks.channelActionService, - playbooks.serviceAdapter, - playbooks.permissions, - ) - api.NewCategoryHandler( - playbooks.handler.APIRouter, - playbooks.serviceAdapter, - playbooks.categoryService, - playbooks.playbookService, - playbooks.playbookRunService, - ) - - isTestingEnabled := false - flag := playbooks.serviceAdapter.GetConfig().ServiceSettings.EnableTesting - if flag != nil { - isTestingEnabled = *flag - } - - if err = command.RegisterCommands(playbooks.serviceAdapter.RegisterCommand, isTestingEnabled); err != nil { - return nil, errors.Wrapf(err, "failed register commands") - } return playbooks, nil } @@ -531,6 +310,228 @@ func (pp *playbooksProduct) setProductServices(services map[product.ServiceKey]i } func (pp *playbooksProduct) Start() error { + logger := logrus.StandardLogger() + ConfigureLogrus(logger, pp.logger) + + botID, err := pp.serviceAdapter.EnsureBot(&model.Bot{ + Username: "playbooks", + DisplayName: "Playbooks", + Description: "Playbooks bot.", + OwnerId: "playbooks", + }) + if err != nil { + return errors.Wrapf(err, "failed to ensure bot") + } + + pp.config = config.NewConfigService(pp.serviceAdapter) + err = pp.config.UpdateConfiguration(func(c *config.Configuration) { + c.BotUserID = botID + c.AdminLogLevel = "debug" + }) + if err != nil { + return errors.Wrapf(err, "failed save bot to config") + } + + pp.handler = api.NewHandler(pp.config) + + if strings.HasPrefix(rudderWriteKey, "placeholder_") { + logrus.Warn("Rudder credentials are not set. Disabling analytics.") + pp.telemetryClient = &telemetry.NoopTelemetry{} + } else { + logrus.Info("Rudder credentials are set. Enabling analytics.") + diagnosticID := pp.serviceAdapter.GetDiagnosticID() + serverVersion := pp.serviceAdapter.GetServerVersion() + pp.telemetryClient, err = telemetry.NewRudder(rudderDataplaneURL, rudderWriteKey, diagnosticID, model.BuildHashPlaybooks, serverVersion) + if err != nil { + return errors.Wrapf(err, "failed init telemetry client") + } + } + + toggleTelemetry := func() { + diagnosticsFlag := pp.serviceAdapter.GetConfig().LogSettings.EnableDiagnostics + telemetryEnabled := diagnosticsFlag != nil && *diagnosticsFlag + + if telemetryEnabled { + if err = pp.telemetryClient.Enable(); err != nil { + logrus.WithError(err).Error("Telemetry could not be enabled") + } + return + } + + if err = pp.telemetryClient.Disable(); err != nil { + logrus.WithError(err).Error("Telemetry could not be disabled") + } + } + + toggleTelemetry() + pp.config.RegisterConfigChangeListener(toggleTelemetry) + + apiClient := sqlstore.NewClient(pp.serviceAdapter) + pp.bot = bot.New(pp.serviceAdapter, pp.config.GetConfiguration().BotUserID, pp.config, pp.telemetryClient) + scheduler := cluster.GetJobOnceScheduler(pp.serviceAdapter) + + sqlStore, err := sqlstore.New(apiClient, scheduler) + if err != nil { + return errors.Wrapf(err, "failed creating the SQL store") + } + + pp.playbookRunStore = sqlstore.NewPlaybookRunStore(apiClient, sqlStore) + pp.playbookStore = sqlstore.NewPlaybookStore(apiClient, sqlStore) + statsStore := sqlstore.NewStatsStore(apiClient, sqlStore) + pp.userInfoStore = sqlstore.NewUserInfoStore(sqlStore) + channelActionStore := sqlstore.NewChannelActionStore(apiClient, sqlStore) + categoryStore := sqlstore.NewCategoryStore(apiClient, sqlStore) + + pp.handler = api.NewHandler(pp.config) + + pp.playbookService = app.NewPlaybookService(pp.playbookStore, pp.bot, pp.telemetryClient, pp.serviceAdapter, pp.metricsService) + + keywordsThreadIgnorer := app.NewKeywordsThreadIgnorer() + pp.channelActionService = app.NewChannelActionsService(pp.serviceAdapter, pp.bot, pp.config, channelActionStore, pp.playbookService, keywordsThreadIgnorer, pp.telemetryClient) + pp.categoryService = app.NewCategoryService(categoryStore, pp.serviceAdapter, pp.telemetryClient) + + pp.licenseChecker = enterprise.NewLicenseChecker(pp.serviceAdapter) + + pp.playbookRunService = app.NewPlaybookRunService( + pp.playbookRunStore, + pp.bot, + pp.config, + scheduler, + pp.telemetryClient, + pp.telemetryClient, + pp.serviceAdapter, + pp.playbookService, + pp.channelActionService, + pp.licenseChecker, + pp.metricsService, + ) + + if err = scheduler.SetCallback(pp.playbookRunService.HandleReminder); err != nil { + logrus.WithError(err).Error("JobOnceScheduler could not add the playbookRunService's HandleReminder") + } + if err = scheduler.Start(); err != nil { + logrus.WithError(err).Error("JobOnceScheduler could not start") + } + + // Migrations use the scheduler, so they have to be run after playbookRunService and scheduler have started + mutex, err := cluster.NewMutex(pp.serviceAdapter, "IR_dbMutex") + if err != nil { + return errors.Wrapf(err, "failed creating cluster mutex") + } + mutex.Lock() + if err = sqlStore.RunMigrations(); err != nil { + mutex.Unlock() + return errors.Wrapf(err, "failed to run migrations") + } + mutex.Unlock() + + pp.permissions = app.NewPermissionsService( + pp.playbookService, + pp.playbookRunService, + pp.serviceAdapter, + pp.config, + pp.licenseChecker, + ) + + // register collections and topics. + // TODO bump the minimum server version + if err = pp.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeStatus); err != nil { + logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeStatus).Warnf("failed to register collection and topic") + } + if err = pp.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeTask); err != nil { + logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeTask).Warnf("failed to register collection and topic") + } + + api.NewGraphQLHandler( + pp.handler.APIRouter, + pp.playbookService, + pp.playbookRunService, + pp.categoryService, + pp.serviceAdapter, + pp.config, + pp.permissions, + pp.playbookStore, + pp.licenseChecker, + ) + api.NewPlaybookHandler( + pp.handler.APIRouter, + pp.playbookService, + pp.serviceAdapter, + pp.config, + pp.permissions, + ) + api.NewPlaybookRunHandler( + pp.handler.APIRouter, + pp.playbookRunService, + pp.playbookService, + pp.permissions, + pp.licenseChecker, + pp.serviceAdapter, + pp.bot, + pp.config, + ) + api.NewStatsHandler( + pp.handler.APIRouter, + pp.serviceAdapter, + statsStore, + pp.playbookService, + pp.permissions, + pp.licenseChecker, + ) + api.NewBotHandler( + pp.handler.APIRouter, + pp.serviceAdapter, pp.bot, + pp.config, + pp.playbookRunService, + pp.userInfoStore, + ) + api.NewTelemetryHandler( + pp.handler.APIRouter, + pp.playbookRunService, + pp.serviceAdapter, + pp.telemetryClient, + pp.playbookService, + pp.telemetryClient, + pp.telemetryClient, + pp.telemetryClient, + pp.permissions, + ) + api.NewSignalHandler( + pp.handler.APIRouter, + pp.serviceAdapter, + pp.playbookRunService, + pp.playbookService, + keywordsThreadIgnorer, + ) + api.NewSettingsHandler( + pp.handler.APIRouter, + pp.serviceAdapter, + pp.config, + ) + api.NewActionsHandler( + pp.handler.APIRouter, + pp.channelActionService, + pp.serviceAdapter, + pp.permissions, + ) + api.NewCategoryHandler( + pp.handler.APIRouter, + pp.serviceAdapter, + pp.categoryService, + pp.playbookService, + pp.playbookRunService, + ) + + isTestingEnabled := false + flag := pp.serviceAdapter.GetConfig().ServiceSettings.EnableTesting + if flag != nil { + isTestingEnabled = *flag + } + + if err = command.RegisterCommands(pp.serviceAdapter.RegisterCommand, isTestingEnabled); err != nil { + return errors.Wrapf(err, "failed register commands") + } + if err := pp.hooksService.RegisterHooks(playbooksProductName, pp); err != nil { return fmt.Errorf("failed to register hooks: %w", err) } From 1cf0cff9c6056f1d57d3042b2341005b6d9c28a7 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Mon, 27 Mar 2023 13:20:39 -0300 Subject: [PATCH 20/20] Boards: Use custom placeholder for telemetry. (#22623) Adopt `placeholder_boards_rudder_key` as the replacement value for injecting the telemetry key, allowing Boards to preserve its unique telemetry key. Co-authored-by: Mattermost Build --- server/boards/services/telemetry/telemetry.go | 2 +- webapp/boards/src/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/boards/services/telemetry/telemetry.go b/server/boards/services/telemetry/telemetry.go index 61b40247a9..9719f76f78 100644 --- a/server/boards/services/telemetry/telemetry.go +++ b/server/boards/services/telemetry/telemetry.go @@ -17,7 +17,7 @@ import ( ) const ( - rudderKey = "placeholder_rudder_key" + rudderKey = "placeholder_boards_rudder_key" rudderDataplaneURL = "placeholder_rudder_dataplane_url" timeBetweenTelemetryChecks = 10 * time.Minute ) diff --git a/webapp/boards/src/index.tsx b/webapp/boards/src/index.tsx index 6413dd486d..3e807e1b5a 100644 --- a/webapp/boards/src/index.tsx +++ b/webapp/boards/src/index.tsx @@ -85,7 +85,7 @@ function getSubpath(siteURL: string): string { return url.pathname.replace(/\/+$/, '') } -const TELEMETRY_RUDDER_KEY = 'placeholder_rudder_key' +const TELEMETRY_RUDDER_KEY = 'placeholder_boards_rudder_key' const TELEMETRY_RUDDER_DATAPLANE_URL = 'placeholder_rudder_dataplane_url' const TELEMETRY_OPTIONS = { context: {