summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-rw-r--r--webapp/.eslintrc.json436
-rw-r--r--webapp/Makefile11
-rw-r--r--webapp/actions/analytics_actions.jsx12
-rw-r--r--webapp/actions/global_actions.jsx (renamed from webapp/action_creators/global_actions.jsx)4
-rw-r--r--webapp/actions/team_actions.jsx38
-rw-r--r--webapp/actions/user_actions.jsx22
-rw-r--r--webapp/actions/websocket_actions.jsx (renamed from webapp/action_creators/websocket_actions.jsx)2
-rw-r--r--webapp/client/client.jsx1497
-rw-r--r--webapp/components/about_build_modal.jsx10
-rw-r--r--webapp/components/activity_log_modal.jsx12
-rw-r--r--webapp/components/admin_console/admin_console.jsx61
-rw-r--r--webapp/components/admin_console/admin_controller.jsx221
-rw-r--r--webapp/components/admin_console/admin_navbar_dropdown.jsx28
-rw-r--r--webapp/components/admin_console/admin_settings.jsx115
-rw-r--r--webapp/components/admin_console/admin_sidebar.jsx830
-rw-r--r--webapp/components/admin_console/admin_sidebar_category.jsx83
-rw-r--r--webapp/components/admin_console/admin_sidebar_section.jsx80
-rw-r--r--webapp/components/admin_console/admin_sidebar_team.jsx87
-rw-r--r--webapp/components/admin_console/boolean_setting.jsx59
-rw-r--r--webapp/components/admin_console/brand_image_setting.jsx182
-rw-r--r--webapp/components/admin_console/compliance_reports.jsx1
-rw-r--r--webapp/components/admin_console/compliance_settings.jsx318
-rw-r--r--webapp/components/admin_console/configuration_settings.jsx76
-rw-r--r--webapp/components/admin_console/connection_security_dropdown_setting.jsx114
-rw-r--r--webapp/components/admin_console/connection_settings.jsx94
-rw-r--r--webapp/components/admin_console/custom_brand_settings.jsx137
-rw-r--r--webapp/components/admin_console/database_settings.jsx194
-rw-r--r--webapp/components/admin_console/developer_settings.jsx83
-rw-r--r--webapp/components/admin_console/dropdown_setting.jsx33
-rw-r--r--webapp/components/admin_console/email_authentication_settings.jsx109
-rw-r--r--webapp/components/admin_console/email_connection_test.jsx118
-rw-r--r--webapp/components/admin_console/email_settings.jsx1218
-rw-r--r--webapp/components/admin_console/external_service_settings.jsx94
-rw-r--r--webapp/components/admin_console/generated_setting.jsx97
-rw-r--r--webapp/components/admin_console/gitlab_settings.jsx515
-rw-r--r--webapp/components/admin_console/image_settings.jsx820
-rw-r--r--webapp/components/admin_console/ldap_settings.jsx999
-rw-r--r--webapp/components/admin_console/legal_and_support_settings.jsx431
-rw-r--r--webapp/components/admin_console/license_settings.jsx1
-rw-r--r--webapp/components/admin_console/log_settings.jsx586
-rw-r--r--webapp/components/admin_console/login_settings.jsx130
-rw-r--r--webapp/components/admin_console/logs.jsx2
-rw-r--r--webapp/components/admin_console/privacy_settings.jsx261
-rw-r--r--webapp/components/admin_console/public_link_settings.jsx91
-rw-r--r--webapp/components/admin_console/push_settings.jsx235
-rw-r--r--webapp/components/admin_console/rate_settings.jsx479
-rw-r--r--webapp/components/admin_console/recycle_db.jsx96
-rw-r--r--webapp/components/admin_console/reload_config.jsx96
-rw-r--r--webapp/components/admin_console/save_button.jsx61
-rw-r--r--webapp/components/admin_console/service_settings.jsx1042
-rw-r--r--webapp/components/admin_console/session_settings.jsx134
-rw-r--r--webapp/components/admin_console/setting.jsx14
-rw-r--r--webapp/components/admin_console/settings_group.jsx42
-rw-r--r--webapp/components/admin_console/signup_settings.jsx124
-rw-r--r--webapp/components/admin_console/sql_settings.jsx390
-rw-r--r--webapp/components/admin_console/storage_settings.jsx200
-rw-r--r--webapp/components/admin_console/team_settings.jsx735
-rw-r--r--webapp/components/admin_console/team_users.jsx41
-rw-r--r--webapp/components/admin_console/text_setting.jsx83
-rw-r--r--webapp/components/admin_console/user_item.jsx4
-rw-r--r--webapp/components/admin_console/users_and_teams_settings.jsx179
-rw-r--r--webapp/components/admin_console/webhook_settings.jsx166
-rw-r--r--webapp/components/analytics/statistic_count.jsx2
-rw-r--r--webapp/components/analytics/system_analytics.jsx3
-rw-r--r--webapp/components/analytics/team_analytics.jsx52
-rw-r--r--webapp/components/backstage/add_command.jsx51
-rw-r--r--webapp/components/backstage/add_incoming_webhook.jsx5
-rw-r--r--webapp/components/backstage/add_outgoing_webhook.jsx4
-rw-r--r--webapp/components/channel_header.jsx22
-rw-r--r--webapp/components/channel_notifications_modal.jsx10
-rw-r--r--webapp/components/channel_select.jsx51
-rw-r--r--webapp/components/claim/claim_controller.jsx (renamed from webapp/components/claim/claim.jsx)6
-rw-r--r--webapp/components/claim/components/email_to_ldap.jsx4
-rw-r--r--webapp/components/claim/components/email_to_oauth.jsx2
-rw-r--r--webapp/components/claim/components/ldap_to_email.jsx23
-rw-r--r--webapp/components/claim/components/oauth_to_email.jsx4
-rw-r--r--webapp/components/create_comment.jsx37
-rw-r--r--webapp/components/create_post.jsx21
-rw-r--r--webapp/components/create_team/components/display_name.jsx38
-rw-r--r--webapp/components/create_team/components/team_url.jsx95
-rw-r--r--webapp/components/create_team/create_team_controller.jsx (renamed from webapp/components/create_team/create_team.jsx)4
-rw-r--r--webapp/components/edit_post_modal.jsx2
-rw-r--r--webapp/components/error_bar.jsx9
-rw-r--r--webapp/components/file_attachment.jsx69
-rw-r--r--webapp/components/file_attachment_list.jsx5
-rw-r--r--webapp/components/file_info_preview.jsx1
-rw-r--r--webapp/components/file_upload.jsx5
-rw-r--r--webapp/components/header_footer_template.jsx10
-rw-r--r--webapp/components/invite_member_modal.jsx114
-rw-r--r--webapp/components/logged_in.jsx2
-rw-r--r--webapp/components/login/login_controller.jsx (renamed from webapp/components/login/login.jsx)19
-rw-r--r--webapp/components/more_channels.jsx2
-rw-r--r--webapp/components/more_direct_channels.jsx2
-rw-r--r--webapp/components/navbar.jsx134
-rw-r--r--webapp/components/navbar_dropdown.jsx25
-rw-r--r--webapp/components/needs_team.jsx2
-rw-r--r--webapp/components/password_reset_form.jsx2
-rw-r--r--webapp/components/pending_post_actions.jsx92
-rw-r--r--webapp/components/post.jsx80
-rw-r--r--webapp/components/post_attachment.jsx34
-rw-r--r--webapp/components/post_attachment_oembed.jsx39
-rw-r--r--webapp/components/post_body.jsx21
-rw-r--r--webapp/components/post_body_additional_content.jsx6
-rw-r--r--webapp/components/post_focus_view.jsx2
-rw-r--r--webapp/components/post_header.jsx7
-rw-r--r--webapp/components/post_info.jsx9
-rw-r--r--webapp/components/posts_view.jsx13
-rw-r--r--webapp/components/posts_view_container.jsx2
-rw-r--r--webapp/components/removed_from_channel_modal.jsx26
-rw-r--r--webapp/components/rhs_comment.jsx63
-rw-r--r--webapp/components/rhs_header_post.jsx2
-rw-r--r--webapp/components/rhs_root_post.jsx5
-rw-r--r--webapp/components/rhs_thread.jsx9
-rw-r--r--webapp/components/root.jsx2
-rw-r--r--webapp/components/search_results.jsx5
-rw-r--r--webapp/components/search_results_item.jsx11
-rw-r--r--webapp/components/select_team/select_team.jsx2
-rw-r--r--webapp/components/settings_sidebar.jsx4
-rw-r--r--webapp/components/sidebar.jsx31
-rw-r--r--webapp/components/sidebar_header.jsx2
-rw-r--r--webapp/components/sidebar_right_menu.jsx8
-rw-r--r--webapp/components/signup_user_complete.jsx70
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx2
-rw-r--r--webapp/components/suggestion/suggestion_list.jsx6
-rw-r--r--webapp/components/team_general_tab.jsx12
-rw-r--r--webapp/components/team_import_tab.jsx3
-rw-r--r--webapp/components/team_settings_modal.jsx6
-rw-r--r--webapp/components/textbox.jsx1
-rw-r--r--webapp/components/time_since.jsx5
-rw-r--r--webapp/components/toggle_modal_button.jsx4
-rw-r--r--webapp/components/tutorial/tutorial_intro_screens.jsx3
-rw-r--r--webapp/components/user_settings/custom_theme_chooser.jsx10
-rw-r--r--webapp/components/user_settings/import_theme_modal.jsx2
-rw-r--r--webapp/components/user_settings/manage_languages.jsx2
-rw-r--r--webapp/components/user_settings/premade_theme_chooser.jsx1
-rw-r--r--webapp/components/user_settings/user_settings_advanced.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_developer.jsx4
-rw-r--r--webapp/components/user_settings/user_settings_display.jsx127
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx16
-rw-r--r--webapp/components/user_settings/user_settings_notifications.jsx189
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx29
-rw-r--r--webapp/components/user_settings/user_settings_theme.jsx8
-rw-r--r--webapp/components/view_image.jsx3
-rw-r--r--webapp/components/view_image_popover_bar.jsx1
-rw-r--r--webapp/components/youtube_video.jsx71
-rw-r--r--webapp/i18n/en.json151
-rw-r--r--webapp/i18n/es.json167
-rw-r--r--webapp/i18n/fr.json611
-rw-r--r--webapp/i18n/ja.json204
-rw-r--r--webapp/i18n/pt.json180
-rw-r--r--webapp/package.json56
-rw-r--r--webapp/root.jsx211
-rw-r--r--webapp/sass/components/_dropdown.scss5
-rw-r--r--webapp/sass/components/_modal.scss4
-rw-r--r--webapp/sass/components/_module.scss1
-rw-r--r--webapp/sass/components/_save-button.scss7
-rw-r--r--webapp/sass/components/_scrollbar.scss8
-rw-r--r--webapp/sass/components/_videos.scss20
-rw-r--r--webapp/sass/layout/_headers.scss8
-rw-r--r--webapp/sass/layout/_markdown.scss8
-rw-r--r--webapp/sass/layout/_post.scss116
-rw-r--r--webapp/sass/layout/_sidebar-left.scss8
-rw-r--r--webapp/sass/responsive/_mobile.scss30
-rw-r--r--webapp/sass/responsive/_tablet.scss114
-rw-r--r--webapp/sass/routes/_about-modal.scss3
-rw-r--r--webapp/sass/routes/_admin-console.scss512
-rw-r--r--webapp/sass/routes/_settings.scss6
-rw-r--r--webapp/sass/routes/_signup.scss4
-rw-r--r--webapp/stores/admin_store.jsx6
-rw-r--r--webapp/stores/browser_store.jsx5
-rw-r--r--webapp/stores/channel_store.jsx2
-rw-r--r--webapp/stores/error_store.jsx10
-rw-r--r--webapp/stores/post_store.jsx5
-rw-r--r--webapp/stores/team_store.jsx5
-rw-r--r--webapp/tests/.eslintrc.json12
-rw-r--r--webapp/tests/client_channel.test.jsx356
-rw-r--r--webapp/tests/client_command.test.jsx123
-rw-r--r--webapp/tests/client_general.test.jsx333
-rw-r--r--webapp/tests/client_hooks.test.jsx139
-rw-r--r--webapp/tests/client_oauth.test.jsx60
-rw-r--r--webapp/tests/client_post.test.jsx208
-rw-r--r--webapp/tests/client_preferences.test.jsx72
-rw-r--r--webapp/tests/client_team.test.jsx235
-rw-r--r--webapp/tests/client_user.test.jsx565
-rw-r--r--webapp/tests/emoticons.test.jsx44
-rw-r--r--webapp/tests/spinner_button.test.jsx9
-rw-r--r--webapp/tests/test_helper.jsx182
-rw-r--r--webapp/utils/async_client.jsx32
-rw-r--r--webapp/utils/channel_intro_messages.jsx2
-rw-r--r--webapp/utils/constants.jsx118
-rw-r--r--webapp/utils/emoticons.jsx18
-rw-r--r--webapp/utils/markdown.jsx357
-rw-r--r--webapp/utils/text_formatting.jsx2
-rw-r--r--webapp/utils/utils.jsx50
-rw-r--r--webapp/utils/web_client.jsx21
-rw-r--r--webapp/webpack.config-test.js131
-rw-r--r--webapp/webpack.config.js21
197 files changed, 8829 insertions, 13050 deletions
diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json
index 3eb02cc40..0597f13b9 100644
--- a/webapp/.eslintrc.json
+++ b/webapp/.eslintrc.json
@@ -1,224 +1,228 @@
{
- "extends": "eslint:recommended",
- "parserOptions": {
- "ecmaVersion": 6,
- "sourceType": "module",
- "ecmaFeatures": {
- "jsx": true,
- "impliedStrict": true,
- "modules": true
- }
- },
- "parser": "babel-eslint",
- "plugins": [
- "react"
- ],
- "env": {
- "browser": true,
- "node": true,
- "jquery": true,
- "es6": true
- },
- "globals": {
+ "extends": "eslint:recommended",
+ "parserOptions": {
+ "ecmaVersion": 6,
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "jsx": true,
+ "impliedStrict": true,
+ "modules": true
+ }
+ },
+ "parser": "babel-eslint",
+ "plugins": [
+ "react"
+ ],
+ "env": {
+ "browser": true,
+ "node": true,
+ "jquery": true,
+ "es6": true
+ },
+ "globals": {
"jest": true,
"describe": true,
"it": true,
"expect": true,
"before": true
},
- "rules": {
- "comma-dangle": [2, "never"],
- "array-callback-return": 2,
- "prefer-rest-params": 2,
- "no-unmodified-loop-condition": 2,
- "sort-imports": 0,
- "no-new-symbol": 2,
- "no-empty-function": 2,
- "no-whitespace-before-property": 2,
- "no-useless-constructor": 2,
- "id-blacklist": 0,
- "one-var-declaration-per-line": 0,
- "no-extra-label": 2,
- "template-curly-spacing": [2, "never"],
- "no-self-assign": 2,
- "newline-per-chained-call": 0,
- "no-confusing-arrow": 2,
- "no-case-declarations": 2,
- "no-cond-assign": [2, "except-parens"],
- "no-console": 2,
- "no-constant-condition": 2,
- "no-debugger": 2,
- "no-dupe-args": 2,
- "no-dupe-keys": 2,
- "no-duplicate-case": 2,
- "no-empty": 2,
- "no-empty-pattern": 2,
- "no-ex-assign": 2,
- "no-extra-semi": 2,
- "no-fallthrough": 2,
- "no-func-assign": 2,
- "no-inner-declarations": 0,
- "no-invalid-regexp": 2,
- "no-irregular-whitespace": 2,
- "no-unexpected-multiline": 2,
- "no-unreachable": 2,
- "no-magic-numbers": [1, { "ignore": [-1, 0, 1, 2], "enforceConst": true, "detectObjects": true } ],
- "valid-typeof": 2,
-
- "block-scoped-var": 2,
- "complexity": [0, 8],
- "consistent-return": 2,
- "curly": [2, "all"],
- "dot-location": [2, "object"],
- "dot-notation": 2,
- "eqeqeq": [2, "smart"],
- "global-require": 2,
- "guard-for-in": 2,
- "no-alert": 2,
- "no-array-constructor": 2,
- "no-caller": 2,
- "no-div-regex": 2,
- "no-else-return": 2,
- "no-eval": 2,
- "no-extend-native": 2,
- "no-extra-bind": 2,
- "no-floating-decimal": 2,
- "no-implied-eval": 2,
- "no-implicit-globals": 0,
- "no-iterator": 2,
- "no-labels": 2,
- "no-lone-blocks": 2,
- "no-loop-func": 2,
- "no-multi-spaces": [2, { "exceptions": { "Property": false } }],
- "no-multi-str": 0,
- "no-native-reassign": 2,
- "no-new": 2,
- "no-new-func": 2,
- "no-new-object": 2,
- "no-new-wrappers": 2,
- "no-octal-escape": 2,
- "no-param-reassign": 2,
- "no-process-env": 2,
- "no-process-exit": 2,
- "no-proto": 2,
- "no-redeclare": 2,
- "no-return-assign": [2, "always"],
- "no-script-url": 2,
- "no-self-compare": 2,
- "no-sequences": 2,
- "no-throw-literal": 2,
- "no-undef-init": 2,
- "no-unused-expressions": 2,
- "no-useless-concat": 1,
- "no-void": 2,
- "no-warning-comments": 1,
- "no-with": 2,
- "radix": 2,
- "vars-on-top": 0,
- "wrap-iife": [2, "outside"],
- "yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}],
-
- "no-undefined": 2,
- "no-shadow": [2, {"hoist": "functions"}],
- "no-shadow-restricted-names": 2,
- "no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
- "no-use-before-define": [2, "nofunc"],
-
- // Style
- "array-bracket-spacing": [2, "never"],
- "brace-style": [2, "1tbs", { "allowSingleLine": false }],
- "camelcase": [2, {"properties": "never"}],
- "comma-spacing": [2, {"before": false, "after": true}],
- "comma-style": [2, "last"],
- "computed-property-spacing": [2, "never"],
- "consistent-this": [2, "self"],
- "func-names": 2,
- "func-style": [2, "declaration"],
- "indent": [2, 4, {"SwitchCase": 0}],
- "jsx-quotes": [2, "prefer-single"],
- "key-spacing": [2, {"beforeColon": false, "afterColon": true}],
- "linebreak-style": 2,
- "lines-around-comment": [2, { "beforeBlockComment": true, "beforeLineComment": true, "allowBlockStart": true, "allowBlockEnd": true }],
- "new-cap": 2,
- "new-parens": 2,
- "no-lonely-if": 2,
- "no-mixed-spaces-and-tabs": 2,
- "no-multiple-empty-lines": [2, {"max": 1}],
- "no-negated-condition": 2,
- "no-nested-ternary": 2,
- "no-spaced-func": 2,
- "no-ternary": 0,
- "no-trailing-spaces": [2, { "skipBlankLines": false }],
- "no-underscore-dangle": 2,
- "no-unneeded-ternary": [2, {"defaultAssignment": false}],
- "object-curly-spacing": [2, "never"],
- "one-var": [2, "never"],
- "operator-linebreak": [2, "after"],
- "padded-blocks": [2, "never"],
- "quote-props": [2, "as-needed"],
- "quotes": [2, "single", "avoid-escape"],
- "semi": [2, "always"],
- "semi-spacing": [2, {"before": false, "after": true}],
- "keyword-spacing": [2, {"before": true, "after": true, "overrides": {}}],
- "space-before-blocks": [2, "always"],
- "space-before-function-paren": [2, "never"],
- "space-in-parens": [2, "never"],
- "space-infix-ops": 2,
- "space-unary-ops": [2, { "words": true, "nonwords": false }],
- "wrap-regex": 2,
-
- // ES6 stuff
- "arrow-parens": [2, "always"],
- "arrow-body-style": 0,
- "arrow-spacing": [2, { "before": true, "after": true }],
- "constructor-super": 2,
- "generator-star-spacing": [2, {"before": false, "after": true}],
- "no-class-assign": 2,
- "no-const-assign": 2,
- "no-dupe-class-members": 2,
- "no-this-before-super": 2,
- "no-var": 0,
- "object-shorthand": [1, "always"],
- "prefer-arrow-callback": 1,
- "prefer-const": 1,
- "prefer-spread": 2,
- "prefer-reflect": 1,
- "prefer-template": 0,
- "require-yield": 2,
-
- // React Specific
- "react/display-name": [2, { "ignoreTranspilerName": false }],
- "react/no-deprecated": 2,
- "react/no-is-mounted": 2,
- "react/no-string-refs": 0,
- "react/jsx-pascal-case": 2,
- "react/jsx-indent": [1, 4],
- "react/jsx-equals-spacing": [2, "never"],
- "react/jsx-handler-names": 0,
- "react/jsx-boolean-value": [2, "always"],
- "react/jsx-closing-bracket-location": [2, { "location": "tag-aligned" }],
- "react/jsx-curly-spacing": [2, "never"],
- "react/jsx-indent-props": [2, 4],
- "react/jsx-key": 2,
- "react/jsx-max-props-per-line": [2, { "maximum": 1 }],
- "react/jsx-no-bind": 1,
- "react/jsx-space-before-closing": [2, "never"],
- "react/jsx-no-duplicate-props": [2, { "ignoreCase": false }],
- "react/jsx-no-literals": 1,
- "react/jsx-no-undef": 2,
- "react/jsx-uses-react": 2,
- "react/jsx-uses-vars": 2,
- "react/no-danger": 0,
- "react/no-did-mount-set-state": 2,
- "react/no-did-update-set-state": 2,
- "react/no-direct-mutation-state": 2,
- "react/no-multi-comp": [2, { "ignoreStateless": true }],
- "react/no-set-state": 0,
- "react/no-unknown-property": 2,
- "react/prefer-es6-class": 2,
- "react/prop-types": 2,
- "react/self-closing-comp": 2,
- "react/sort-comp": 0,
- "react/wrap-multilines": 2
- }
+ "rules": {
+ "array-bracket-spacing": [2, "never"],
+ "array-callback-return": 2,
+ "arrow-body-style": 0,
+ "arrow-parens": [2, "always"],
+ "arrow-spacing": [2, { "before": true, "after": true }],
+ "block-scoped-var": 2,
+ "brace-style": [2, "1tbs", { "allowSingleLine": false }],
+ "camelcase": [2, {"properties": "never"}],
+ "comma-dangle": [2, "never"],
+ "comma-spacing": [2, {"before": false, "after": true}],
+ "comma-style": [2, "last"],
+ "complexity": [1, 10],
+ "computed-property-spacing": [2, "never"],
+ "consistent-return": 2,
+ "consistent-this": [2, "self"],
+ "constructor-super": 2,
+ "curly": [2, "all"],
+ "dot-location": [2, "object"],
+ "dot-notation": 2,
+ "eqeqeq": [2, "smart"],
+ "func-names": 2,
+ "func-style": [2, "declaration"],
+ "generator-star-spacing": [2, {"before": false, "after": true}],
+ "global-require": 2,
+ "guard-for-in": 2,
+ "id-blacklist": 0,
+ "indent": [2, 4, {"SwitchCase": 0}],
+ "jsx-quotes": [2, "prefer-single"],
+ "key-spacing": [2, {"beforeColon": false, "afterColon": true}],
+ "keyword-spacing": [2, {"before": true, "after": true, "overrides": {}}],
+ "linebreak-style": 2,
+ "lines-around-comment": [2, { "beforeBlockComment": true, "beforeLineComment": true, "allowBlockStart": true, "allowBlockEnd": true }],
+ "max-nested-callbacks": [1, {"max":1}],
+ "max-nested-callbacks": [2, {"max":2}],
+ "max-statements-per-line": [2, {"max": 1}],
+ "new-cap": 2,
+ "new-parens": 2,
+ "newline-before-return": 0,
+ "newline-per-chained-call": 0,
+ "no-alert": 2,
+ "no-array-constructor": 2,
+ "no-caller": 2,
+ "no-case-declarations": 2,
+ "no-class-assign": 2,
+ "no-cond-assign": [2, "except-parens"],
+ "no-confusing-arrow": 2,
+ "no-console": 2,
+ "no-const-assign": 2,
+ "no-constant-condition": 2,
+ "no-debugger": 2,
+ "no-div-regex": 2,
+ "no-dupe-args": 2,
+ "no-dupe-class-members": 2,
+ "no-dupe-keys": 2,
+ "no-duplicate-case": 2,
+ "no-duplicate-imports": [2, {"includeExports": true}],
+ "no-else-return": 2,
+ "no-empty": 2,
+ "no-empty-function": 2,
+ "no-empty-pattern": 2,
+ "no-eval": 2,
+ "no-ex-assign": 2,
+ "no-extend-native": 2,
+ "no-extra-bind": 2,
+ "no-extra-label": 2,
+ "no-extra-semi": 2,
+ "no-fallthrough": 2,
+ "no-floating-decimal": 2,
+ "no-func-assign": 2,
+ "no-implicit-globals": 0,
+ "no-implied-eval": 2,
+ "no-inner-declarations": 0,
+ "no-invalid-regexp": 2,
+ "no-irregular-whitespace": 2,
+ "no-iterator": 2,
+ "no-labels": 2,
+ "no-lone-blocks": 2,
+ "no-lonely-if": 2,
+ "no-loop-func": 2,
+ "no-magic-numbers": [1, { "ignore": [-1, 0, 1, 2], "enforceConst": true, "detectObjects": true } ],
+ "no-mixed-spaces-and-tabs": 2,
+ "no-multi-spaces": [2, { "exceptions": { "Property": false } }],
+ "no-multi-str": 0,
+ "no-multiple-empty-lines": [2, {"max": 1}],
+ "no-native-reassign": 2,
+ "no-negated-condition": 2,
+ "no-nested-ternary": 2,
+ "no-new": 2,
+ "no-new-func": 2,
+ "no-new-object": 2,
+ "no-new-symbol": 2,
+ "no-new-wrappers": 2,
+ "no-octal-escape": 2,
+ "no-param-reassign": 2,
+ "no-process-env": 2,
+ "no-process-exit": 2,
+ "no-proto": 2,
+ "no-redeclare": 2,
+ "no-return-assign": [2, "always"],
+ "no-script-url": 2,
+ "no-self-assign": 2,
+ "no-self-compare": 2,
+ "no-sequences": 2,
+ "no-shadow": [2, {"hoist": "functions"}],
+ "no-shadow-restricted-names": 2,
+ "no-spaced-func": 2,
+ "no-ternary": 0,
+ "no-this-before-super": 2,
+ "no-throw-literal": 2,
+ "no-trailing-spaces": [2, { "skipBlankLines": false }],
+ "no-undef-init": 2,
+ "no-undefined": 2,
+ "no-underscore-dangle": 2,
+ "no-unexpected-multiline": 2,
+ "no-unmodified-loop-condition": 2,
+ "no-unneeded-ternary": [2, {"defaultAssignment": false}],
+ "no-unreachable": 2,
+ "no-unsafe-finally": 2,
+ "no-unused-expressions": 2,
+ "no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
+ "no-use-before-define": [2, "nofunc"],
+ "no-useless-computed-key": 2,
+ "no-useless-concat": 2,
+ "no-useless-constructor": 2,
+ "no-useless-escape": 2,
+ "no-var": 0,
+ "no-void": 2,
+ "no-warning-comments": 1,
+ "no-whitespace-before-property": 2,
+ "no-with": 2,
+ "object-curly-spacing": [2, "never"],
+ "object-shorthand": [1, "always"],
+ "one-var": [2, "never"],
+ "one-var-declaration-per-line": 0,
+ "operator-linebreak": [2, "after"],
+ "padded-blocks": [2, "never"],
+ "prefer-arrow-callback": 2,
+ "prefer-const": 2,
+ "prefer-reflect": 2,
+ "prefer-rest-params": 2,
+ "prefer-spread": 2,
+ "prefer-template": 0,
+ "quote-props": [2, "as-needed"],
+ "quotes": [2, "single", "avoid-escape"],
+ "radix": 2,
+ "react/display-name": [2, { "ignoreTranspilerName": false }],
+ "react/jsx-boolean-value": [2, "always"],
+ "react/jsx-closing-bracket-location": [2, { "location": "tag-aligned" }],
+ "react/jsx-curly-spacing": [2, "never"],
+ "react/jsx-equals-spacing": [2, "never"],
+ "react/jsx-first-prop-new-line": [2, "multiline"],
+ "react/jsx-handler-names": 0,
+ "react/jsx-indent": [2, 4],
+ "react/jsx-indent-props": [2, 4],
+ "react/jsx-key": 2,
+ "react/jsx-max-props-per-line": [2, { "maximum": 1 }],
+ "react/jsx-no-bind": 1,
+ "react/jsx-no-duplicate-props": [2, { "ignoreCase": false }],
+ "react/jsx-no-literals": 2,
+ "react/jsx-no-target-blank": 2,
+ "react/jsx-no-undef": 2,
+ "react/jsx-pascal-case": 2,
+ "react/jsx-space-before-closing": [2, "never"],
+ "react/jsx-uses-react": 2,
+ "react/jsx-uses-vars": 2,
+ "react/no-danger": 0,
+ "react/no-deprecated": 2,
+ "react/no-did-mount-set-state": 2,
+ "react/no-did-update-set-state": 2,
+ "react/no-direct-mutation-state": 2,
+ "react/no-is-mounted": 2,
+ "react/no-multi-comp": [2, { "ignoreStateless": true }],
+ "react/no-set-state": 0,
+ "react/no-string-refs": 0,
+ "react/no-unknown-property": 2,
+ "react/prefer-es6-class": 2,
+ "react/prefer-stateless-function": 0,
+ "react/prop-types": 2,
+ "react/require-render-return": 2,
+ "react/self-closing-comp": 2,
+ "react/sort-comp": 0,
+ "react/wrap-multilines": 2,
+ "require-yield": 2,
+ "semi": [2, "always"],
+ "semi-spacing": [2, {"before": false, "after": true}],
+ "sort-imports": 0,
+ "space-before-blocks": [2, "always"],
+ "space-before-function-paren": [2, "never"],
+ "space-in-parens": [2, "never"],
+ "space-infix-ops": 2,
+ "space-unary-ops": [2, { "words": true, "nonwords": false }],
+ "template-curly-spacing": [2, "never"],
+ "valid-typeof": 2,
+ "vars-on-top": 0,
+ "wrap-iife": [2, "outside"],
+ "wrap-regex": 2,
+ "yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}]
+ }
}
diff --git a/webapp/Makefile b/webapp/Makefile
index b0c2c831a..48172273f 100644
--- a/webapp/Makefile
+++ b/webapp/Makefile
@@ -1,10 +1,15 @@
-.PHONY: build test run clean stop
+.PHONY: build test run clean stop check-style run-unit
-test: .npminstall
+BUILD_SERVER_DIR = ..
+
+check-style: .npminstall
@echo Checking for style guide compliance
npm run check
+test: .npminstall
+ cd $(BUILD_SERVER_DIR) && $(MAKE) internal-test-client
+
.npminstall: package.json
@echo Getting dependencies using npm
@@ -12,7 +17,7 @@ test: .npminstall
touch $@
-build: | .npminstall test
+build: .npminstall
@echo Building mattermost Webapp
npm run build
diff --git a/webapp/actions/analytics_actions.jsx b/webapp/actions/analytics_actions.jsx
new file mode 100644
index 000000000..05e4eeee2
--- /dev/null
+++ b/webapp/actions/analytics_actions.jsx
@@ -0,0 +1,12 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Client from 'utils/web_client.jsx';
+
+export function track(category, action, label, property, value) {
+ Client.track(category, action, label, property, value);
+}
+
+export function trackPage() {
+ Client.trackPage();
+}
diff --git a/webapp/action_creators/global_actions.jsx b/webapp/actions/global_actions.jsx
index f437e8a03..91b51a9c2 100644
--- a/webapp/action_creators/global_actions.jsx
+++ b/webapp/actions/global_actions.jsx
@@ -18,6 +18,8 @@ import * as Utils from 'utils/utils.jsx';
import * as Websockets from './websocket_actions.jsx';
import * as I18n from 'i18n/i18n.jsx';
+import {trackPage} from 'actions/analytics_actions.jsx';
+
import {browserHistory} from 'react-router';
import en from 'i18n/en.json';
@@ -40,7 +42,7 @@ export function emitChannelClickEvent(channel) {
AsyncClient.getChannelExtraInfo(chan.id);
AsyncClient.updateLastViewedAt(chan.id);
AsyncClient.getPosts(chan.id);
- Client.trackPage();
+ trackPage();
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
diff --git a/webapp/actions/team_actions.jsx b/webapp/actions/team_actions.jsx
new file mode 100644
index 000000000..2408e55fd
--- /dev/null
+++ b/webapp/actions/team_actions.jsx
@@ -0,0 +1,38 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import UserStore from 'stores/user_store.jsx';
+
+import Constants from 'utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import Client from 'utils/web_client.jsx';
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+
+import {browserHistory} from 'react-router';
+
+export function checkIfTeamExists(teamName, onSuccess, onError) {
+ Client.findTeamByName(teamName, onSuccess, onError);
+}
+
+export function createTeam(team, onSuccess, onError) {
+ Client.createTeam(team,
+ (rteam) => {
+ AsyncClient.getDirectProfiles();
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.CREATED_TEAM,
+ team: rteam,
+ member: {team_id: rteam.id, user_id: UserStore.getCurrentId(), roles: 'admin'}
+ });
+
+ browserHistory.push('/' + rteam.name + '/channels/town-square');
+
+ if (onSuccess) {
+ onSuccess(rteam);
+ }
+ },
+ onError
+ );
+}
diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx
new file mode 100644
index 000000000..370bfc302
--- /dev/null
+++ b/webapp/actions/user_actions.jsx
@@ -0,0 +1,22 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Client from 'utils/web_client.jsx';
+
+export function switchFromLdapToEmail(email, password, ldapPassword, onSuccess, onError) {
+ Client.ldapToEmail(
+ email,
+ password,
+ ldapPassword,
+ (data) => {
+ if (data.follow_link) {
+ window.location.href = data.follow_link;
+ }
+
+ if (onSuccess) {
+ onSuccess(data);
+ }
+ },
+ onError
+ );
+}
diff --git a/webapp/action_creators/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx
index b208b4d33..e317b8db0 100644
--- a/webapp/action_creators/websocket_actions.jsx
+++ b/webapp/actions/websocket_actions.jsx
@@ -13,7 +13,7 @@ import NotificationStore from 'stores/notification_store.jsx'; //eslint-disable-
import Client from 'utils/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
const SocketEvents = Constants.SocketEvents;
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
deleted file mode 100644
index 3ee7b1de9..000000000
--- a/webapp/client/client.jsx
+++ /dev/null
@@ -1,1497 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import request from 'superagent';
-
-const HEADER_X_VERSION_ID = 'x-version-id';
-const HEADER_TOKEN = 'token';
-const HEADER_BEARER = 'BEARER';
-const HEADER_AUTH = 'Authorization';
-
-export default class Client {
- constructor() {
- this.teamId = '';
- this.serverVersion = '';
- this.logToConsole = false;
- this.useToken = false;
- this.token = '';
- this.url = '';
- this.urlVersion = '/api/v3';
- this.defaultHeaders = {
- 'X-Requested-With': 'XMLHttpRequest'
- };
-
- this.translations = {
- connectionError: 'There appears to be a problem with your internet connection.',
- unknownError: 'We received an unexpected status code from the server.'
- };
- }
-
- setUrl = (url) => {
- this.url = url;
- }
-
- setTeamId = (id) => {
- this.teamId = id;
- }
-
- getTeamId = () => {
- if (this.teamId === '') {
- console.error('You are trying to use a route that requires a team_id, but you have not called setTeamId() in client.jsx'); // eslint-disable-line no-console
- }
-
- return this.teamId;
- }
-
- getServerVersion = () => {
- return this.serverVersion;
- }
-
- getBaseRoute() {
- return `${this.url}${this.urlVersion}`;
- }
-
- getAdminRoute() {
- return `${this.url}${this.urlVersion}/admin`;
- }
-
- getLicenseRoute() {
- return `${this.url}${this.urlVersion}/license`;
- }
-
- getTeamsRoute() {
- return `${this.url}${this.urlVersion}/teams`;
- }
-
- getTeamNeededRoute() {
- return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}`;
- }
-
- getChannelsRoute() {
- return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels`;
- }
-
- getChannelNameRoute(channelName) {
- return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels/name/${channelName}`;
- }
-
- getChannelNeededRoute(channelId) {
- return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels/${channelId}`;
- }
-
- getCommandsRoute() {
- return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/commands`;
- }
-
- getHooksRoute() {
- return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/hooks`;
- }
-
- getPostsRoute(channelId) {
- return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels/${channelId}/posts`;
- }
-
- getUsersRoute() {
- return `${this.url}${this.urlVersion}/users`;
- }
-
- getFilesRoute() {
- return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/files`;
- }
-
- getOAuthRoute() {
- return `${this.url}${this.urlVersion}/oauth`;
- }
-
- getUserNeededRoute(userId) {
- return `${this.url}${this.urlVersion}/users/${userId}`;
- }
-
- setTranslations = (messages) => {
- this.translations = messages;
- }
-
- enableLogErrorsToConsole = (enabled) => {
- this.logToConsole = enabled;
- }
-
- useHeaderToken = () => {
- this.useToken = true;
- if (this.token !== '') {
- this.defaultHeaders[HEADER_AUTH] = `${HEADER_BEARER} ${this.token}`;
- }
- }
-
- track = (category, action, label, property, value) => { // eslint-disable-line no-unused-vars
- // NO-OP for inherited classes to override
- }
-
- trackPage = () => {
- // NO-OP for inherited classes to override
- }
-
- handleError = (err, res) => { // eslint-disable-line no-unused-vars
- // NO-OP for inherited classes to override
- }
-
- handleResponse = (methodName, successCallback, errorCallback, err, res) => {
- if (res && res.header) {
- this.serverVersion = res.header[HEADER_X_VERSION_ID];
- if (res.header[HEADER_X_VERSION_ID]) {
- this.serverVersion = res.header[HEADER_X_VERSION_ID];
- }
- }
-
- if (err) {
- // test to make sure it looks like a server JSON error response
- var e = null;
- if (res && res.body && res.body.id) {
- e = res.body;
- }
-
- var msg = '';
-
- if (e) {
- msg = 'method=' + methodName + ' msg=' + e.message + ' detail=' + e.detailed_error + ' rid=' + e.request_id;
- } else {
- msg = 'method=' + methodName + ' status=' + err.status + ' statusCode=' + err.statusCode + ' err=' + err;
-
- if (err.status === 0 || !err.status) {
- e = {message: this.translations.connectionError};
- } else {
- e = {message: this.translations.unknownError + ' (' + err.status + ')'};
- }
- }
-
- if (this.logToConsole) {
- console.error(msg); // eslint-disable-line no-console
- console.error(e); // eslint-disable-line no-console
- }
-
- this.track('api', 'api_weberror', methodName, 'message', msg);
-
- this.handleError(err, res);
-
- if (errorCallback) {
- errorCallback(e, err, res);
- return;
- }
- }
-
- if (successCallback) {
- successCallback(res.body, res);
- }
- }
-
- // General / Admin / Licensing Routes Section
-
- getTranslations = (url, success, error) => {
- return request.
- get(url).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getTranslations', success, error));
- }
-
- getClientConfig = (success, error) => {
- return request.
- get(`${this.getAdminRoute()}/client_props`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getClientConfig', success, error));
- }
-
- getComplianceReports = (success, error) => {
- return request.
- get(`${this.getAdminRoute()}/compliance_reports`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getComplianceReports', success, error));
- }
-
- uploadBrandImage = (image, success, error) => {
- request.
- post(`${this.getAdminRoute()}/upload_brand_image`).
- set(this.defaultHeaders).
- accept('application/json').
- attach('image', image, image.name).
- end(this.handleResponse.bind(this, 'uploadBrandImage', success, error));
- }
-
- saveComplianceReports = (job, success, error) => {
- return request.
- post(`${this.getAdminRoute()}/save_compliance_report`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(job).
- end(this.handleResponse.bind(this, 'saveComplianceReports', success, error));
- }
-
- getLogs = (success, error) => {
- return request.
- get(`${this.getAdminRoute()}/logs`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getLogs', success, error));
- }
-
- getServerAudits = (success, error) => {
- return request.
- get(`${this.getAdminRoute()}/audits`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getServerAudits', success, error));
- }
-
- getConfig = (success, error) => {
- return request.
- get(`${this.getAdminRoute()}/config`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getConfig', success, error));
- }
-
- getAnalytics = (name, teamId, success, error) => {
- let url = `${this.getAdminRoute()}/analytics/`;
- if (teamId == null) {
- url += name;
- } else {
- url += teamId + '/' + name;
- }
-
- return request.
- get(url).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getAnalytics', success, error));
- }
-
- getTeamAnalytics = (teamId, name, success, error) => {
- return request.
- get(`${this.getAdminRoute()}/analytics/${teamId}/${name}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getTeamAnalytics', success, error));
- }
-
- saveConfig = (config, success, error) => {
- request.
- post(`${this.getAdminRoute()}/save_config`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(config).
- end(this.handleResponse.bind(this, 'saveConfig', success, error));
- }
-
- testEmail = (config, success, error) => {
- request.
- post(`${this.getAdminRoute()}/test_email`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(config).
- end(this.handleResponse.bind(this, 'testEmail', success, error));
- }
-
- logClientError = (msg) => {
- var l = {};
- l.level = 'ERROR';
- l.message = msg;
-
- request.
- post(`${this.getAdminRoute()}/log_client`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(l).
- end(this.handleResponse.bind(this, 'logClientError', null, null));
- }
-
- getClientLicenceConfig = (success, error) => {
- request.
- get(`${this.getLicenseRoute()}/client_config`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getClientLicenceConfig', success, error));
- }
-
- removeLicenseFile = (success, error) => {
- request.
- post(`${this.getLicenseRoute()}/remove`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'removeLicenseFile', success, error));
- }
-
- uploadLicenseFile = (license, success, error) => {
- request.
- post(`${this.getLicenseRoute()}/add`).
- set(this.defaultHeaders).
- accept('application/json').
- attach('license', license, license.name).
- end(this.handleResponse.bind(this, 'uploadLicenseFile', success, error));
-
- this.track('api', 'api_license_upload');
- }
-
- importSlack = (fileData, success, error) => {
- request.
- post(`${this.getTeamNeededRoute()}/import_team`).
- set(this.defaultHeaders).
- accept('application/octet-stream').
- send(fileData).
- end(this.handleResponse.bind(this, 'importSlack', success, error));
- }
-
- exportTeam = (success, error) => {
- request.
- get(`${this.getTeamsRoute()}/export_team`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'exportTeam', success, error));
- }
-
- signupTeam = (email, success, error) => {
- request.
- post(`${this.getTeamsRoute()}/signup`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({email}).
- end(this.handleResponse.bind(this, 'signupTeam', success, error));
-
- this.track('api', 'api_teams_signup');
- }
-
- adminResetMfa = (userId, success, error) => {
- const data = {};
- data.user_id = userId;
-
- request.
- post(`${this.getAdminRoute()}/reset_mfa`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'adminResetMfa', success, error));
- }
-
- adminResetPassword = (userId, newPassword, success, error) => {
- var data = {};
- data.new_password = newPassword;
- data.user_id = userId;
-
- request.
- post(`${this.getAdminRoute()}/reset_password`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'adminResetPassword', success, error));
-
- this.track('api', 'api_admin_reset_password');
- }
-
- // Team Routes Section
-
- createTeamFromSignup = (teamSignup, success, error) => {
- request.
- post(`${this.getTeamsRoute()}/create_from_signup`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(teamSignup).
- end(this.handleResponse.bind(this, 'createTeamFromSignup', success, error));
- }
-
- findTeamByName = (teamName, success, error) => {
- request.
- post(`${this.getTeamsRoute()}/find_team_by_name`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({name: teamName}).
- end(this.handleResponse.bind(this, 'findTeamByName', success, error));
- }
-
- createTeam = (team, success, error) => {
- request.
- post(`${this.getTeamsRoute()}/create`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(team).
- end(this.handleResponse.bind(this, 'createTeam', success, error));
-
- this.track('api', 'api_users_create', '', 'email', team.name);
- }
-
- updateTeam = (team, success, error) => {
- request.
- post(`${this.getTeamNeededRoute()}/update`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(team).
- end(this.handleResponse.bind(this, 'updateTeam', success, error));
-
- this.track('api', 'api_teams_update_name');
- }
-
- getAllTeams = (success, error) => {
- request.
- get(`${this.getTeamsRoute()}/all`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getAllTeams', success, error));
- }
-
- getAllTeamListings = (success, error) => {
- request.
- get(`${this.getTeamsRoute()}/all_team_listings`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getAllTeamListings', success, error));
- }
-
- getMyTeam = (success, error) => {
- request.
- get(`${this.getTeamNeededRoute()}/me`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getMyTeam', success, error));
- }
-
- getTeamMembers = (teamId, success, error) => {
- request.
- get(`${this.getTeamsRoute()}/members/${teamId}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getTeamMembers', success, error));
- }
-
- inviteMembers = (data, success, error) => {
- request.
- post(`${this.getTeamNeededRoute()}/invite_members`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'inviteMembers', success, error));
-
- this.track('api', 'api_teams_invite_members');
- }
-
- addUserToTeam = (userId, success, error) => {
- request.
- post(`${this.getTeamNeededRoute()}/add_user_to_team`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({user_id: userId}).
- end(this.handleResponse.bind(this, 'addUserToTeam', success, error));
-
- this.track('api', 'api_teams_invite_members');
- }
-
- addUserToTeamFromInvite = (data, hash, inviteId, success, error) => {
- request.
- post(`${this.getTeamsRoute()}/add_user_to_team_from_invite`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({hash, data, invite_id: inviteId}).
- end(this.handleResponse.bind(this, 'addUserToTeam', success, error));
-
- this.track('api', 'api_teams_invite_members');
- }
-
- getInviteInfo = (inviteId, success, error) => {
- request.
- post(`${this.getTeamsRoute()}/get_invite_info`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({invite_id: inviteId}).
- end(this.handleResponse.bind(this, 'getInviteInfo', success, error));
- }
-
- // User Routes Setions
-
- createUser = (user, success, error) => {
- this.createUserWithInvite(user, null, null, null, success, error);
- }
-
- createUserWithInvite = (user, data, emailHash, inviteId, success, error) => {
- var url = `${this.getUsersRoute()}/create`;
-
- url += '?d=' + encodeURIComponent(data);
-
- if (emailHash) {
- url += '&h=' + encodeURIComponent(emailHash);
- }
-
- if (inviteId) {
- url += '&iid=' + encodeURIComponent(inviteId);
- }
-
- request.
- post(url).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(user).
- end(this.handleResponse.bind(this, 'createUser', success, error));
-
- this.track('api', 'api_users_create', '', 'email', user.email);
- }
-
- updateUser = (user, success, error) => {
- request.
- post(`${this.getUsersRoute()}/update`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(user).
- end(this.handleResponse.bind(this, 'updateUser', success, error));
-
- this.track('api', 'api_users_update');
- }
-
- updatePassword = (userId, currentPassword, newPassword, success, error) => {
- var data = {};
- data.user_id = userId;
- data.current_password = currentPassword;
- data.new_password = newPassword;
-
- request.
- post(`${this.getUsersRoute()}/newpassword`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'updatePassword', success, error));
-
- this.track('api', 'api_users_newpassword');
- }
-
- updateUserNotifyProps = (notifyProps, success, error) => {
- request.
- post(`${this.getUsersRoute()}/update_notify`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(notifyProps).
- end(this.handleResponse.bind(this, 'updateUserNotifyProps', success, error));
- }
-
- updateRoles = (teamId, userId, newRoles, success, error) => {
- var data = {
- team_id: teamId,
- user_id: userId,
- new_roles: newRoles
- };
-
- request.
- post(`${this.getUsersRoute()}/update_roles`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'updateRoles', success, error));
-
- this.track('api', 'api_users_update_roles');
- }
-
- updateActive = (userId, active, success, error) => {
- var data = {};
- data.user_id = userId;
- data.active = '' + active;
-
- request.
- post(`${this.getUsersRoute()}/update_active`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'updateActive', success, error));
-
- this.track('api', 'api_users_update_roles');
- }
-
- sendPasswordReset = (email, success, error) => {
- var data = {};
- data.email = email;
-
- request.
- post(`${this.getUsersRoute()}/send_password_reset`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'sendPasswordReset', success, error));
-
- this.track('api', 'api_users_send_password_reset');
- }
-
- resetPassword = (code, newPassword, success, error) => {
- var data = {};
- data.new_password = newPassword;
- data.code = code;
-
- request.
- post(`${this.getUsersRoute()}/reset_password`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'resetPassword', success, error));
-
- this.track('api', 'api_users_reset_password');
- }
-
- emailToOAuth = (email, password, service, success, error) => {
- var data = {};
- data.password = password;
- data.email = email;
- data.service = service;
-
- request.
- post(`${this.getUsersRoute()}/claim/email_to_oauth`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'emailToOAuth', success, error));
-
- this.track('api', 'api_users_email_to_oauth');
- }
-
- oauthToEmail = (email, password, success, error) => {
- var data = {};
- data.password = password;
- data.email = email;
-
- request.
- post(`${this.getUsersRoute()}/claim/oauth_to_email`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'oauthToEmail', success, error));
-
- this.track('api', 'api_users_oauth_to_email');
- }
-
- emailToLdap = (email, password, ldapId, ldapPassword, success, error) => {
- var data = {};
- data.email_password = password;
- data.email = email;
- data.ldap_id = ldapId;
- data.ldap_password = ldapPassword;
-
- request.
- post(`${this.getUsersRoute()}/claim/email_to_ldap`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'emailToLdap', success, error));
-
- this.track('api', 'api_users_email_to_ldap');
- }
-
- ldapToEmail = (email, emailPassword, ldapPassword, success, error) => {
- var data = {};
- data.email = email;
- data.ldap_password = ldapPassword;
- data.email_password = emailPassword;
-
- request.
- post(`${this.getUsersRoute()}/claim/ldap_to_email`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'ldapToEmail', success, error));
-
- this.track('api', 'api_users_oauth_to_email');
- }
-
- getInitialLoad = (success, error) => {
- request.
- get(`${this.getUsersRoute()}/initial_load`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getInitialLoad', success, error));
- }
-
- getMe = (success, error) => {
- request.
- get(`${this.getUsersRoute()}/me`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getMe', success, error));
- }
-
- login = (loginId, password, mfaToken, success, error) => {
- this.doLogin({login_id: loginId, password, token: mfaToken}, success, error);
-
- this.track('api', 'api_users_login', '', 'login_id', loginId);
- }
-
- loginById = (id, password, mfaToken, success, error) => {
- this.doLogin({id, password, token: mfaToken}, success, error);
-
- this.track('api', 'api_users_login', '', 'id', id);
- }
-
- loginByLdap = (loginId, password, mfaToken, success, error) => {
- this.doLogin({login_id: loginId, password, token: mfaToken, ldap_only: 'true'}, success, error);
-
- this.track('api', 'api_users_login', '', 'login_id', loginId);
- }
-
- doLogin = (outgoingData, success, error) => {
- var outer = this; // eslint-disable-line consistent-this
-
- request.
- post(`${this.getUsersRoute()}/login`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(outgoingData).
- end(this.handleResponse.bind(
- this,
- 'login',
- (data, res) => {
- if (res && res.header) {
- outer.token = res.header[HEADER_TOKEN];
-
- if (outer.useToken) {
- outer.defaultHeaders[HEADER_AUTH] = `${HEADER_BEARER} ${outer.token}`;
- }
- }
-
- if (success) {
- success(data, res);
- }
- },
- error
- ));
- }
-
- logout = (success, error) => {
- request.
- post(`${this.getUsersRoute()}/logout`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'logout', success, error));
-
- this.track('api', 'api_users_logout');
- }
-
- checkMfa = (loginId, success, error) => {
- const data = {
- login_id: loginId
- };
-
- request.
- post(`${this.getUsersRoute()}/mfa`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'checkMfa', success, error));
-
- this.track('api', 'api_users_oauth_to_email');
- }
-
- revokeSession = (altId, success, error) => {
- request.
- post(`${this.getUsersRoute()}/revoke_session`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({id: altId}).
- end(this.handleResponse.bind(this, 'revokeSession', success, error));
- }
-
- getSessions = (userId, success, error) => {
- request.
- get(`${this.getUserNeededRoute(userId)}/sessions`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getSessions', success, error));
- }
-
- getAudits = (userId, success, error) => {
- request.
- get(`${this.getUserNeededRoute(userId)}/audits`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getAudits', success, error));
- }
-
- getDirectProfiles = (success, error) => {
- request.
- get(`${this.getUsersRoute()}/direct_profiles`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getDirectProfiles', success, error));
- }
-
- getProfiles = (success, error) => {
- request.
- get(`${this.getUsersRoute()}/profiles/${this.getTeamId()}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getProfiles', success, error));
- }
-
- getProfilesForTeam = (teamId, success, error) => {
- request.
- get(`${this.getUsersRoute()}/profiles/${teamId}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getProfilesForTeam', success, error));
- }
-
- getProfilesForDirectMessageList = (success, error) => {
- request.
- get(`${this.getUsersRoute()}/profiles_for_dm_list/${this.getTeamId()}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getProfilesForDirectMessageList', success, error));
- }
-
- getStatuses = (ids, success, error) => {
- request.
- post(`${this.getUsersRoute()}/status`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(ids).
- end(this.handleResponse.bind(this, 'getStatuses', success, error));
- }
-
- verifyEmail = (uid, hid, success, error) => {
- request.
- post(`${this.getUsersRoute()}/verify_email`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({uid, hid}).
- end(this.handleResponse.bind(this, 'verifyEmail', success, error));
- }
-
- resendVerification = (email, success, error) => {
- request.
- post(`${this.getUsersRoute()}/resend_verification`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({email}).
- end(this.handleResponse.bind(this, 'resendVerification', success, error));
- }
-
- updateMfa = (token, activate, success, error) => {
- const data = {};
- data.activate = activate;
- data.token = token;
-
- request.
- post(`${this.getUsersRoute()}/update_mfa`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'updateMfa', success, error));
- }
-
- uploadProfileImage = (image, success, error) => {
- request.
- post(`${this.getUsersRoute()}/newimage`).
- set(this.defaultHeaders).
- attach('image', image, image.name).
- accept('application/json').
- end(this.handleResponse.bind(this, 'uploadProfileImage', success, error));
- }
-
- // Channel Routes Section
-
- createChannel = (channel, success, error) => {
- request.
- post(`${this.getChannelsRoute()}/create`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(channel).
- end(this.handleResponse.bind(this, 'createChannel', success, error));
-
- this.track('api', 'api_channels_create', channel.type, 'name', channel.name);
- }
-
- createDirectChannel = (userId, success, error) => {
- request.
- post(`${this.getChannelsRoute()}/create_direct`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({user_id: userId}).
- end(this.handleResponse.bind(this, 'createDirectChannel', success, error));
- }
-
- updateChannel = (channel, success, error) => {
- request.
- post(`${this.getChannelsRoute()}/update`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(channel).
- end(this.handleResponse.bind(this, 'updateChannel', success, error));
-
- this.track('api', 'api_channels_update');
- }
-
- updateChannelHeader = (channelId, header, success, error) => {
- const data = {
- channel_id: channelId,
- channel_header: header
- };
-
- request.
- post(`${this.getChannelsRoute()}/update_header`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'updateChannel', success, error));
-
- this.track('api', 'api_channels_header');
- }
-
- updateChannelPurpose = (channelId, purpose, success, error) => {
- const data = {
- channel_id: channelId,
- channel_purpose: purpose
- };
-
- request.
- post(`${this.getChannelsRoute()}/update_purpose`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'updateChannelPurpose', success, error));
-
- this.track('api', 'api_channels_purpose');
- }
-
- updateChannelNotifyProps = (data, success, error) => {
- request.
- post(`${this.getChannelsRoute()}/update_notify_props`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'updateChannelNotifyProps', success, error));
- }
-
- leaveChannel = (channelId, success, error) => {
- request.
- post(`${this.getChannelNeededRoute(channelId)}/leave`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'leaveChannel', success, error));
-
- this.track('api', 'api_channels_leave');
- }
-
- joinChannel = (channelId, success, error) => {
- request.
- post(`${this.getChannelNeededRoute(channelId)}/join`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'joinChannel', success, error));
-
- this.track('api', 'api_channels_join');
- }
-
- joinChannelByName = (name, success, error) => {
- request.
- post(`${this.getChannelNameRoute(name)}/join`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'joinChannelByName', success, error));
-
- this.track('api', 'api_channels_join_name');
- }
-
- deleteChannel = (channelId, success, error) => {
- request.
- post(`${this.getChannelNeededRoute(channelId)}/delete`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'deleteChannel', success, error));
-
- this.track('api', 'api_channels_delete');
- }
-
- updateLastViewedAt = (channelId, success, error) => {
- request.
- post(`${this.getChannelNeededRoute(channelId)}/update_last_viewed_at`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'updateLastViewedAt', success, error));
- }
-
- getChannels = (success, error) => {
- request.
- get(`${this.getChannelsRoute()}/`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getChannels', success, error));
- }
-
- getChannel = (channelId, success, error) => {
- request.
- get(`${this.getChannelNeededRoute(channelId)}/`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getChannel', success, error));
-
- this.track('api', 'api_channel_get');
- }
-
- getMoreChannels = (success, error) => {
- request.
- get(`${this.getChannelsRoute()}/more`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getMoreChannels', success, error));
- }
-
- getChannelCounts = (success, error) => {
- request.
- get(`${this.getChannelsRoute()}/counts`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getChannelCounts', success, error));
- }
-
- getChannelExtraInfo = (channelId, memberLimit, success, error) => {
- var url = `${this.getChannelNeededRoute(channelId)}/extra_info`;
- if (memberLimit) {
- url += '/' + memberLimit;
- }
-
- request.
- get(url).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getChannelExtraInfo', success, error));
- }
-
- addChannelMember = (channelId, userId, success, error) => {
- request.
- post(`${this.getChannelNeededRoute(channelId)}/add`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({user_id: userId}).
- end(this.handleResponse.bind(this, 'addChannelMember', success, error));
-
- this.track('api', 'api_channels_add_member');
- }
-
- removeChannelMember = (channelId, userId, success, error) => {
- request.
- post(`${this.getChannelNeededRoute(channelId)}/remove`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({user_id: userId}).
- end(this.handleResponse.bind(this, 'removeChannelMember', success, error));
-
- this.track('api', 'api_channels_remove_member');
- }
-
- // Routes for Commands
-
- listCommands = (success, error) => {
- request.
- get(`${this.getCommandsRoute()}/list`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'listCommands', success, error));
- }
-
- executeCommand = (channelId, command, suggest, success, error) => {
- request.
- post(`${this.getCommandsRoute()}/execute`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({channelId, command, suggest: '' + suggest}).
- end(this.handleResponse.bind(this, 'executeCommand', success, error));
- }
-
- addCommand = (command, success, error) => {
- request.
- post(`${this.getCommandsRoute()}/create`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(command).
- end(this.handleResponse.bind(this, 'addCommand', success, error));
- }
-
- deleteCommand = (commandId, success, error) => {
- request.
- post(`${this.getCommandsRoute()}/delete`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({id: commandId}).
- end(this.handleResponse.bind(this, 'deleteCommand', success, error));
- }
-
- listTeamCommands = (success, error) => {
- request.
- get(`${this.getCommandsRoute()}/list_team_commands`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'listTeamCommands', success, error));
- }
-
- regenCommandToken = (commandId, success, error) => {
- request.
- post(`${this.getCommandsRoute()}/regen_token`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({id: commandId}).
- end(this.handleResponse.bind(this, 'regenCommandToken', success, error));
- }
-
- // Routes for Posts
-
- createPost = (post, success, error) => {
- request.
- post(`${this.getPostsRoute(post.channel_id)}/create`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(post).
- end(this.handleResponse.bind(this, 'createPost', success, error));
-
- this.track('api', 'api_posts_create', post.channel_id, 'length', post.message.length);
- }
-
- // This is a temporary route to get around a problem with the permissions system that
- // will be fixed in 3.1 or 3.2
- getPermalinkTmp = (postId, success, error) => {
- request.
- get(`${this.getTeamNeededRoute()}/pltmp/${postId}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getPermalinkTmp', success, error));
- }
-
- getPostById = (postId, success, error) => {
- request.
- get(`${this.getTeamNeededRoute()}/posts/${postId}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getPostById', success, error));
- }
-
- getPost = (channelId, postId, success, error) => {
- request.
- get(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/get`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getPost', success, error));
- }
-
- updatePost = (post, success, error) => {
- request.
- post(`${this.getPostsRoute(post.channel_id)}/update`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(post).
- end(this.handleResponse.bind(this, 'updatePost', success, error));
-
- this.track('api', 'api_posts_update');
- }
-
- deletePost = (channelId, postId, success, error) => {
- request.
- post(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/delete`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'deletePost', success, error));
-
- this.track('api', 'api_posts_delete');
- }
-
- search = (terms, isOrSearch, success, error) => {
- const data = {};
- data.terms = terms;
- data.is_or_search = isOrSearch;
-
- request.
- post(`${this.getTeamNeededRoute()}/posts/search`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'search', success, error));
-
- this.track('api', 'api_posts_search');
- }
-
- getPostsPage = (channelId, offset, limit, success, error) => {
- request.
- get(`${this.getPostsRoute(channelId)}/page/${offset}/${limit}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getPostsPage', success, error));
- }
-
- getPosts = (channelId, since, success, error) => {
- request.
- get(`${this.getPostsRoute(channelId)}/since/${since}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getPosts', success, error));
- }
-
- getPostsBefore = (channelId, postId, offset, numPost, success, error) => {
- request.
- get(`${this.getPostsRoute(channelId)}/${postId}/before/${offset}/${numPost}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getPostsBefore', success, error));
- }
-
- getPostsAfter = (channelId, postId, offset, numPost, success, error) => {
- request.
- get(`${this.getPostsRoute(channelId)}/${postId}/after/${offset}/${numPost}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getPostsAfter', success, error));
- }
-
- // Routes for Files
-
- getFileInfo = (filename, success, error) => {
- request.
- get(`${this.getFilesRoute()}/get_info${filename}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getFileInfo', success, error));
- }
-
- getPublicLink = (filename, success, error) => {
- const data = {
- filename
- };
-
- request.
- post(`${this.getFilesRoute()}/get_public_link`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(data).
- end(this.handleResponse.bind(this, 'getPublicLink', success, error));
- }
-
- uploadFile = (file, filename, channelId, clientId, success, error) => {
- return request.
- post(`${this.getFilesRoute()}/upload`).
- set(this.defaultHeaders).
- attach('files', file, filename).
- field('channel_id', channelId).
- field('client_ids', clientId).
- accept('application/json').
- end(this.handleResponse.bind(this, 'uploadFile', success, error));
- }
-
- // Routes for OAuth
-
- registerOAuthApp = (app, success, error) => {
- request.
- post(`${this.getOAuthRoute()}/register`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(app).
- end(this.handleResponse.bind(this, 'registerOAuthApp', success, error));
-
- this.track('api', 'api_apps_register');
- }
-
- allowOAuth2 = (responseType, clientId, redirectUri, state, scope, success, error) => {
- request.
- get(`${this.getOAuthRoute()}/allow`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- query({response_type: responseType}).
- query({client_id: clientId}).
- query({redirect_uri: redirectUri}).
- query({scope}).
- query({state}).
- end(this.handleResponse.bind(this, 'allowOAuth2', success, error));
- }
-
- // Routes for Hooks
-
- addIncomingHook = (hook, success, error) => {
- request.
- post(`${this.getHooksRoute()}/incoming/create`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(hook).
- end(this.handleResponse.bind(this, 'addIncomingHook', success, error));
- }
-
- deleteIncomingHook = (hookId, success, error) => {
- request.
- post(`${this.getHooksRoute()}/incoming/delete`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({id: hookId}).
- end(this.handleResponse.bind(this, 'deleteIncomingHook', success, error));
- }
-
- listIncomingHooks = (success, error) => {
- request.
- get(`${this.getHooksRoute()}/incoming/list`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'listIncomingHooks', success, error));
- }
-
- addOutgoingHook = (hook, success, error) => {
- request.
- post(`${this.getHooksRoute()}/outgoing/create`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(hook).
- end(this.handleResponse.bind(this, 'addOutgoingHook', success, error));
- }
-
- deleteOutgoingHook = (hookId, success, error) => {
- request.
- post(`${this.getHooksRoute()}/outgoing/delete`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({id: hookId}).
- end(this.handleResponse.bind(this, 'deleteOutgoingHook', success, error));
- }
-
- listOutgoingHooks = (success, error) => {
- request.
- get(`${this.getHooksRoute()}/outgoing/list`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'listOutgoingHooks', success, error));
- }
-
- regenOutgoingHookToken = (hookId, success, error) => {
- request.
- post(`${this.getHooksRoute()}/outgoing/regen_token`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send({id: hookId}).
- end(this.handleResponse.bind(this, 'regenOutgoingHookToken', success, error));
- }
-
- //Routes for Prefrecnes
-
- getAllPreferences = (success, error) => {
- request.
- get(`${this.getBaseRoute()}/preferences/`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getAllPreferences', success, error));
- }
-
- savePreferences = (preferences, success, error) => {
- request.
- post(`${this.getBaseRoute()}/preferences/save`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- send(preferences).
- end(this.handleResponse.bind(this, 'savePreferences', success, error));
- }
-
- getPreferenceCategory = (category, success, error) => {
- request.
- get(`${this.getBaseRoute()}/preferences/${category}`).
- set(this.defaultHeaders).
- type('application/json').
- accept('application/json').
- end(this.handleResponse.bind(this, 'getPreferenceCategory', success, error));
- }
-}
diff --git a/webapp/components/about_build_modal.jsx b/webapp/components/about_build_modal.jsx
index 4fd946401..2f7b3e781 100644
--- a/webapp/components/about_build_modal.jsx
+++ b/webapp/components/about_build_modal.jsx
@@ -45,6 +45,7 @@ export default class AboutBuildModal extends React.Component {
/>
<a
target='_blank'
+ rel='noopener noreferrer'
href='http://www.mattermost.org/'
>
{'mattermost.org'}
@@ -76,6 +77,7 @@ export default class AboutBuildModal extends React.Component {
/>
<a
target='_blank'
+ rel='noopener noreferrer'
href='http://about.mattermost.com/'
>
{'about.mattermost.com'}
@@ -133,7 +135,7 @@ export default class AboutBuildModal extends React.Component {
id='about.version'
defaultMessage='Version:'
/>
- &nbsp;{config.Version}&nbsp;({config.BuildNumber})
+ {'\u00a0' + config.Version + '\u00a0' + config.BuildNumber}
</div>
</div>
{licensee}
@@ -155,6 +157,12 @@ export default class AboutBuildModal extends React.Component {
defaultMessage='Build Hash:'
/>
&nbsp;{config.BuildHash}
+ <br/>
+ <FormattedMessage
+ id='about.hashee'
+ defaultMessage='EE Build Hash:'
+ />
+ &nbsp;{config.BuildHashEnterprise}
</p>
<p>
<FormattedMessage
diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx
index d3e5ce66d..ab6224906 100644
--- a/webapp/components/activity_log_modal.jsx
+++ b/webapp/components/activity_log_modal.jsx
@@ -23,7 +23,7 @@ export default class ActivityLogModal extends React.Component {
this.onHide = this.onHide.bind(this);
this.onShow = this.onShow.bind(this);
- let state = this.getStateFromStores();
+ const state = this.getStateFromStores();
state.moreInfo = [];
this.state = state;
@@ -43,14 +43,14 @@ export default class ActivityLogModal extends React.Component {
modalContent.removeClass('animation--highlight');
}, 1500);
Client.revokeSession(altId,
- function handleRevokeSuccess() {
+ () => {
AsyncClient.getSessions();
},
- function handleRevokeError(err) {
- let state = this.getStateFromStores();
+ (err) => {
+ const state = this.getStateFromStores();
state.serverError = err;
this.setState(state);
- }.bind(this)
+ }
);
}
onShow() {
@@ -85,7 +85,7 @@ export default class ActivityLogModal extends React.Component {
}
}
handleMoreInfo(index) {
- let newMoreInfo = this.state.moreInfo;
+ const newMoreInfo = this.state.moreInfo;
newMoreInfo[index] = true;
this.setState({moreInfo: newMoreInfo});
}
diff --git a/webapp/components/admin_console/admin_console.jsx b/webapp/components/admin_console/admin_console.jsx
new file mode 100644
index 000000000..e5c528614
--- /dev/null
+++ b/webapp/components/admin_console/admin_console.jsx
@@ -0,0 +1,61 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import React from 'react';
+
+import AdminStore from 'stores/admin_store.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import AdminSidebar from './admin_sidebar.jsx';
+
+export default class AdminConsole extends React.Component {
+ static get propTypes() {
+ return {
+ children: React.PropTypes.node.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleConfigChange = this.handleConfigChange.bind(this);
+
+ this.state = {
+ config: AdminStore.getConfig()
+ };
+ }
+
+ componentWillMount() {
+ AdminStore.addConfigChangeListener(this.handleConfigChange);
+ AsyncClient.getConfig();
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeConfigChangeListener(this.handleConfigChange);
+ }
+
+ handleConfigChange() {
+ this.setState({
+ config: AdminStore.getConfig()
+ });
+ }
+
+ render() {
+ if ($.isEmptyObject(this.state.config)) {
+ return <div className='admin-console'/>;
+ }
+
+ // not every page in the system console will need the config, but the vast majority will
+ const children = React.cloneElement(this.props.children, {
+ config: this.state.config
+ });
+
+ return (
+ <div className='admin-console'>
+ <AdminSidebar/>
+ {children}
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_controller.jsx b/webapp/components/admin_console/admin_controller.jsx
deleted file mode 100644
index aea2a0197..000000000
--- a/webapp/components/admin_console/admin_controller.jsx
+++ /dev/null
@@ -1,221 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import AdminSidebar from './admin_sidebar.jsx';
-import AdminStore from 'stores/admin_store.jsx';
-import TeamStore from 'stores/team_store.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import LoadingScreen from '../loading_screen.jsx';
-
-import EmailSettingsTab from './email_settings.jsx';
-import LogSettingsTab from './log_settings.jsx';
-import LogsTab from './logs.jsx';
-import AuditsTab from './audits.jsx';
-import FileSettingsTab from './image_settings.jsx';
-import PrivacySettingsTab from './privacy_settings.jsx';
-import RateSettingsTab from './rate_settings.jsx';
-import GitLabSettingsTab from './gitlab_settings.jsx';
-import SqlSettingsTab from './sql_settings.jsx';
-import TeamSettingsTab from './team_settings.jsx';
-import ServiceSettingsTab from './service_settings.jsx';
-import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
-import TeamUsersTab from './team_users.jsx';
-import TeamAnalyticsTab from '../analytics/team_analytics.jsx';
-import LdapSettingsTab from './ldap_settings.jsx';
-import ComplianceSettingsTab from './compliance_settings.jsx';
-import LicenseSettingsTab from './license_settings.jsx';
-import SystemAnalyticsTab from '../analytics/system_analytics.jsx';
-
-import React from 'react';
-
-export default class AdminController extends React.Component {
- constructor(props) {
- super(props);
-
- this.selectTab = this.selectTab.bind(this);
- this.removeSelectedTeam = this.removeSelectedTeam.bind(this);
- this.addSelectedTeam = this.addSelectedTeam.bind(this);
- this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
- this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this);
-
- var selectedTeams = AdminStore.getSelectedTeams();
- if (selectedTeams == null) {
- selectedTeams = {};
- selectedTeams[TeamStore.getCurrentId()] = 'true';
- AdminStore.saveSelectedTeams(selectedTeams);
- }
-
- this.state = {
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams,
- selected: props.tab || 'system_analytics',
- selectedTeam: props.teamId || null
- };
- }
-
- componentDidMount() {
- AdminStore.addConfigChangeListener(this.onConfigListenerChange);
- AsyncClient.getConfig();
-
- AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
- AsyncClient.getAllTeams();
-
- $('[data-toggle="tooltip"]').tooltip();
- $('[data-toggle="popover"]').popover();
- }
-
- componentWillUnmount() {
- AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
- AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange);
- }
-
- onConfigListenerChange() {
- this.setState({
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams: AdminStore.getSelectedTeams(),
- selected: this.state.selected,
- selectedTeam: this.state.selectedTeam
- });
- }
-
- onAllTeamsListenerChange() {
- this.setState({
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams: AdminStore.getSelectedTeams(),
- selected: this.state.selected,
- selectedTeam: this.state.selectedTeam
-
- });
- }
-
- selectTab(tab, teamId) {
- this.setState({
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams: AdminStore.getSelectedTeams(),
- selected: tab,
- selectedTeam: teamId
- });
- }
-
- removeSelectedTeam(teamId) {
- var selectedTeams = AdminStore.getSelectedTeams();
- Reflect.deleteProperty(selectedTeams, teamId);
- AdminStore.saveSelectedTeams(selectedTeams);
-
- this.setState({
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams: AdminStore.getSelectedTeams(),
- selected: this.state.selected,
- selectedTeam: this.state.selectedTeam
- });
- }
-
- addSelectedTeam(teamId) {
- var selectedTeams = AdminStore.getSelectedTeams();
- selectedTeams[teamId] = 'true';
- AdminStore.saveSelectedTeams(selectedTeams);
-
- this.setState({
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams: AdminStore.getSelectedTeams(),
- selected: this.state.selected,
- selectedTeam: this.state.selectedTeam
- });
- }
-
- render() {
- var tab = <LoadingScreen/>;
-
- if (this.state.config != null) {
- if (this.state.selected === 'email_settings') {
- tab = <EmailSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'log_settings') {
- tab = <LogSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'logs') {
- tab = <LogsTab/>;
- } else if (this.state.selected === 'audits') {
- tab = <AuditsTab/>;
- } else if (this.state.selected === 'image_settings') {
- tab = <FileSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'privacy_settings') {
- tab = <PrivacySettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'rate_settings') {
- tab = <RateSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'gitlab_settings') {
- tab = <GitLabSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'sql_settings') {
- tab = <SqlSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'team_settings') {
- tab = <TeamSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'service_settings') {
- tab = <ServiceSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'legal_and_support_settings') {
- tab = <LegalAndSupportSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'ldap_settings') {
- tab = <LdapSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'compliance_settings') {
- tab = <ComplianceSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'license') {
- tab = <LicenseSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'team_users') {
- if (this.state.teams) {
- tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]}/>;
- }
- } else if (this.state.selected === 'team_analytics') {
- if (this.state.teams) {
- tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]}/>;
- }
- } else if (this.state.selected === 'system_analytics') {
- tab = <SystemAnalyticsTab/>;
- }
- }
-
- return (
- <div
- id='admin_controller'
- className='admin-controller'
- >
- <div
- className='sidebar--menu'
- id='sidebar-menu'
- />
- <AdminSidebar
- selected={this.state.selected}
- selectedTeam={this.state.selectedTeam}
- selectTab={this.selectTab}
- teams={this.state.teams}
- selectedTeams={this.state.selectedTeams}
- removeSelectedTeam={this.removeSelectedTeam}
- addSelectedTeam={this.addSelectedTeam}
- />
- <div className='inner-wrap channel__wrap'>
- <div className='row header'>
- </div>
- <div className='row main'>
- <div
- id='app-content'
- className='app__content admin'
- >
- {tab}
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
-
-AdminController.defaultProps = {
-};
-
-AdminController.propTypes = {
- tab: React.PropTypes.string,
- teamId: React.PropTypes.string
-};
diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx
index 37b8f2135..65a76a517 100644
--- a/webapp/components/admin_console/admin_navbar_dropdown.jsx
+++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx
@@ -6,7 +6,7 @@ import ReactDOM from 'react-dom';
import TeamStore from 'stores/team_store.jsx';
import Constants from 'utils/constants.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
@@ -53,13 +53,6 @@ export default class AdminNavbarDropdown extends React.Component {
var teams = [];
if (this.state.teamMembers && this.state.teamMembers.length > 0) {
- teams.push(
- <li
- key='teamDiv'
- className='divider'
- ></li>
- );
-
for (var index in this.state.teamMembers) {
if (this.state.teamMembers.hasOwnProperty(index)) {
var teamMember = this.state.teamMembers[index];
@@ -69,12 +62,23 @@ export default class AdminNavbarDropdown extends React.Component {
<Link
to={'/' + team.name + '/channels/town-square'}
>
+ <FormattedMessage
+ id='navbar_dropdown.switchTo'
+ defaultMessage='Switch to '
+ />
{team.display_name}
</Link>
</li>
);
}
}
+
+ teams.push(
+ <li
+ key='teamDiv'
+ className='divider'
+ ></li>
+ );
}
return (
@@ -99,20 +103,18 @@ export default class AdminNavbarDropdown extends React.Component {
className='dropdown-menu'
role='menu'
>
+ {teams}
<li>
<Link
to={'/select_team'}
>
+ <i className='fa fa-exchange'/>
<FormattedMessage
id='admin.nav.switch'
- defaultMessage='Switch to {display_name}'
- values={{
- display_name: global.window.mm_config.SiteName
- }}
+ defaultMessage='Team Selection'
/>
</Link>
</li>
- {teams}
<li
key='teamDiv'
className='divider'
diff --git a/webapp/components/admin_console/admin_settings.jsx b/webapp/components/admin_console/admin_settings.jsx
new file mode 100644
index 000000000..d76e1331a
--- /dev/null
+++ b/webapp/components/admin_console/admin_settings.jsx
@@ -0,0 +1,115 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import Client from 'utils/web_client.jsx';
+
+import FormError from 'components/form_error.jsx';
+import SaveButton from 'components/admin_console/save_button.jsx';
+
+export default class AdminSettings extends React.Component {
+ static get propTypes() {
+ return {
+ config: React.PropTypes.object
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ saving: false,
+ serverError: null
+ };
+ }
+
+ handleChange(id, value) {
+ this.setState({
+ saveNeeded: true,
+ [id]: value
+ });
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ this.setState({
+ saving: true,
+ serverError: null
+ });
+
+ const config = this.getConfigFromState(this.props.config);
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ saveNeeded: false,
+ saving: false
+ });
+ },
+ (err) => {
+ this.setState({
+ saving: false,
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ parseInt(str) {
+ const n = parseInt(str, 10);
+
+ if (isNaN(n)) {
+ return 0;
+ }
+
+ return n;
+ }
+
+ parseIntNonZero(str) {
+ const n = parseInt(str, 10);
+
+ if (isNaN(n) || n < 1) {
+ return 1;
+ }
+
+ return n;
+ }
+
+ render() {
+ let saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass += 'btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ {this.renderTitle()}
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ {this.renderSettings()}
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ <FormError error={this.state.serverError}/>
+ <SaveButton
+ saving={this.state.saving}
+ disabled={!this.state.saveNeeded || (this.canSave && !this.canSave())}
+ onClick={this.handleSubmit}
+ />
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx
index da406e647..cdb7e29d5 100644
--- a/webapp/components/admin_console/admin_sidebar.jsx
+++ b/webapp/components/admin_console/admin_sidebar.jsx
@@ -2,69 +2,80 @@
// See License.txt for license information.
import $ from 'jquery';
+import React from 'react';
-import AdminSidebarHeader from './admin_sidebar_header.jsx';
-import SelectTeamModal from './select_team_modal.jsx';
+import AdminStore from 'stores/admin_store.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
+import AdminSidebarHeader from './admin_sidebar_header.jsx';
+import AdminSidebarTeam from './admin_sidebar_team.jsx';
import {FormattedMessage} from 'react-intl';
-
-import {Tooltip, OverlayTrigger} from 'react-bootstrap';
-
-import React from 'react';
+import {browserHistory} from 'react-router';
+import {OverlayTrigger, Tooltip} from 'react-bootstrap';
+import SelectTeamModal from './select_team_modal.jsx';
+import AdminSidebarCategory from './admin_sidebar_category.jsx';
+import AdminSidebarSection from './admin_sidebar_section.jsx';
export default class AdminSidebar extends React.Component {
+ static get contextTypes() {
+ return {
+ router: React.PropTypes.object.isRequired
+ };
+ }
+
constructor(props) {
super(props);
- this.isSelected = this.isSelected.bind(this);
- this.handleClick = this.handleClick.bind(this);
+ this.handleAllTeamsChange = this.handleAllTeamsChange.bind(this);
+
this.removeTeam = this.removeTeam.bind(this);
this.showTeamSelect = this.showTeamSelect.bind(this);
this.teamSelectedModal = this.teamSelectedModal.bind(this);
this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this);
+ this.renderAddTeamButton = this.renderAddTeamButton.bind(this);
+ this.renderTeams = this.renderTeams.bind(this);
+
this.state = {
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
showSelectModal: false
};
}
+ componentDidMount() {
+ AdminStore.addAllTeamsChangeListener(this.handleAllTeamsChange);
+ AsyncClient.getAllTeams();
+ }
+
componentDidUpdate() {
if (!Utils.isMobile()) {
- $('.sidebar--left .nav-pills__container').perfectScrollbar();
+ $('.admin-sidebar .nav-pills__container').perfectScrollbar();
}
}
- handleClick(name, teamId, e) {
- e.preventDefault();
- this.props.selectTab(name, teamId);
+ componentWillUnmount() {
+ AdminStore.removeAllTeamsChangeListener(this.handleAllTeamsChange);
}
- isSelected(name, teamId) {
- if (this.props.selected === name) {
- if (name === 'team_users' || name === 'team_analytics') {
- if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
- return 'active';
- }
- } else {
- return 'active';
- }
- }
-
- return '';
+ handleAllTeamsChange() {
+ this.setState({
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams()
+ });
}
- removeTeam(teamId, e) {
- e.preventDefault();
- e.stopPropagation();
- Reflect.deleteProperty(this.props.selectedTeams, teamId);
- this.props.removeSelectedTeam(teamId);
+ removeTeam(team) {
+ const selectedTeams = Object.assign({}, this.state.selectedTeams);
+ Reflect.deleteProperty(selectedTeams, team.id);
+ AdminStore.saveSelectedTeams(selectedTeams);
- if (this.props.selected === 'team_users') {
- if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
- this.props.selectTab('service_settings', null);
- }
+ this.handleAllTeamsChange();
+
+ if (this.context.router.isActive('/admin_console/team/' + team.id)) {
+ browserHistory.push('/admin_console');
}
}
@@ -74,31 +85,23 @@ export default class AdminSidebar extends React.Component {
}
teamSelectedModal(teamId) {
- this.setState({showSelectModal: false});
- this.props.addSelectedTeam(teamId);
- this.forceUpdate();
+ this.setState({
+ showSelectModal: false
+ });
+
+ const selectedTeams = Object.assign({}, this.state.selectedTeams);
+ selectedTeams[teamId] = true;
+
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.handleAllTeamsChange();
}
teamSelectedModalDismissed() {
this.setState({showSelectModal: false});
}
- render() {
- var count = '*';
- var teams = (
- <FormattedMessage
- id='admin.sidebar.loading'
- defaultMessage='Loading'
- />
- );
- const removeTooltip = (
- <Tooltip id='remove-team-tooltip'>
- <FormattedMessage
- id='admin.sidebar.rmTeamSidebar'
- defaultMessage='Remove team from sidebar menu'
- />
- </Tooltip>
- );
+ renderAddTeamButton() {
const addTeamTooltip = (
<Tooltip id='add-team-tooltip'>
<FormattedMessage
@@ -108,393 +111,468 @@ export default class AdminSidebar extends React.Component {
</Tooltip>
);
- if (this.props.teams != null) {
- count = '' + Object.keys(this.props.teams).length;
+ return (
+ <span className='menu-icon--right'>
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={addTeamTooltip}
+ >
+ <a
+ href='#'
+ onClick={this.showTeamSelect}
+ >
+ <i
+ className='fa fa-plus'
+ ></i>
+ </a>
+ </OverlayTrigger>
+ </span>
+ );
+ }
+
+ renderTeams() {
+ const teams = [];
- teams = [];
- for (var key in this.props.selectedTeams) {
- if (this.props.selectedTeams.hasOwnProperty(key)) {
- var team = this.props.teams[key];
+ for (const key in this.state.selectedTeams) {
+ if (!this.state.selectedTeams.hasOwnProperty(key)) {
+ continue;
+ }
- if (team != null) {
- teams.push(
- <ul
- key={'team_' + team.id}
- className='nav nav__sub-menu'
- >
- <li>
- <a
- href='#'
- onClick={this.handleClick.bind(this, 'team_users', team.id)}
- className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id) + ' ' + this.isSelected('team_analytics', team.id)}
- >
- {team.name}
- <OverlayTrigger
- delayShow={1000}
- placement='top'
- overlay={removeTooltip}
- >
- <span
- className='menu-icon--right menu__close'
- onClick={this.removeTeam.bind(this, team.id)}
- style={{cursor: 'pointer'}}
- >
- {'×'}
- </span>
- </OverlayTrigger>
- </a>
- </li>
- <li>
- <ul className='nav nav__inner-menu'>
- <li>
- <a
- href='#'
- className={this.isSelected('team_users', team.id)}
- onClick={this.handleClick.bind(this, 'team_users', team.id)}
- >
- <FormattedMessage
- id='admin.sidebar.users'
- defaultMessage='- Users'
- />
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('team_analytics', team.id)}
- onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
- >
- <FormattedMessage
- id='admin.sidebar.statistics'
- defaultMessage='- Statistics'
- />
- </a>
- </li>
- </ul>
- </li>
- </ul>
- );
- }
- }
+ const team = this.state.teams[key];
+
+ if (!team) {
+ continue;
}
+
+ teams.push(
+ <AdminSidebarTeam
+ key={team.id}
+ team={team}
+ onRemoveTeam={this.removeTeam}
+ />
+ );
}
- let ldapSettings;
- let complianceSettings;
- let licenseSettings;
- if (global.window.mm_config.BuildEnterpriseReady === 'true') {
- if (global.window.mm_license.IsLicensed === 'true') {
+ return (
+ <AdminSidebarCategory
+ parentLink='/admin_console'
+ icon='fa-gear'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.teams'
+ defaultMessage='TEAMS ({count, number})'
+ values={{
+ count: Object.keys(this.state.teams).length
+ }}
+ />
+ }
+ action={this.renderAddTeamButton()}
+ >
+ {teams}
+ </AdminSidebarCategory>
+ );
+ }
+
+ render() {
+ let ldapSettings = null;
+ let complianceSettings = null;
+
+ let license = null;
+ let audits = null;
+
+ if (window.mm_config.BuildEnterpriseReady === 'true') {
+ if (window.mm_license.IsLicensed === 'true') {
if (global.window.mm_license.LDAP === 'true') {
ldapSettings = (
- <li>
- <a
- href='#'
- className={this.isSelected('ldap_settings')}
- onClick={this.handleClick.bind(this, 'ldap_settings', null)}
- >
+ <AdminSidebarSection
+ name='ldap'
+ title={
<FormattedMessage
id='admin.sidebar.ldap'
- defaultMessage='LDAP Settings'
+ defaultMessage='LDAP'
/>
- </a>
- </li>
+ }
+ />
);
}
if (global.window.mm_license.Compliance === 'true') {
complianceSettings = (
- <li>
- <a
- href='#'
- className={this.isSelected('compliance_settings')}
- onClick={this.handleClick.bind(this, 'compliance_settings', null)}
- >
+ <AdminSidebarSection
+ name='compliance'
+ title={
<FormattedMessage
id='admin.sidebar.compliance'
- defaultMessage='Compliance Settings'
+ defaultMessage='Compliance'
/>
- </a>
- </li>
+ }
+ />
);
}
}
- licenseSettings = (
- <li>
- <a
- href='#'
- className={this.isSelected('license')}
- onClick={this.handleClick.bind(this, 'license', null)}
- >
+ license = (
+ <AdminSidebarSection
+ name='license'
+ title={
<FormattedMessage
id='admin.sidebar.license'
defaultMessage='Edition and License'
/>
- </a>
- </li>
+ }
+ />
);
}
- let audits;
- if (global.window.mm_license.IsLicensed === 'true') {
+ if (window.mm_license.IsLicensed === 'true') {
audits = (
- <li>
- <a
- href='#'
- className={this.isSelected('audits')}
- onClick={this.handleClick.bind(this, 'audits', null)}
- >
+ <AdminSidebarSection
+ name='audits'
+ title={
<FormattedMessage
id='admin.sidebar.audits'
- defaultMessage='Compliance and Auditing'
+ defaultMessage='Complaince and Auditing'
/>
- </a>
- </li>
+ }
+ />
);
}
return (
- <div className='sidebar--left sidebar--collapsable'>
+ <div className='admin-sidebar'>
<AdminSidebarHeader/>
<div className='nav-pills__container'>
<ul className='nav nav-pills nav-stacked'>
- <li>
- <ul className='nav nav__sub-menu'>
- <li>
- <h4>
- <span className='icon fa fa-gear'></span>
- <span>
- <FormattedMessage
- id='admin.sidebar.reports'
- defaultMessage='SITE REPORTS'
- />
- </span>
- </h4>
- </li>
- </ul>
- <ul className='nav nav__sub-menu padded'>
- <li>
- <a
- href='#'
- className={this.isSelected('system_analytics')}
- onClick={this.handleClick.bind(this, 'system_analytics', null)}
- >
+ <AdminSidebarCategory
+ parentLink='/admin_console'
+ icon='fa-gear'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.reports'
+ defaultMessage='SITE REPORTS'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='system_analytics'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.view_statistics'
+ defaultMessage='View Statistics'
+ />
+ }
+ />
+ </AdminSidebarCategory>
+ <AdminSidebarCategory
+ parentLink='/admin_console'
+ icon='fa-gear'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.settings'
+ defaultMessage='SETTINGS'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='general'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.general'
+ defaultMessage='General'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='configuration'
+ title={
<FormattedMessage
- id='admin.sidebar.view_statistics'
- defaultMessage='View Statistics'
+ id='admin.sidebar.configuration'
+ defaultMessage='Configuration'
/>
- </a>
- </li>
- </ul>
- <ul className='nav nav__sub-menu'>
- <li>
- <h4>
- <span className='icon fa fa-gear'></span>
- <span>
- <FormattedMessage
- id='admin.sidebar.settings'
- defaultMessage='SETTINGS'
- />
- </span>
- </h4>
- </li>
- </ul>
- <ul className='nav nav__sub-menu padded'>
- <li>
- <a
- href='#'
- className={this.isSelected('service_settings')}
- onClick={this.handleClick.bind(this, 'service_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='users_and_teams'
+ title={
<FormattedMessage
- id='admin.sidebar.service'
- defaultMessage='Service Settings'
+ id='admin.sidebar.usersAndTeams'
+ defaultMessage='Users and Teams'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('team_settings')}
- onClick={this.handleClick.bind(this, 'team_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='privacy'
+ title={
<FormattedMessage
- id='admin.sidebar.team'
- defaultMessage='Team Settings'
+ id='admin.sidebar.privacy'
+ defaultMessage='Privacy'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('sql_settings')}
- onClick={this.handleClick.bind(this, 'sql_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='logging'
+ title={
<FormattedMessage
- id='admin.sidebar.sql'
- defaultMessage='SQL Settings'
+ id='admin.sidebar.logging'
+ defaultMessage='Logging'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('email_settings')}
- onClick={this.handleClick.bind(this, 'email_settings', null)}
- >
+ }
+ />
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='authentication'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.authentication'
+ defaultMessage='Authentication'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='email'
+ title={
<FormattedMessage
id='admin.sidebar.email'
- defaultMessage='Email Settings'
+ defaultMessage='Email'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('image_settings')}
- onClick={this.handleClick.bind(this, 'image_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='gitlab'
+ title={
<FormattedMessage
- id='admin.sidebar.file'
- defaultMessage='File Settings'
+ id='admin.sidebar.gitlab'
+ defaultMessage='GitLab'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('log_settings')}
- onClick={this.handleClick.bind(this, 'log_settings', null)}
- >
+ }
+ />
+ {ldapSettings}
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='security'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.security'
+ defaultMessage='Security'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='sign_up'
+ title={
<FormattedMessage
- id='admin.sidebar.log'
- defaultMessage='Log Settings'
+ id='admin.sidebar.signUp'
+ defaultMessage='Sign Up'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('rate_settings')}
- onClick={this.handleClick.bind(this, 'rate_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='login'
+ title={
<FormattedMessage
- id='admin.sidebar.rate_limit'
- defaultMessage='Rate Limit Settings'
+ id='admin.sidebar.login'
+ defaultMessage='Login'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('privacy_settings')}
- onClick={this.handleClick.bind(this, 'privacy_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='public_links'
+ title={
<FormattedMessage
- id='admin.sidebar.privacy'
- defaultMessage='Privacy Settings'
+ id='admin.sidebar.publicLinks'
+ defaultMessage='Public Links'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('gitlab_settings')}
- onClick={this.handleClick.bind(this, 'gitlab_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='sessions'
+ title={
<FormattedMessage
- id='admin.sidebar.gitlab'
- defaultMessage='GitLab Settings'
+ id='admin.sidebar.sessions'
+ defaultMessage='Sessions'
/>
- </a>
- </li>
- {ldapSettings}
- {complianceSettings}
- <li>
- <a
- href='#'
- className={this.isSelected('legal_and_support_settings')}
- onClick={this.handleClick.bind(this, 'legal_and_support_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='connections'
+ title={
<FormattedMessage
- id='admin.sidebar.support'
- defaultMessage='Legal and Support Settings'
+ id='admin.sidebar.connections'
+ defaultMessage='Connections'
/>
- </a>
- </li>
- </ul>
- <ul className='nav nav__sub-menu'>
- <li>
- <h4>
- <span className='icon fa fa-gear'></span>
- <span>
- <FormattedMessage
- id='admin.sidebar.teams'
- defaultMessage='TEAMS ({count})'
- values={{
- count: count
- }}
- />
- </span>
- <span className='menu-icon--right'>
- <OverlayTrigger
- delayShow={1000}
- placement='top'
- overlay={addTeamTooltip}
- >
- <a
- href='#'
- onClick={this.showTeamSelect}
- >
- <i
- className='fa fa-plus'
- ></i>
- </a>
- </OverlayTrigger>
- </span>
- </h4>
- </li>
- </ul>
- <ul className='nav nav__sub-menu padded'>
- <li>
- {teams}
- </li>
- </ul>
- <ul className='nav nav__sub-menu'>
- <li>
- <h4>
- <span className='icon fa fa-gear'></span>
- <span>
- <FormattedMessage
- id='admin.sidebar.other'
- defaultMessage='OTHER'
- />
- </span>
- </h4>
- </li>
- </ul>
- <ul className='nav nav__sub-menu padded'>
- {licenseSettings}
- {audits}
- <li>
- <a
- href='#'
- className={this.isSelected('logs')}
- onClick={this.handleClick.bind(this, 'logs', null)}
- >
+ }
+ />
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='notifications'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.notifications'
+ defaultMessage='Notifications'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='email'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.email'
+ defaultMessage='Email'
+ />
+ }
+ />
+ <AdminSidebarSection
+ name='push'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.push'
+ defaultMessage='Mobile Push'
+ />
+ }
+ />
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='integrations'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.integrations'
+ defaultMessage='Integrations'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='webhooks'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.webhooks'
+ defaultMessage='Webhooks and Commands'
+ />
+ }
+ />
+ <AdminSidebarSection
+ name='external'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.external'
+ defaultMessage='External Services'
+ />
+ }
+ />
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='database'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.database'
+ defaultMessage='Database'
+ />
+ }
+ />
+ <AdminSidebarSection
+ name='files'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.files'
+ defaultMessage='Files'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='storage'
+ title={
<FormattedMessage
- id='admin.sidebar.logs'
- defaultMessage='Logs'
+ id='admin.sidebar.storage'
+ defaultMessage='Storage'
/>
- </a>
- </li>
- </ul>
- </li>
+ }
+ />
+ <AdminSidebarSection
+ name='images'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.images'
+ defaultMessage='Images'
+ />
+ }
+ />
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='customization'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.customization'
+ defaultMessage='Customization'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='custom_brand'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.customBrand'
+ defaultMessage='Custom Branding'
+ />
+
+ }
+ />
+ <AdminSidebarSection
+ name='legal_and_support'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.legalAndSupport'
+ defaultMessage='Legal and Support'
+ />
+ }
+ />
+ </AdminSidebarSection>
+ {complianceSettings}
+ <AdminSidebarSection
+ name='rate'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.rate'
+ defaultMessage='Rate Limiting'
+ />
+ }
+ />
+ <AdminSidebarSection
+ name='developer'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.developer'
+ defaultMessage='Developer'
+ />
+ }
+ />
+ </AdminSidebarCategory>
+ {this.renderTeams()}
+ <AdminSidebarCategory
+ parentLink='/admin_console'
+ icon='fa-gear'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.other'
+ defaultMessage='OTHER'
+ />
+ }
+ >
+ {license}
+ {audits}
+ <AdminSidebarSection
+ name='logs'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.logs'
+ defaultMessage='Logs'
+ />
+ }
+ />
+ </AdminSidebarCategory>
</ul>
</div>
-
<SelectTeamModal
- teams={this.props.teams}
+ teams={this.state.teams}
show={this.state.showSelectModal}
onModalSubmit={this.teamSelectedModal}
onModalDismissed={this.teamSelectedModalDismissed}
@@ -503,13 +581,3 @@ export default class AdminSidebar extends React.Component {
);
}
}
-
-AdminSidebar.propTypes = {
- teams: React.PropTypes.object,
- selectedTeams: React.PropTypes.object,
- removeSelectedTeam: React.PropTypes.func,
- addSelectedTeam: React.PropTypes.func,
- selected: React.PropTypes.string,
- selectedTeam: React.PropTypes.string,
- selectTab: React.PropTypes.func
-};
diff --git a/webapp/components/admin_console/admin_sidebar_category.jsx b/webapp/components/admin_console/admin_sidebar_category.jsx
new file mode 100644
index 000000000..c31c84ff7
--- /dev/null
+++ b/webapp/components/admin_console/admin_sidebar_category.jsx
@@ -0,0 +1,83 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {Link} from 'react-router';
+
+export default class AdminSidebarCategory extends React.Component {
+ static get propTypes() {
+ return {
+ name: React.PropTypes.string,
+ title: React.PropTypes.node.isRequired,
+ icon: React.PropTypes.string.isRequired,
+ parentLink: React.PropTypes.string,
+ children: React.PropTypes.node,
+ action: React.PropTypes.node
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ parentLink: ''
+ };
+ }
+
+ static get contextTypes() {
+ return {
+ router: React.PropTypes.object.isRequired
+ };
+ }
+
+ render() {
+ let link = this.props.parentLink;
+ let title = (
+ <div className='category-title category-title--active'>
+ <i className={'category-icon fa ' + this.props.icon}/>
+ <span className='category-title__text'>
+ {this.props.title}
+ </span>
+ {this.props.action}
+ </div>
+ );
+
+ if (this.props.name) {
+ link += '/' + name;
+ title = (
+ <Link
+ to={link}
+ className='category-title'
+ activeClassName='category-title category-title--active'
+ >
+ {title}
+ </Link>
+ );
+ }
+
+ let clonedChildren = null;
+ if (this.props.children && this.context.router.isActive(link)) {
+ clonedChildren = (
+ <ul className='sections'>
+ {
+ React.Children.map(this.props.children, (child) => {
+ if (child === null) {
+ return null;
+ }
+
+ return React.cloneElement(child, {
+ parentLink: link
+ });
+ })
+ }
+ </ul>
+ );
+ }
+
+ return (
+ <li className='sidebar-category'>
+ {title}
+ {clonedChildren}
+ </li>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_sidebar_section.jsx b/webapp/components/admin_console/admin_sidebar_section.jsx
new file mode 100644
index 000000000..0492745ca
--- /dev/null
+++ b/webapp/components/admin_console/admin_sidebar_section.jsx
@@ -0,0 +1,80 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {Link} from 'react-router';
+
+export default class AdminSidebarSection extends React.Component {
+ static get propTypes() {
+ return {
+ name: React.PropTypes.string.isRequired,
+ title: React.PropTypes.node.isRequired,
+ parentLink: React.PropTypes.string,
+ subsection: React.PropTypes.bool,
+ children: React.PropTypes.arrayOf(React.PropTypes.element),
+ action: React.PropTypes.node,
+ onlyActiveOnIndex: React.PropTypes.bool
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ parentLink: '',
+ subsection: false,
+ children: [],
+ onlyActiveOnIndex: true
+ };
+ }
+
+ getLink() {
+ return this.props.parentLink + '/' + this.props.name;
+ }
+
+ render() {
+ const link = this.getLink();
+
+ let clonedChildren = null;
+ if (this.props.children.length > 0) {
+ clonedChildren = (
+ <ul className='nav nav__sub-menu subsections'>
+ {
+ React.Children.map(this.props.children, (child) => {
+ if (child === null) {
+ return null;
+ }
+
+ return React.cloneElement(child, {
+ parentLink: link,
+ subsection: true
+ });
+ })
+ }
+ </ul>
+ );
+ }
+
+ let className = 'sidebar-section';
+ if (this.props.subsection) {
+ className += ' sidebar-subsection';
+ }
+
+ return (
+ <li className={className}>
+ <Link
+ className={`${className}-title`}
+ activeClassName={`${className}-title ${className}-title--active`}
+ onlyActiveOnIndex={this.props.onlyActiveOnIndex}
+ onClick={this.handleClick}
+ to={link}
+ >
+ <span className={`${className}-title__text`}>
+ {this.props.title}
+ </span>
+ {this.props.action}
+ </Link>
+ {clonedChildren}
+ </li>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_sidebar_team.jsx b/webapp/components/admin_console/admin_sidebar_team.jsx
new file mode 100644
index 000000000..2b85c712c
--- /dev/null
+++ b/webapp/components/admin_console/admin_sidebar_team.jsx
@@ -0,0 +1,87 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {FormattedMessage} from 'react-intl';
+import {OverlayTrigger, Tooltip} from 'react-bootstrap';
+import AdminSidebarSection from './admin_sidebar_section.jsx';
+
+export default class AdminSidebarTeam extends React.Component {
+ static get propTypes() {
+ return {
+ team: React.PropTypes.object.isRequired,
+ onRemoveTeam: React.PropTypes.func.isRequired,
+ parentLink: React.PropTypes.string
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleRemoveTeam = this.handleRemoveTeam.bind(this);
+ }
+
+ handleRemoveTeam(e) {
+ e.preventDefault();
+
+ this.props.onRemoveTeam(this.props.team);
+ }
+
+ render() {
+ const team = this.props.team;
+
+ const removeTeamTooltip = (
+ <Tooltip id='remove-team-tooltip'>
+ <FormattedMessage
+ id='admin.sidebar.rmTeamSidebar'
+ defaultMessage='Remove team from sidebar menu'
+ />
+ </Tooltip>
+ );
+
+ const removeTeamButton = (
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={removeTeamTooltip}
+ >
+ <span
+ className='menu-icon--right menu__close'
+ onClick={this.handleRemoveTeam}
+ >
+ {'×'}
+ </span>
+ </OverlayTrigger>
+ );
+
+ return (
+ <AdminSidebarSection
+ key={team.id}
+ name={'team/' + team.id}
+ parentLink={this.props.parentLink}
+ title={team.display_name}
+ action={removeTeamButton}
+ >
+ <AdminSidebarSection
+ name='users'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.users'
+ defaultMessage='- Users'
+ />
+ }
+ />
+ <AdminSidebarSection
+ name='analytics'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.statistics'
+ defaultMessage='- Statistics'
+ />
+ }
+ />
+ </AdminSidebarSection>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/boolean_setting.jsx b/webapp/components/admin_console/boolean_setting.jsx
index 99d508d68..bdc1d79bf 100644
--- a/webapp/components/admin_console/boolean_setting.jsx
+++ b/webapp/components/admin_console/boolean_setting.jsx
@@ -8,16 +8,44 @@ import Setting from './setting.jsx';
import {FormattedMessage} from 'react-intl';
export default class BooleanSetting extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value === 'true');
+ }
+
render() {
+ let helpText;
+ if (this.props.disabled && this.props.disabledText) {
+ helpText = (
+ <div>
+ <span className='admin-console__disabled-text'>
+ {this.props.disabledText}
+ </span>
+ {this.props.helpText}
+ </div>
+ );
+ } else {
+ helpText = this.props.helpText;
+ }
+
return (
- <Setting label={this.props.label}>
+ <Setting
+ label={this.props.label}
+ helpText={helpText}
+ >
<label className='radio-inline'>
<input
type='radio'
value='true'
- checked={this.props.currentValue}
- onChange={this.props.handleChange}
- disabled={this.props.isDisabled}
+ name={this.props.id}
+ checked={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
/>
{this.props.trueText}
</label>
@@ -25,13 +53,13 @@ export default class BooleanSetting extends React.Component {
<input
type='radio'
value='false'
- checked={!this.props.currentValue}
- onChange={this.props.handleChange}
- disabled={this.props.isDisabled}
+ name={this.props.id}
+ checked={!this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
/>
{this.props.falseText}
</label>
- {this.props.helpText}
</Setting>
);
}
@@ -39,24 +67,27 @@ export default class BooleanSetting extends React.Component {
BooleanSetting.defaultProps = {
trueText: (
<FormattedMessage
- id='admin.ldap.true'
+ id='admin.true'
defaultMessage='true'
/>
),
falseText: (
<FormattedMessage
- id='admin.ldap.false'
+ id='admin.false'
defaultMessage='false'
/>
- )
+ ),
+ disabled: false
};
BooleanSetting.propTypes = {
+ id: React.PropTypes.string.isRequired,
label: React.PropTypes.node.isRequired,
- currentValue: React.PropTypes.bool.isRequired,
+ value: React.PropTypes.bool.isRequired,
+ onChange: React.PropTypes.func.isRequired,
trueText: React.PropTypes.node,
falseText: React.PropTypes.node,
- isDisabled: React.PropTypes.bool.isRequired,
- handleChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool.isRequired,
+ disabledText: React.PropTypes.node,
helpText: React.PropTypes.node.isRequired
};
diff --git a/webapp/components/admin_console/brand_image_setting.jsx b/webapp/components/admin_console/brand_image_setting.jsx
new file mode 100644
index 000000000..74f2290af
--- /dev/null
+++ b/webapp/components/admin_console/brand_image_setting.jsx
@@ -0,0 +1,182 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import Client from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import FormError from 'components/form_error.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+
+export default class BrandImageSetting extends React.Component {
+ static get propTypes() {
+ return {
+ disabled: React.PropTypes.bool.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleImageChange = this.handleImageChange.bind(this);
+ this.handleImageSubmit = this.handleImageSubmit.bind(this);
+
+ this.state = {
+ brandImage: null,
+ brandImageExists: false,
+ brandImageTimestamp: Date.now(),
+ uploading: false,
+ uploadCompleted: false,
+ error: ''
+ };
+ }
+
+ componentWillMount() {
+ $.get(Client.getAdminRoute() + '/get_brand_image?t=' + this.state.brandImageTimestamp).done(() => {
+ this.setState({brandImageExists: true});
+ });
+ }
+
+ handleImageChange() {
+ const element = $(this.refs.fileInput);
+
+ if (element.prop('files').length > 0) {
+ this.setState({
+ brandImage: element.prop('files')[0]
+ });
+ }
+ }
+
+ handleImageSubmit(e) {
+ e.preventDefault();
+
+ if (!this.state.brandImage) {
+ return;
+ }
+
+ if (this.state.uploading) {
+ return;
+ }
+
+ $(ReactDOM.findDOMNode(this.refs.upload)).button('loading');
+
+ this.setState({
+ uploading: true,
+ error: ''
+ });
+
+ Client.uploadBrandImage(
+ this.state.brandImage,
+ () => {
+ $(ReactDOM.findDOMNode(this.refs.upload)).button('complete');
+
+ this.setState({
+ brandImageExists: true,
+ brandImage: null,
+ brandImageTimestamp: Date.now(),
+ uploading: false
+ });
+ },
+ (err) => {
+ $(ReactDOM.findDOMNode(this.refs.upload)).button('reset');
+
+ this.setState({
+ uploading: false,
+ error: err.message
+ });
+ }
+ );
+ }
+
+ render() {
+ let btnClass = 'btn';
+ if (this.state.brandImage) {
+ btnClass += ' btn-primary';
+ }
+
+ let img = null;
+ if (this.state.brandImage) {
+ img = (
+ <img
+ ref='image'
+ className='brand-img'
+ src=''
+ />
+ );
+ } else if (this.state.brandImageExists) {
+ img = (
+ <img
+ className='brand-img'
+ src={Client.getAdminRoute() + '/get_brand_image?t=' + this.state.brandImageTimestamp}
+ />
+ );
+ } else {
+ img = (
+ <p>
+ <FormattedMessage
+ id='admin.team.noBrandImage'
+ defaultMessage='No brand image uploaded'
+ />
+ </p>
+ );
+ }
+
+ return (
+ <div className='form-group'>
+ <label className='control-label col-sm-4'>
+ <FormattedMessage
+ id='admin.team.brandImageTitle'
+ defaultMessage='Custom Brand Image:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ {img}
+ </div>
+ <div className='col-sm-4'/>
+ <div className='col-sm-8'>
+ <div className='file__upload'>
+ <button
+ className='btn btn-default'
+ disabled={this.props.disabled}
+ >
+ <FormattedMessage
+ id='admin.team.chooseImage'
+ defaultMessage='Choose New Image'
+ />
+ </button>
+ <input
+ ref='fileInput'
+ type='file'
+ accept='.jpg,.png,.bmp'
+ onChange={this.handleImageChange}
+ />
+ </div>
+ <button
+ className={btnClass}
+ disabled={this.props.disabled || !this.state.brandImage}
+ onClick={this.handleImageSubmit}
+ id='upload-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + Utils.localizeMessage('admin.team.uploading', 'Uploading..')}
+ data-complete-text={'<span class=\'glyphicon glyphicon-ok\'></span> ' + Utils.localizeMessage('admin.team.uploaded', 'Uploaded!')}
+ >
+ <FormattedMessage
+ id='admin.team.upload'
+ defaultMessage='Upload'
+ />
+ </button>
+ <br/>
+ <FormError error={this.state.error}/>
+ <p className='help-text no-margin'>
+ <FormattedHTMLMessage
+ id='admin.team.uploadDesc'
+ defaultMessage='Customize your user experience by adding a custom image to your login screen. See examples at <a href="http://docs.mattermost.com/administration/config-settings.html#custom-branding" target="_blank">docs.mattermost.com/administration/config-settings.html#custom-branding</a>.'
+ />
+ </p>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/compliance_reports.jsx b/webapp/components/admin_console/compliance_reports.jsx
index 79b0d2210..a93f7a17c 100644
--- a/webapp/components/admin_console/compliance_reports.jsx
+++ b/webapp/components/admin_console/compliance_reports.jsx
@@ -273,7 +273,6 @@ export default class ComplianceReports extends React.Component {
defaultMessage='Compliance Reports'
/>
</h3>
-
<div className='row'>
<div className='col-sm-6 col-md-4 form-group'>
<label>
diff --git a/webapp/components/admin_console/compliance_settings.jsx b/webapp/components/admin_console/compliance_settings.jsx
index 53f060e11..d31759150 100644
--- a/webapp/components/admin_console/compliance_settings.jsx
+++ b/webapp/components/admin_console/compliance_settings.jsx
@@ -1,83 +1,51 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from '../../utils/async_client.jsx';
-import * as Utils from '../../utils/utils.jsx';
+import React from 'react';
-import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import * as Utils from 'utils/utils.jsx';
-import React from 'react';
-import ReactDOM from 'react-dom';
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-export default class ComplianceSettings extends React.Component {
+export default class ComplianceSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleChange = this.handleChange.bind(this);
- this.handleEnable = this.handleEnable.bind(this);
- this.handleDisable = this.handleDisable.bind(this);
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- this.state = {
- saveNeeded: false,
- serverError: null,
- enable: this.props.config.ComplianceSettings.Enable
- };
- }
- handleChange() {
- this.setState({saveNeeded: true});
- }
- handleEnable() {
- this.setState({saveNeeded: true, enable: true});
- }
- handleDisable() {
- this.setState({saveNeeded: true, enable: false});
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enable: props.config.ComplianceSettings.Enable,
+ directory: props.config.ComplianceSettings.Directory,
+ enableDaily: props.config.ComplianceSettings.EnableDaily
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
- const config = this.props.config;
- const oldEnable = config.ComplianceSettings.Enable;
- config.ComplianceSettings.Enable = this.refs.Enable.checked;
- config.ComplianceSettings.Directory = ReactDOM.findDOMNode(this.refs.Directory).value;
- config.ComplianceSettings.EnableDaily = this.refs.EnableDaily.checked;
+ getConfigFromState(config) {
+ config.ComplianceSettings.Enable = this.state.enable;
+ config.ComplianceSettings.Directory = this.state.directory;
+ config.ComplianceSettings.EnableDaily = this.state.enableDaily;
- Client.saveConfig(
- config,
- () => {
- $('#save-button').button('reset');
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- if (oldEnable !== config.ComplianceSettings.Enable) {
- window.location.reload();
- }
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
- );
+ return config;
}
- render() {
- let serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
- let saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.compliance.title'
+ defaultMessage='Compliance Settings'
+ />
+ </h3>
+ );
+ }
+ renderSettings() {
const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.Compliance === 'true';
let bannerContent;
@@ -95,170 +63,64 @@ export default class ComplianceSettings extends React.Component {
}
return (
- <div className='wrapper--fixed'>
+ <SettingsGroup>
{bannerContent}
- <h3>
- <FormattedMessage
- id='admin.compliance.title'
- defaultMessage='Compliance Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Enable'
- >
- <FormattedMessage
- id='admin.compliance.enableTitle'
- defaultMessage='Enable Compliance:'
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='true'
- ref='Enable'
- defaultChecked={this.props.config.ComplianceSettings.Enable}
- onChange={this.handleEnable}
- disabled={!licenseEnabled}
- />
- <FormattedMessage
- id='admin.compliance.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='false'
- defaultChecked={!this.props.config.ComplianceSettings.Enable}
- onChange={this.handleDisable}
- />
- <FormattedMessage
- id='admin.compliance.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.compliance.enableDesc'
- defaultMessage='When true, Mattermost allows compliance reporting'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Directory'
- >
- <FormattedMessage
- id='admin.compliance.directoryTitle'
- defaultMessage='Compliance Directory Location:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='Directory'
- ref='Directory'
- placeholder={Utils.localizeMessage('admin.compliance.directoryExample', 'Ex "./data/"')}
- defaultValue={this.props.config.ComplianceSettings.Directory}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.compliance.directoryDescription'
- defaultMessage='Directory to which compliance reports are written. If blank, will be set to ./data/.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableDaily'
- >
- <FormattedMessage
- id='admin.compliance.enableDailyTitle'
- defaultMessage='Enable Daily Report:'
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableDaily'
- value='true'
- ref='EnableDaily'
- defaultChecked={this.props.config.ComplianceSettings.EnableDaily}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <FormattedMessage
- id='admin.compliance.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableDaily'
- value='false'
- defaultChecked={!this.props.config.ComplianceSettings.EnableDaily}
- disabled={!this.state.enable}
- />
- <FormattedMessage
- id='admin.compliance.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.compliance.enableDailyDesc'
- defaultMessage='When true, Mattermost will generate a daily compliance report.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + Utils.localizeMessage('admin.compliance.saving', 'Saving Config...')}
- >
- <FormattedMessage
- id='admin.compliance.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
- </form>
- </div>
+ <BooleanSetting
+ id='enable'
+ label={
+ <FormattedMessage
+ id='admin.compliance.enableTitle'
+ defaultMessage='Enable Compliance:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.compliance.enableDesc'
+ defaultMessage='When true, Mattermost allows compliance reporting'
+ />
+ }
+ value={this.state.enable}
+ onChange={this.handleChange}
+ disabled={!licenseEnabled}
+ />
+ <TextSetting
+ id='directory'
+ label={
+ <FormattedMessage
+ id='admin.compliance.directoryTitle'
+ defaultMessage='Compliance Directory Location:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.sql.maxOpenExample', 'Ex "10"')}
+ helpText={
+ <FormattedMessage
+ id='admin.compliance.directoryDescription'
+ defaultMessage='Directory to which compliance reports are written. If blank, will be set to ./data/.'
+ />
+ }
+ value={this.state.directory}
+ onChange={this.handleChange}
+ disabled={!licenseEnabled || !this.state.enable}
+ />
+ <BooleanSetting
+ id='enableDaily'
+ label={
+ <FormattedMessage
+ id='admin.compliance.enableDailyTitle'
+ defaultMessage='Enable Daily Report:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.compliance.enableDailyDesc'
+ defaultMessage='When true, Mattermost will generate a daily compliance report.'
+ />
+ }
+ value={this.state.enableDaily}
+ onChange={this.handleChange}
+ disabled={!licenseEnabled || !this.state.enable}
+ />
+ </SettingsGroup>
);
}
-}
-
-ComplianceSettings.propTypes = {
- config: React.PropTypes.object
-};
-
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/configuration_settings.jsx b/webapp/components/admin_console/configuration_settings.jsx
new file mode 100644
index 000000000..9521ed22c
--- /dev/null
+++ b/webapp/components/admin_console/configuration_settings.jsx
@@ -0,0 +1,76 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+import ReloadConfigButton from './reload_config.jsx';
+
+export default class ConfigurationSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ listenAddress: props.config.ServiceSettings.ListenAddress
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.ListenAddress = this.state.listenAddress;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.general.title'
+ defaultMessage='General Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.general.configuration'
+ defaultMessage='Configuration'
+ />
+ }
+ >
+ <ReloadConfigButton/>
+ <TextSetting
+ id='listenAddress'
+ label={
+ <FormattedMessage
+ id='admin.service.listenAddress'
+ defaultMessage='Listen Address:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.listenExample', 'Ex ":8065"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.listenDescription'
+ defaultMessage='The address to which to bind and listen. Entering ":8065" will bind to all interfaces or you can choose one like "127.0.0.1:8065". Changing this will require a server restart before taking effect.'
+ />
+ }
+ value={this.state.listenAddress}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/connection_security_dropdown_setting.jsx b/webapp/components/admin_console/connection_security_dropdown_setting.jsx
index 02b56b192..b3e9ac31c 100644
--- a/webapp/components/admin_console/connection_security_dropdown_setting.jsx
+++ b/webapp/components/admin_console/connection_security_dropdown_setting.jsx
@@ -8,63 +8,62 @@ import DropdownSetting from './dropdown_setting.jsx';
import {FormattedMessage} from 'react-intl';
const CONNECTION_SECURITY_HELP_TEXT = (
- <div className='help-text'>
- <table
- className='table table-bordered table-margin--none'
- cellPadding='5'
- >
- <tbody>
- <tr>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityNone'
- defaultMessage='None'
- />
- </td>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityNoneDescription'
- defaultMessage='Mattermost will connect over an unsecure connection.'
- />
- </td>
- </tr>
- <tr>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityTls'
- defaultMessage='TLS'
- />
- </td>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityTlsDescription'
- defaultMessage='Encrypts the communication between Mattermost and your server.'
- />
- </td>
- </tr>
- <tr>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityStart'
- defaultMessage='STARTTLS'
- />
- </td>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityStartDescription'
- defaultMessage='Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'
- />
- </td>
- </tr>
- </tbody>
- </table>
- </div>
+ <table
+ className='table table-bordered table-margin--none'
+ cellPadding='5'
+ >
+ <tbody>
+ <tr>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityNone'
+ defaultMessage='None'
+ />
+ </td>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityNoneDescription'
+ defaultMessage='Mattermost will connect over an unsecure connection.'
+ />
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityTls'
+ defaultMessage='TLS'
+ />
+ </td>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityTlsDescription'
+ defaultMessage='Encrypts the communication between Mattermost and your server.'
+ />
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityStart'
+ defaultMessage='STARTTLS'
+ />
+ </td>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityStartDescription'
+ defaultMessage='Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
);
export default class ConnectionSecurityDropdownSetting extends React.Component {
render() {
return (
<DropdownSetting
+ id='connectionSecurity'
values={[
{value: '', text: Utils.localizeMessage('admin.connectionSecurityNone', 'None')},
{value: 'TLS', text: Utils.localizeMessage('admin.connectionSecurityTls', 'TLS (Recommended)')},
@@ -76,11 +75,10 @@ export default class ConnectionSecurityDropdownSetting extends React.Component {
defaultMessage='Connection Security:'
/>
}
- currentValue={this.props.currentValue}
- handleChange={this.props.handleChange}
- isDisabled={this.props.isDisabled}
+ value={this.props.value}
+ onChange={this.props.onChange}
+ disabled={this.props.disabled}
helpText={CONNECTION_SECURITY_HELP_TEXT}
- margin='small'
/>
);
}
@@ -89,7 +87,7 @@ ConnectionSecurityDropdownSetting.defaultProps = {
};
ConnectionSecurityDropdownSetting.propTypes = {
- currentValue: React.PropTypes.string.isRequired,
- handleChange: React.PropTypes.func.isRequired,
- isDisabled: React.PropTypes.bool.isRequired
+ value: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool.isRequired
};
diff --git a/webapp/components/admin_console/connection_settings.jsx b/webapp/components/admin_console/connection_settings.jsx
new file mode 100644
index 000000000..59b32ec23
--- /dev/null
+++ b/webapp/components/admin_console/connection_settings.jsx
@@ -0,0 +1,94 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class ConnectionSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ allowCorsFrom: props.config.ServiceSettings.AllowCorsFrom,
+ enableInsecureOutgoingConnections: props.config.ServiceSettings.EnableInsecureOutgoingConnections
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.AllowCorsFrom = this.state.allowCorsFrom;
+ config.ServiceSettings.EnableInsecureOutgoingConnections = this.state.enableInsecureOutgoingConnections;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.security.title'
+ defaultMessage='Security Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.security.connection'
+ defaultMessage='Connections'
+ />
+ }
+ >
+ <TextSetting
+ id='allowCorsFrom'
+ label={
+ <FormattedMessage
+ id='admin.service.corsTitle'
+ defaultMessage='Allow Cross-origin Requests from:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.corsEx', 'http://example.com')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.corsDescription'
+ defaultMessage='Enable HTTP Cross origin request from a specific domain. Use "*" if you want to allow CORS from any domain or leave it blank to disable it.'
+ />
+ }
+ value={this.state.allowCorsFrom}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableInsecureOutgoingConnections'
+ label={
+ <FormattedMessage
+ id='admin.service.insecureTlsTitle'
+ defaultMessage='Enable Insecure Outgoing Connections: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.insecureTlsDesc'
+ defaultMessage='When true, any outgoing HTTPS requests will accept unverified, self-signed certificates. For example, outgoing webhooks to a server with a self-signed TLS certificate, using any domain, will be allowed. Note that this makes these connections susceptible to man-in-the-middle attacks.'
+ />
+ }
+ value={this.state.enableInsecureOutgoingConnections}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/custom_brand_settings.jsx b/webapp/components/admin_console/custom_brand_settings.jsx
new file mode 100644
index 000000000..307bbad8c
--- /dev/null
+++ b/webapp/components/admin_console/custom_brand_settings.jsx
@@ -0,0 +1,137 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import BrandImageSetting from './brand_image_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class CustomBrandSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ siteName: props.config.TeamSettings.SiteName,
+ enableCustomBrand: props.config.TeamSettings.EnableCustomBrand,
+ customBrandText: props.config.TeamSettings.CustomBrandText
+ });
+ }
+
+ getConfigFromState(config) {
+ config.TeamSettings.SiteName = this.state.siteName;
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true') {
+ config.TeamSettings.EnableCustomBrand = this.state.enableCustomBrand;
+ config.TeamSettings.CustomBrandText = this.state.customBrandText;
+ }
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.customization.title'
+ defaultMessage='Customization Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ const enterpriseSettings = [];
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true') {
+ enterpriseSettings.push(
+ <BooleanSetting
+ key='enableCustomBrand'
+ id='enableCustomBrand'
+ label={
+ <FormattedMessage
+ id='admin.team.brandTitle'
+ defaultMessage='Enable Custom Branding: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.brandDesc'
+ defaultMessage='Enable custom branding to show an image of your choice, uploaded below, and some help text, written below, on the login page.'
+ />
+ }
+ value={this.state.enableCustomBrand}
+ onChange={this.handleChange}
+ />
+ );
+
+ enterpriseSettings.push(
+ <BrandImageSetting
+ key='customBrandImage'
+ disabled={!this.state.enableCustomBrand}
+ />
+ );
+
+ enterpriseSettings.push(
+ <TextSetting
+ key='customBrandText'
+ id='customBrandText'
+ type='textarea'
+ label={
+ <FormattedMessage
+ id='admin.team.brandTextTitle'
+ defaultMessage='Custom Brand Text:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.brandTextDescription'
+ defaultMessage='The custom branding Markdown-formatted text you would like to appear below your custom brand image on your login sreen.'
+ />
+ }
+ value={this.state.customBrandText}
+ onChange={this.handleChange}
+ disabled={!this.state.enableCustomBrand}
+ />
+ );
+ }
+
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.customization.customBrand'
+ defaultMessage='Custom Branding'
+ />
+ }
+ >
+ <TextSetting
+ id='siteName'
+ label={
+ <FormattedMessage
+ id='admin.team.siteNameTitle'
+ defaultMessage='Site Name:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.team.siteNameExample', 'Ex "Mattermost"')}
+ helpText={
+ <FormattedMessage
+ id='admin.team.siteNameDescription'
+ defaultMessage='Name of service shown in login screens and UI.'
+ />
+ }
+ value={this.state.siteName}
+ onChange={this.handleChange}
+ />
+ {enterpriseSettings}
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/database_settings.jsx b/webapp/components/admin_console/database_settings.jsx
new file mode 100644
index 000000000..97a6b692c
--- /dev/null
+++ b/webapp/components/admin_console/database_settings.jsx
@@ -0,0 +1,194 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import GeneratedSetting from './generated_setting.jsx';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+import RecycleDbButton from './recycle_db.jsx';
+
+export default class DatabaseSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ driverName: this.props.config.SqlSettings.DriverName,
+ dataSource: this.props.config.SqlSettings.DataSource,
+ dataSourceReplicas: this.props.config.SqlSettings.DataSourceReplicas,
+ maxIdleConns: props.config.SqlSettings.MaxIdleConns,
+ maxOpenConns: props.config.SqlSettings.MaxOpenConns,
+ atRestEncryptKey: props.config.SqlSettings.AtRestEncryptKey,
+ trace: props.config.SqlSettings.Trace
+ });
+ }
+
+ getConfigFromState(config) {
+ // driverName, dataSource, and dataSourceReplicas are read-only from the UI
+
+ config.SqlSettings.MaxIdleConns = this.parseIntNonZero(this.state.maxIdleConns);
+ config.SqlSettings.MaxOpenConns = this.parseIntNonZero(this.state.maxOpenConns);
+ config.SqlSettings.AtRestEncryptKey = this.state.atRestEncryptKey;
+ config.SqlSettings.Trace = this.state.trace;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.database.title'
+ defaultMessage='Database Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ const dataSource = '**********' + this.state.dataSource.substring(this.state.dataSource.indexOf('@'));
+
+ let dataSourceReplicas = '';
+ this.state.dataSourceReplicas.forEach((replica) => {
+ dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] ';
+ });
+
+ if (this.state.dataSourceReplicas.length === 0) {
+ dataSourceReplicas = 'none';
+ }
+
+ return (
+ <SettingsGroup>
+ <p>
+ <FormattedMessage
+ id='admin.sql.noteDescription'
+ defaultMessage='Changing properties in this section will require a server restart before taking effect.'
+ />
+ </p>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DriverName'
+ >
+ <FormattedMessage
+ id='admin.sql.driverName'
+ defaultMessage='Driver Name:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{this.state.driverName}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSource'
+ >
+ <FormattedMessage
+ id='admin.sql.dataSource'
+ defaultMessage='Data Source:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSource}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSourceReplicas'
+ >
+ <FormattedMessage
+ id='admin.sql.replicas'
+ defaultMessage='Data Source Replicas:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSourceReplicas}</p>
+ </div>
+ </div>
+ <TextSetting
+ id='maxIdleConns'
+ label={
+ <FormattedMessage
+ id='admin.sql.maxConnectionsTitle'
+ defaultMessage='Maximum Idle Connections:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.sql.maxConnectionsExample', 'Ex "10"')}
+ helpText={
+ <FormattedMessage
+ id='admin.sql.maxConnectionsDescription'
+ defaultMessage='Maximum number of idle connections held open to the database.'
+ />
+ }
+ value={this.state.maxIdleConns}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='maxOpenConns'
+ label={
+ <FormattedMessage
+ id='admin.sql.maxOpenTitle'
+ defaultMessage='Maximum Open Connections:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.sql.maxOpenExample', 'Ex "10"')}
+ helpText={
+ <FormattedMessage
+ id='admin.sql.maxOpenDescription'
+ defaultMessage='Maximum number of open connections held open to the database.'
+ />
+ }
+ value={this.state.maxOpenConns}
+ onChange={this.handleChange}
+ />
+ <GeneratedSetting
+ id='atRestEncryptKey'
+ label={
+ <FormattedMessage
+ id='admin.sql.keyTitle'
+ defaultMessage='At Rest Encrypt Key:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.sql.keyExample', 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"')}
+ helpText={
+ <FormattedMessage
+ id='admin.sql.keyDescription'
+ defaultMessage='32-character salt available to encrypt and decrypt sensitive fields in database.'
+ />
+ }
+ value={this.state.atRestEncryptKey}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='trace'
+ label={
+ <FormattedMessage
+ id='admin.sql.traceTitle'
+ defaultMessage='Trace: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.sql.traceDescription'
+ defaultMessage='(Development Mode) When true, executing SQL statements are written to the log.'
+ />
+ }
+ value={this.state.trace}
+ onChange={this.handleChange}
+ />
+ <RecycleDbButton/>
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/developer_settings.jsx b/webapp/components/admin_console/developer_settings.jsx
new file mode 100644
index 000000000..9b153ed26
--- /dev/null
+++ b/webapp/components/admin_console/developer_settings.jsx
@@ -0,0 +1,83 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+
+export default class DeveloperSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enableTesting: props.config.ServiceSettings.EnableTesting,
+ enableDeveloper: props.config.ServiceSettings.EnableDeveloper
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.EnableTesting = this.state.enableTesting;
+ config.ServiceSettings.EnableDeveloper = this.state.enableDeveloper;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.developer.title'
+ defaultMessage='Developer Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup>
+ <BooleanSetting
+ id='enableTesting'
+ label={
+ <FormattedMessage
+ id='admin.service.testingTitle'
+ defaultMessage='Enable Testing: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.testingDescription'
+ defaultMessage='(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.'
+ />
+ }
+ value={this.state.enableTesting}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableDeveloper'
+ label={
+ <FormattedMessage
+ id='admin.service.developerTitle'
+ defaultMessage='Enable Developer Mode: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.developerDesc'
+ defaultMessage='(Developer Option) When true, extra information around errors will be displayed in the UI.'
+ />
+ }
+ value={this.state.enableDeveloper}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/dropdown_setting.jsx b/webapp/components/admin_console/dropdown_setting.jsx
index fca8dd170..cf733ec90 100644
--- a/webapp/components/admin_console/dropdown_setting.jsx
+++ b/webapp/components/admin_console/dropdown_setting.jsx
@@ -6,6 +6,16 @@ import React from 'react';
import Setting from './setting.jsx';
export default class DropdownSetting extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value);
+ }
+
render() {
const options = [];
for (const {value, text} of this.props.values) {
@@ -22,30 +32,33 @@ export default class DropdownSetting extends React.Component {
return (
<Setting
label={this.props.label}
- margin={this.props.margin}
+ inputId={this.props.id}
+ helpText={this.props.helpText}
>
<select
className='form-control'
- value={this.props.currentValue}
- onChange={this.props.handleChange}
- disabled={this.props.isDisabled}
+ id={this.props.id}
+ value={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
>
{options}
</select>
- {this.props.helpText}
</Setting>
);
}
}
+
DropdownSetting.defaultProps = {
+ isDisabled: false
};
DropdownSetting.propTypes = {
+ id: React.PropTypes.string.isRequired,
values: React.PropTypes.array.isRequired,
label: React.PropTypes.node.isRequired,
- currentValue: React.PropTypes.string.isRequired,
- handleChange: React.PropTypes.func.isRequired,
- isDisabled: React.PropTypes.bool.isRequired,
- helpText: React.PropTypes.node.isRequired,
- margin: React.PropTypes.oneOf(['', 'small'])
+ value: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
+ helpText: React.PropTypes.node
};
diff --git a/webapp/components/admin_console/email_authentication_settings.jsx b/webapp/components/admin_console/email_authentication_settings.jsx
new file mode 100644
index 000000000..2f5c423bf
--- /dev/null
+++ b/webapp/components/admin_console/email_authentication_settings.jsx
@@ -0,0 +1,109 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+
+export default class EmailAuthenticationSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enableSignUpWithEmail: props.config.EmailSettings.EnableSignUpWithEmail,
+ enableSignInWithEmail: props.config.EmailSettings.EnableSignInWithEmail,
+ enableSignInWithUsername: props.config.EmailSettings.EnableSignInWithUsername
+ });
+ }
+
+ getConfigFromState(config) {
+ config.EmailSettings.EnableSignUpWithEmail = this.state.enableSignUpWithEmail;
+ config.EmailSettings.EnableSignInWithEmail = this.state.enableSignInWithEmail;
+ config.EmailSettings.EnableSignInWithUsername = this.state.enableSignInWithUsername;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.authentication.title'
+ defaultMessage='Authentication Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.authentication.email'
+ defaultMessage='Email'
+ />
+ }
+ >
+ <BooleanSetting
+ id='enableSignUpWithEmail'
+ label={
+ <FormattedMessage
+ id='admin.email.allowSignupTitle'
+ defaultMessage='Allow Sign Up With Email: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.allowSignupDescription'
+ defaultMessage='When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.'
+ />
+ }
+ value={this.state.enableSignUpWithEmail}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableSignInWithEmail'
+ label={
+ <FormattedMessage
+ id='admin.email.allowEmailSignInTitle'
+ defaultMessage='Allow Sign In With Email: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.allowEmailSignInDescription'
+ defaultMessage='When true, Mattermost allows users to sign in using their email and password.'
+ />
+ }
+ value={this.state.enableSignInWithEmail}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableSignInWithUsername'
+ label={
+ <FormattedMessage
+ id='admin.email.allowUsernameSignInTitle'
+ defaultMessage='Allow Sign In With Username: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.allowUsernameSignInDescription'
+ defaultMessage='When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.'
+ />
+ }
+ value={this.state.enableSignInWithUsername}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/email_connection_test.jsx b/webapp/components/admin_console/email_connection_test.jsx
new file mode 100644
index 000000000..87612e4d5
--- /dev/null
+++ b/webapp/components/admin_console/email_connection_test.jsx
@@ -0,0 +1,118 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Client from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class EmailConnectionTestButton extends React.Component {
+ static get propTypes() {
+ return {
+ config: React.PropTypes.object.isRequired,
+ disabled: React.PropTypes.bool.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleTestConnection = this.handleTestConnection.bind(this);
+
+ this.state = {
+ testing: false,
+ success: false,
+ fail: null
+ };
+ }
+
+ handleTestConnection(e) {
+ e.preventDefault();
+
+ this.setState({
+ testing: true,
+ success: false,
+ fail: null
+ });
+
+ Client.testEmail(
+ this.props.config,
+ () => {
+ this.setState({
+ testing: false,
+ success: true
+ });
+ },
+ (err) => {
+ this.setState({
+ testing: false,
+ fail: err.message + ' - ' + err.detailed_error
+ });
+ }
+ );
+ }
+
+ render() {
+ let testMessage = null;
+ if (this.state.success) {
+ testMessage = (
+ <div className='alert alert-success'>
+ <i className='fa fa-check'></i>
+ <FormattedMessage
+ id='admin.email.emailSuccess'
+ defaultMessage='No errors were reported while sending an email. Please check your inbox to make sure.'
+ />
+ </div>
+ );
+ } else if (this.state.fail) {
+ testMessage = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-warning'></i>
+ <FormattedMessage
+ id='admin.email.emailFail'
+ defaultMessage='Connection unsuccessful: {error}'
+ values={{
+ error: this.state.fail
+ }}
+ />
+ </div>
+ );
+ }
+
+ let contents = null;
+ if (this.state.testing) {
+ contents = (
+ <span>
+ <span className='glyphicon glyphicon-refresh glyphicon-refresh-animate'/>
+ {Utils.localizeMessage('admin.email.testing', 'Testing...')}
+ </span>
+ );
+ } else {
+ contents = (
+ <FormattedMessage
+ id='admin.email.connectionSecurityTest'
+ defaultMessage='Test Connection'
+ />
+ );
+ }
+
+ return (
+ <div className='form-group email-connection-test'>
+ <div className='col-sm-offset-4 col-sm-8'>
+ <div className='help-text'>
+ <button
+ className='btn btn-default'
+ onClick={this.handleTestConnection}
+ disabled={this.props.disabled}
+ >
+ {contents}
+ </button>
+ {testMessage}
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/email_settings.jsx b/webapp/components/admin_console/email_settings.jsx
index dcdf52486..5067b562b 100644
--- a/webapp/components/admin_console/email_settings.jsx
+++ b/webapp/components/admin_console/email_settings.jsx
@@ -1,1052 +1,232 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import crypto from 'crypto';
-import ConnectionSecurityDropdownSetting from './connection_security_dropdown_setting.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import React from 'react';
import * as Utils from 'utils/utils.jsx';
-import Constants from 'utils/constants.jsx';
-var holders = defineMessages({
- notificationDisplayExample: {
- id: 'admin.email.notificationDisplayExample',
- defaultMessage: 'Ex: "Mattermost Notification", "System", "No-Reply"'
- },
- notificationEmailExample: {
- id: 'admin.email.notificationEmailExample',
- defaultMessage: 'Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"'
- },
- smtpUsernameExample: {
- id: 'admin.email.smtpUsernameExample',
- defaultMessage: 'Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"'
- },
- smtpPasswordExample: {
- id: 'admin.email.smtpPasswordExample',
- defaultMessage: 'Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
- },
- smtpServerExample: {
- id: 'admin.email.smtpServerExample',
- defaultMessage: 'Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"'
- },
- smtpPortExample: {
- id: 'admin.email.smtpPortExample',
- defaultMessage: 'Ex: "25", "465"'
- },
- inviteSaltExample: {
- id: 'admin.email.inviteSaltExample',
- defaultMessage: 'Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
- },
- passwordSaltExample: {
- id: 'admin.email.passwordSaltExample',
- defaultMessage: 'Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
- },
- testing: {
- id: 'admin.email.testing',
- defaultMessage: 'Testing...'
- },
- saving: {
- id: 'admin.email.saving',
- defaultMessage: 'Saving Config...'
- }
-});
-
-import React from 'react';
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import ConnectionSecurityDropdownSetting from './connection_security_dropdown_setting.jsx';
+import EmailConnectionTest from './email_connection_test.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-class EmailSettings extends React.Component {
+export default class EmailSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleTestConnection = this.handleTestConnection.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.buildConfig = this.buildConfig.bind(this);
- this.handleGenerateInvite = this.handleGenerateInvite.bind(this);
- this.handleGenerateReset = this.handleGenerateReset.bind(this);
- this.handleSendPushNotificationsChange = this.handleSendPushNotificationsChange.bind(this);
- this.handlePushServerChange = this.handlePushServerChange.bind(this);
- this.handleAgreeChange = this.handleAgreeChange.bind(this);
-
- let sendNotificationValue;
- let agree = false;
- if (!props.config.EmailSettings.SendPushNotifications) {
- sendNotificationValue = 'off';
- } else if (props.config.EmailSettings.PushNotificationServer === Constants.MHPNS && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MHPNS === 'true') {
- sendNotificationValue = 'mhpns';
- agree = true;
- } else if (props.config.EmailSettings.PushNotificationServer === Constants.MTPNS) {
- sendNotificationValue = 'mtpns';
- } else {
- sendNotificationValue = 'self';
- }
-
- let pushNotificationServer = this.props.config.EmailSettings.PushNotificationServer;
- if (sendNotificationValue === 'mtpns') {
- pushNotificationServer = Constants.MTPNS;
- } else if (sendNotificationValue === 'mhpns') {
- pushNotificationServer = Constants.MHPNS;
- }
-
- this.state = {
- sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
- sendPushNotifications: this.props.config.EmailSettings.SendPushNotifications,
- saveNeeded: false,
- serverError: null,
- emailSuccess: null,
- emailFail: null,
- pushNotificationContents: this.props.config.EmailSettings.PushNotificationContents,
- connectionSecurity: this.props.config.EmailSettings.ConnectionSecurity,
- sendNotificationValue,
- pushNotificationServer,
- agree
- };
- }
-
- handleChange(action) {
- const s = {saveNeeded: true};
-
- if (action === 'sendEmailNotifications_true') {
- s.sendEmailNotifications = true;
- }
-
- if (action === 'sendEmailNotifications_false') {
- s.sendEmailNotifications = false;
- }
-
- if (action === 'sendPushNotifications_true') {
- s.sendPushNotifications = true;
- }
-
- if (action === 'sendPushNotifications_false') {
- s.sendPushNotifications = false;
- }
-
- this.setState(s);
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ sendEmailNotifications: props.config.EmailSettings.SendEmailNotifications,
+ feedbackName: props.config.EmailSettings.FeedbackName,
+ feedbackEmail: props.config.EmailSettings.FeedbackEmail,
+ smtpUsername: props.config.EmailSettings.SMTPUsername,
+ smtpPassword: props.config.EmailSettings.SMTPPassword,
+ smtpServer: props.config.EmailSettings.SMTPServer,
+ smtpPort: props.config.EmailSettings.SMTPPort,
+ connectionSecurity: props.config.EmailSettings.ConnectionSecurity,
+ enableSecurityFixAlert: props.config.ServiceSettings.EnableSecurityFixAlert
+ });
}
- buildConfig() {
- const config = this.props.config;
- config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
- config.EmailSettings.EnableSignInWithEmail = ReactDOM.findDOMNode(this.refs.allowSignInWithEmail).checked;
- config.EmailSettings.EnableSignInWithUsername = ReactDOM.findDOMNode(this.refs.allowSignInWithUsername).checked;
- config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
- config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
- config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim();
- config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim();
- config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim();
- config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim();
- config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim();
- config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim();
- config.EmailSettings.ConnectionSecurity = this.state.connectionSecurity.trim();
-
- config.EmailSettings.InviteSalt = ReactDOM.findDOMNode(this.refs.InviteSalt).value.trim();
- if (config.EmailSettings.InviteSalt === '') {
- config.EmailSettings.InviteSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
- ReactDOM.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt;
- }
-
- config.EmailSettings.PasswordResetSalt = ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value.trim();
- if (config.EmailSettings.PasswordResetSalt === '') {
- config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
- ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt;
- }
-
- const sendPushNotifications = this.refs.sendPushNotifications.value;
- if (sendPushNotifications === 'off') {
- config.EmailSettings.SendPushNotifications = false;
- } else {
- config.EmailSettings.SendPushNotifications = true;
- }
-
- if (this.refs.PushNotificationServer) {
- config.EmailSettings.PushNotificationServer = this.refs.PushNotificationServer.value.trim();
- }
-
- if (this.refs.PushNotificationContents) {
- config.EmailSettings.PushNotificationContents = this.refs.PushNotificationContents.value;
- }
+ getConfigFromState(config) {
+ config.EmailSettings.SendEmailNotifications = this.state.sendEmailNotifications;
+ config.EmailSettings.FeedbackName = this.state.feedbackName;
+ config.EmailSettings.FeedbackEmail = this.state.feedbackEmail;
+ config.EmailSettings.SMTPUsername = this.state.smtpUsername;
+ config.EmailSettings.SMTPPassword = this.state.smtpPassword;
+ config.EmailSettings.SMTPServer = this.state.smtpServer;
+ config.EmailSettings.SMTPPort = this.state.smtpPort;
+ config.EmailSettings.ConnectionSecurity = this.state.connectionSecurity;
+ config.ServiceSettings.EnableSecurityFixAlert = this.state.enableSecurityFixAlert;
return config;
}
- handleSendPushNotificationsChange(e) {
- const sendNotificationValue = e.target.value;
- let pushNotificationServer = this.state.pushNotificationServer;
- if (sendNotificationValue === 'mtpns') {
- pushNotificationServer = Constants.MTPNS;
- } else if (sendNotificationValue === 'mhpns') {
- pushNotificationServer = Constants.MHPNS;
- }
- this.setState({saveNeeded: true, sendNotificationValue, pushNotificationServer, agree: false});
- }
-
- handlePushServerChange(e) {
- this.setState({saveNeeded: true, pushNotificationServer: e.target.value});
- }
-
- handleAgreeChange(e) {
- this.setState({agree: e.target.checked});
- }
-
- handleGenerateInvite(e) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
- }
-
- handleGenerateReset(e) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
- }
-
- handleTestConnection(e) {
- e.preventDefault();
- $('#connection-button').button('loading');
-
- var config = this.buildConfig();
-
- Client.testEmail(
- config,
- () => {
- this.setState({
- sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
- serverError: null,
- saveNeeded: true,
- emailSuccess: true,
- emailFail: null
- });
- $('#connection-button').button('reset');
- },
- (err) => {
- this.setState({
- sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
- serverError: null,
- saveNeeded: true,
- emailSuccess: null,
- emailFail: err.message + ' - ' + err.detailed_error
- });
- $('#connection-button').button('reset');
- }
- );
- }
-
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.buildConfig();
-
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
- serverError: null,
- saveNeeded: false,
- emailSuccess: null,
- emailFail: null
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
- serverError: err.message,
- saveNeeded: true,
- emailSuccess: null,
- emailFail: null
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.notifications.title'
+ defaultMessage='Notification Settings'
+ />
+ </h3>
);
}
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
- var emailSuccess = '';
- if (this.state.emailSuccess) {
- emailSuccess = (
- <div className='alert alert-success'>
- <i className='fa fa-check'></i>
- <FormattedMessage
- id='admin.email.emailSuccess'
- defaultMessage='No errors were reported while sending an email. Please check your inbox to make sure.'
- />
- </div>
- );
- }
-
- var emailFail = '';
- if (this.state.emailFail) {
- emailSuccess = (
- <div className='alert alert-warning'>
- <i className='fa fa-warning'></i>
- <FormattedMessage
- id='admin.email.emailFail'
- defaultMessage='Connection unsuccessful: {error}'
- values={{
- error: this.state.emailFail
- }}
- />
- </div>
- );
- }
-
- let mhpnsOption;
- if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MHPNS === 'true') {
- mhpnsOption = <option value='mhpns'>{Utils.localizeMessage('admin.email.mhpns', 'Use encrypted, production-quality HPNS connection to iOS and Android apps')}</option>;
- }
-
- let disableSave = !this.state.saveNeeded;
-
- let tosCheckbox;
- if (this.state.sendNotificationValue === 'mhpns') {
- tosCheckbox = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- >
- {''}
- </label>
- <div className='col-sm-8'>
- <input
- type='checkbox'
- ref='agree'
- checked={this.state.agree}
- onChange={this.handleAgreeChange}
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.notifications.email'
+ defaultMessage='Email'
+ />
+ }
+ >
+ <BooleanSetting
+ id='sendEmailNotifications'
+ label={
+ <FormattedMessage
+ id='admin.email.notificationsTitle'
+ defaultMessage='Send Email Notifications: '
/>
+ }
+ helpText={
<FormattedHTMLMessage
- id='admin.email.agreeHPNS'
- defaultMessage=' I understand and accept the Mattermost Hosted Push Notification Service <a href="https://about.mattermost.com/hpns-terms/" target="_blank">Terms of Service</a> and <a href="https://about.mattermost.com/hpns-privacy/" target="_blank">Privacy Policy</a>.'
+ id='admin.email.notificationsDescription'
+ defaultMessage='Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).'
/>
- </div>
- </div>
- );
-
- disableSave = disableSave || !this.state.agree;
- }
-
- let sendHelpText;
- let pushServerHelpText;
- if (this.state.sendNotificationValue === 'off') {
- sendHelpText = (
- <FormattedHTMLMessage
- id='admin.email.pushOffHelp'
- defaultMessage='Please see <a href="http://docs.mattermost.com/deployment/push.html#push-notifications-and-mobile-devices" target="_blank">documentation on push notifications</a> to learn more about setup options.'
+ }
+ value={this.state.sendEmailNotifications}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='feedbackName'
+ label={
+ <FormattedMessage
+ id='admin.email.notificationDisplayTitle'
+ defaultMessage='Notification Display Name:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.email.notificationDisplayExample', 'Ex: "Mattermost Notification", "System", "No-Reply"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.notificationDisplayDescription'
+ defaultMessage='Display name on email account used when sending notification emails from Mattermost.'
+ />
+ }
+ value={this.state.feedbackName}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
- );
- } else if (this.state.sendNotificationValue === 'mhpns') {
- pushServerHelpText = (
- <FormattedHTMLMessage
- id='admin.email.mhpnsHelp'
- defaultMessage='Download <a href="https://itunes.apple.com/us/app/mattermost/id984966508?mt=8" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns" target="_blank">Mattermost Hosted Push Notification Service</a>.'
+ <TextSetting
+ id='feedbackEmail'
+ label={
+ <FormattedMessage
+ id='admin.email.notificationEmailTitle'
+ defaultMessage='Notification Email Address:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.email.notificationEmailExample', 'Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.notificationEmailDescription'
+ defaultMessage='Email address displayed on email account used when sending notification emails from Mattermost.'
+ />
+ }
+ value={this.state.feedbackEmail}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
- );
- } else if (this.state.sendNotificationValue === 'mtpns') {
- pushServerHelpText = (
- <FormattedHTMLMessage
- id='admin.email.mtpnsHelp'
- defaultMessage='Download <a href="https://itunes.apple.com/us/app/mattermost/id984966508?mt=8" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns" target="_blank">Mattermost Test Push Notification Service</a>.'
+ <TextSetting
+ id='smtpUsername'
+ label={
+ <FormattedMessage
+ id='admin.email.smtpUsernameTitle'
+ defaultMessage='SMTP Username:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.email.smtpUsernameExample', 'Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.smtpUsernameDescription'
+ defaultMessage=' Obtain this credential from administrator setting up your email server.'
+ />
+ }
+ value={this.state.smtpUsername}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
- );
- } else {
- pushServerHelpText = (
- <FormattedHTMLMessage
- id='admin.email.easHelp'
- defaultMessage='Learn more about compiling and deploying your own mobile apps from an <a href="http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas" target="_blank">Enterprise App Store</a>.'
+ <TextSetting
+ id='smtpPassword'
+ label={
+ <FormattedMessage
+ id='admin.email.smtpPasswordTitle'
+ defaultMessage='SMTP Password:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.email.smtpPasswordExample', 'Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.smtpPasswordDescription'
+ defaultMessage=' Obtain this credential from administrator setting up your email server.'
+ />
+ }
+ value={this.state.smtpPassword}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
- );
- }
-
- const sendPushNotifications = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='sendPushNotifications'
- >
- <FormattedMessage
- id='admin.email.pushTitle'
- defaultMessage='Send Push Notifications: '
- />
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='sendPushNotifications'
- ref='sendPushNotifications'
- value={this.state.sendNotificationValue}
- onChange={this.handleSendPushNotificationsChange}
- >
- <option value='off'>{Utils.localizeMessage('admin.email.pushOff', 'Do not send push notifications')}</option>
- {mhpnsOption}
- <option value='mtpns'>{Utils.localizeMessage('admin.email.mtpns', 'Use iOS and Android apps on iTunes and Google Play with TPNS')}</option>
- <option value='self'>{Utils.localizeMessage('admin.email.selfPush', 'Manually enter Push Notification Service location')}</option>
- </select>
- <p className='help-text'>
- {sendHelpText}
- </p>
- </div>
- </div>
- );
-
- let pushNotificationServer;
- let pushNotificationContent;
- if (this.state.sendNotificationValue !== 'off') {
- pushNotificationServer = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PushNotificationServer'
- >
+ <TextSetting
+ id='smtpServer'
+ label={
<FormattedMessage
- id='admin.email.pushServerTitle'
- defaultMessage='Push Notification Server:'
+ id='admin.email.smtpServerTitle'
+ defaultMessage='SMTP Server:'
/>
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PushNotificationServer'
- ref='PushNotificationServer'
- placeholder={Utils.localizeMessage('admin.email.pushServerEx', 'E.g.: "http://push-test.mattermost.com"')}
- value={this.state.pushNotificationServer}
- onChange={this.handlePushServerChange}
- disabled={this.state.sendNotificationValue !== 'self'}
+ }
+ placeholder={Utils.localizeMessage('admin.email.smtpServerExample', 'Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.smtpServerDescription'
+ defaultMessage='Location of SMTP email server.'
/>
- <p className='help-text'>
- {pushServerHelpText}
- </p>
- </div>
- </div>
- );
-
- pushNotificationContent = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='pushNotificationContents'
- >
+ }
+ value={this.state.smtpServer}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <TextSetting
+ id='smtpPort'
+ label={
<FormattedMessage
- id='admin.email.pushContentTitle'
- defaultMessage='Push Notification Contents:'
+ id='admin.email.smtpPortTitle'
+ defaultMessage='SMTP Port:'
/>
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='pushNotificationContents'
- ref='PushNotificationContents'
- defaultValue={this.props.config.EmailSettings.PushNotificationContents}
- onChange={this.handleChange.bind(this, 'pushNotificationContents')}
- >
- <option value='generic'>{Utils.localizeMessage('admin.email.genericPushNotification', 'Send generic description with user and channel names')}</option>
- <option value='full'>{Utils.localizeMessage('admin.email.fullPushNotification', 'Send full message snippet')}</option>
- </select>
- <p className='help-text'>
- <FormattedHTMLMessage
- id='admin.email.pushContentDesc'
- defaultMessage='Selecting "Send generic description with user and channel names" provides push notifications with generic messages, including names of users and channels but no specific details from the message text.<br /><br />
- Selecting "Send full message snippet" sends excerpts from messages triggering notifications with specifics and may include confidential information sent in messages. If your Push Notification Service is outside your firewall, it is HIGHLY RECOMMENDED this option only be used with an "https" protocol to encrypt the connection.'
- />
- </p>
- </div>
- </div>
- );
- }
-
- return (
- <div className='wrapper--fixed'>
- <h3>
- <FormattedMessage
- id='admin.email.emailSettings'
- defaultMessage='Email Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='allowSignUpWithEmail'
- >
- <FormattedMessage
- id='admin.email.allowSignupTitle'
- defaultMessage='Allow Sign Up With Email: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignUpWithEmail'
- value='true'
- ref='allowSignUpWithEmail'
- defaultChecked={this.props.config.EmailSettings.EnableSignUpWithEmail}
- onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_true')}
- />
- <FormattedMessage
- id='admin.email.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignUpWithEmail'
- value='false'
- defaultChecked={!this.props.config.EmailSettings.EnableSignUpWithEmail}
- onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_false')}
- />
- <FormattedMessage
- id='admin.email.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.allowSignupDescription'
- defaultMessage='When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='allowSignInWithEmail'
- >
- <FormattedMessage
- id='admin.email.allowEmailSignInTitle'
- defaultMessage='Allow Sign In With Email: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignInWithEmail'
- value='true'
- ref='allowSignInWithEmail'
- defaultChecked={this.props.config.EmailSettings.EnableSignInWithEmail}
- onChange={this.handleChange.bind(this, 'allowSignInWithEmail_true')}
- />
- <FormattedMessage
- id='admin.email.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignInWithEmail'
- value='false'
- defaultChecked={!this.props.config.EmailSettings.EnableSignInWithEmail}
- onChange={this.handleChange.bind(this, 'allowSignInWithEmail_false')}
- />
- <FormattedMessage
- id='admin.email.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.allowEmailSignInDescription'
- defaultMessage='When true, Mattermost allows users to sign in using their email and password.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='allowSignInWithUsername'
- >
- <FormattedMessage
- id='admin.email.allowUsernameSignInTitle'
- defaultMessage='Allow Sign In With Username: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignInWithUsername'
- value='true'
- ref='allowSignInWithUsername'
- defaultChecked={this.props.config.EmailSettings.EnableSignInWithUsername}
- onChange={this.handleChange.bind(this, 'allowSignInWithUsername_true')}
- />
- <FormattedMessage
- id='admin.email.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignInWithUsername'
- value='false'
- defaultChecked={!this.props.config.EmailSettings.EnableSignInWithUsername}
- onChange={this.handleChange.bind(this, 'allowSignInWithUsername_false')}
- />
- <FormattedMessage
- id='admin.email.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.allowUsernameSignInDescription'
- defaultMessage='When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='sendEmailNotifications'
- >
- <FormattedMessage
- id='admin.email.notificationsTitle'
- defaultMessage='Send Email Notifications: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='sendEmailNotifications'
- value='true'
- ref='sendEmailNotifications'
- defaultChecked={this.props.config.EmailSettings.SendEmailNotifications}
- onChange={this.handleChange.bind(this, 'sendEmailNotifications_true')}
- />
- <FormattedMessage
- id='admin.email.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='sendEmailNotifications'
- value='false'
- defaultChecked={!this.props.config.EmailSettings.SendEmailNotifications}
- onChange={this.handleChange.bind(this, 'sendEmailNotifications_false')}
- />
- <FormattedMessage
- id='admin.email.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedHTMLMessage
- id='admin.email.notificationsDescription'
- defaultMessage='Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='requireEmailVerification'
- >
- <FormattedMessage
- id='admin.email.requireVerificationTitle'
- defaultMessage='Require Email Verification: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='requireEmailVerification'
- value='true'
- ref='requireEmailVerification'
- defaultChecked={this.props.config.EmailSettings.RequireEmailVerification}
- onChange={this.handleChange.bind(this, 'requireEmailVerification_true')}
- disabled={!this.state.sendEmailNotifications}
- />
- <FormattedMessage
- id='admin.email.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='requireEmailVerification'
- value='false'
- defaultChecked={!this.props.config.EmailSettings.RequireEmailVerification}
- onChange={this.handleChange.bind(this, 'requireEmailVerification_false')}
- disabled={!this.state.sendEmailNotifications}
- />
- <FormattedMessage
- id='admin.email.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.requireVerificationDescription'
- defaultMessage='Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='feedbackName'
- >
- <FormattedMessage
- id='admin.email.notificationDisplayTitle'
- defaultMessage='Notification Display Name:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='feedbackName'
- ref='feedbackName'
- placeholder={formatMessage(holders.notificationDisplayExample)}
- defaultValue={this.props.config.EmailSettings.FeedbackName}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.notificationDisplayDescription'
- defaultMessage='Display name on email account used when sending notification emails from Mattermost.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='feedbackEmail'
- >
- <FormattedMessage
- id='admin.email.notificationEmailTitle'
- defaultMessage='Notification Email Address:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='email'
- className='form-control'
- id='feedbackEmail'
- ref='feedbackEmail'
- placeholder={formatMessage(holders.notificationEmailExample)}
- defaultValue={this.props.config.EmailSettings.FeedbackEmail}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.notificationEmailDescription'
- defaultMessage='Email address displayed on email account used when sending notification emails from Mattermost.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SMTPUsername'
- >
- <FormattedMessage
- id='admin.email.smtpUsernameTitle'
- defaultMessage='SMTP Username:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SMTPUsername'
- ref='SMTPUsername'
- placeholder={formatMessage(holders.smtpUsernameExample)}
- defaultValue={this.props.config.EmailSettings.SMTPUsername}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.smtpUsernameDescription'
- defaultMessage=' Obtain this credential from administrator setting up your email server.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SMTPPassword'
- >
- <FormattedMessage
- id='admin.email.smtpPasswordTitle'
- defaultMessage='SMTP Password:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SMTPPassword'
- ref='SMTPPassword'
- placeholder={formatMessage(holders.smtpPasswordExample)}
- defaultValue={this.props.config.EmailSettings.SMTPPassword}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.smtpPasswordDescription'
- defaultMessage=' Obtain this credential from administrator setting up your email server.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SMTPServer'
- >
- <FormattedMessage
- id='admin.email.smtpServerTitle'
- defaultMessage='SMTP Server:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SMTPServer'
- ref='SMTPServer'
- placeholder={formatMessage(holders.smtpServerExample)}
- defaultValue={this.props.config.EmailSettings.SMTPServer}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.smtpServerDescription'
- defaultMessage='Location of SMTP email server.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SMTPPort'
- >
- <FormattedMessage
- id='admin.email.smtpPortTitle'
- defaultMessage='SMTP Port:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SMTPPort'
- ref='SMTPPort'
- placeholder={formatMessage(holders.smtpPortExample)}
- defaultValue={this.props.config.EmailSettings.SMTPPort}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.smtpPortDescription'
- defaultMessage='Port of SMTP email server.'
- />
- </p>
- </div>
- </div>
-
- <ConnectionSecurityDropdownSetting
- currentValue={this.state.connectionSecurity}
- handleChange={(e) => this.setState({connectionSecurity: e.target.value, saveNeeded: true})}
- isDisabled={!this.state.sendEmailNotifications}
- />
- <div className='form-group'>
- <div className='col-sm-offset-4 col-sm-8'>
- <div className='help-text'>
- <button
- className='btn btn-default'
- onClick={this.handleTestConnection}
- disabled={!this.state.sendEmailNotifications}
- id='connection-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.testing)}
- >
- <FormattedMessage
- id='admin.email.connectionSecurityTest'
- defaultMessage='Test Connection'
- />
- </button>
- {emailSuccess}
- {emailFail}
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='InviteSalt'
- >
- <FormattedMessage
- id='admin.email.inviteSaltTitle'
- defaultMessage='Invite Salt:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='InviteSalt'
- ref='InviteSalt'
- placeholder={formatMessage(holders.inviteSaltExample)}
- defaultValue={this.props.config.EmailSettings.InviteSalt}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.inviteSaltDescription'
- defaultMessage='32-character salt added to signing of email invites. Randomly generated on install. Click "Re-Generate" to create new salt.'
- />
- </p>
- <div className='help-text'>
- <button
- className='btn btn-default'
- onClick={this.handleGenerateInvite}
- disabled={!this.state.sendEmailNotifications}
- >
- <FormattedMessage
- id='admin.email.regenerate'
- defaultMessage='Re-Generate'
- />
- </button>
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PasswordResetSalt'
- >
- <FormattedMessage
- id='admin.email.passwordSaltTitle'
- defaultMessage='Password Reset Salt:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PasswordResetSalt'
- ref='PasswordResetSalt'
- placeholder={formatMessage(holders.passwordSaltExample)}
- defaultValue={this.props.config.EmailSettings.PasswordResetSalt}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.passwordSaltDescription'
- defaultMessage='32-character salt added to signing of password reset emails. Randomly generated on install. Click "Re-Generate" to create new salt.'
- />
- </p>
- <div className='help-text'>
- <button
- className='btn btn-default'
- onClick={this.handleGenerateReset}
- disabled={!this.state.sendEmailNotifications}
- >
- <FormattedMessage
- id='admin.email.regenerate'
- defaultMessage='Re-Generate'
- />
- </button>
- </div>
- </div>
- </div>
-
- {sendPushNotifications}
- {tosCheckbox}
- {pushNotificationServer}
- {pushNotificationContent}
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={disableSave}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.email.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ }
+ placeholder={Utils.localizeMessage('admin.email.smtpPortExample', 'Ex: "25", "465"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.smtpPortDescription'
+ defaultMessage='Port of SMTP email server.'
+ />
+ }
+ value={this.state.smtpPort}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <ConnectionSecurityDropdownSetting
+ value={this.state.connectionSecurity}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <EmailConnectionTest
+ config={this.getConfigFromState(this.props.config)}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <BooleanSetting
+ id='enableSecurityFixAlert'
+ label={
+ <FormattedMessage
+ id='admin.service.securityTitle'
+ defaultMessage='Enable Security Alerts: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.securityDesc'
+ defaultMessage='When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.'
+ />
+ }
+ value={this.state.enableSecurityFixAlert}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
);
}
-}
-
-EmailSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(EmailSettings);
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/external_service_settings.jsx b/webapp/components/admin_console/external_service_settings.jsx
new file mode 100644
index 000000000..88c6c28ea
--- /dev/null
+++ b/webapp/components/admin_console/external_service_settings.jsx
@@ -0,0 +1,94 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class ExternalServiceSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ segmentDeveloperKey: props.config.ServiceSettings.SegmentDeveloperKey,
+ googleDeveloperKey: props.config.ServiceSettings.GoogleDeveloperKey
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.SegmentDeveloperKey = this.state.segmentDeveloperKey;
+ config.ServiceSettings.GoogleDeveloperKey = this.state.googleDeveloperKey;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.integration.title'
+ defaultMessage='Integration Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.integrations.external'
+ defaultMessage='External Services'
+ />
+ }
+ >
+ <TextSetting
+ id='segmentDeveloperKey'
+ label={
+ <FormattedMessage
+ id='admin.service.segmentTitle'
+ defaultMessage='Segment Developer Key:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.segmentExample', 'Ex "g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.segmentDescription'
+ defaultMessage='For users running a SaaS services, sign up for a key at Segment.com to track metrics.'
+ />
+ }
+ value={this.state.segmentDeveloperKey}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='googleDeveloperKey'
+ label={
+ <FormattedMessage
+ id='admin.service.googleTitle'
+ defaultMessage='Google Developer Key:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.googleExample', 'Ex "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"')}
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.service.googleDescription'
+ defaultMessage='Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Leaving the field blank disables the automatic generation of YouTube video previews from links.'
+ />
+ }
+ value={this.state.googleDeveloperKey}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/generated_setting.jsx b/webapp/components/admin_console/generated_setting.jsx
new file mode 100644
index 000000000..29bb96985
--- /dev/null
+++ b/webapp/components/admin_console/generated_setting.jsx
@@ -0,0 +1,97 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import crypto from 'crypto';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class GeneratedSetting extends React.Component {
+ static get propTypes() {
+ return {
+ id: React.PropTypes.string.isRequired,
+ label: React.PropTypes.node.isRequired,
+ placeholder: React.PropTypes.string,
+ value: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool.isRequired,
+ disabledText: React.PropTypes.node,
+ helpText: React.PropTypes.node.isRequired,
+ regenerateText: React.PropTypes.node
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ disabled: false,
+ regenerateText: (
+ <FormattedMessage
+ id='admin.regenerate'
+ defaultMessage='Re-Generate'
+ />
+ )
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.regenerate = this.regenerate.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value === 'true');
+ }
+
+ regenerate(e) {
+ e.preventDefault();
+
+ this.props.onChange(this.props.id, crypto.randomBytes(256).toString('base64').substring(0, 32));
+ }
+
+ render() {
+ let disabledText = null;
+ if (this.props.disabled && this.props.disabledText) {
+ disabledText = (
+ <div className='admin-console__disabled-text'>
+ {this.props.disabledText}
+ </div>
+ );
+ }
+
+ return (
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor={this.props.id}
+ >
+ {this.props.label}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id={this.props.id}
+ placeholder={this.props.placeholder}
+ value={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ {disabledText}
+ <div className='help-text'>
+ {this.props.helpText}
+ </div>
+ <button
+ className='btn btn-default'
+ onClick={this.regenerate}
+ disabled={this.props.disabled}
+ >
+ {this.props.regenerateText}
+ </button>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/gitlab_settings.jsx b/webapp/components/admin_console/gitlab_settings.jsx
index 747905ac6..bd3cd8dec 100644
--- a/webapp/components/admin_console/gitlab_settings.jsx
+++ b/webapp/components/admin_console/gitlab_settings.jsx
@@ -1,383 +1,186 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import React from 'react';
-const holders = defineMessages({
- clientIdExample: {
- id: 'admin.gitlab.clientIdExample',
- defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
- },
- clientSecretExample: {
- id: 'admin.gitlab.clientSecretExample',
- defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
- },
- authExample: {
- id: 'admin.gitlab.authExample',
- defaultMessage: 'Ex ""'
- },
- tokenExample: {
- id: 'admin.gitlab.tokenExample',
- defaultMessage: 'Ex ""'
- },
- userExample: {
- id: 'admin.gitlab.userExample',
- defaultMessage: 'Ex ""'
- },
- saving: {
- id: 'admin.gitlab.saving',
- defaultMessage: 'Saving Config...'
- }
-});
+import * as Utils from 'utils/utils.jsx';
-import React from 'react';
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-class GitLabSettings extends React.Component {
+export default class GitLabSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
-
- this.state = {
- Enable: this.props.config.GitLabSettings.Enable,
- saveNeeded: false,
- serverError: null
- };
- }
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- handleChange(action) {
- var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.renderSettings = this.renderSettings.bind(this);
- if (action === 'EnableTrue') {
- s.Enable = true;
- }
-
- if (action === 'EnableFalse') {
- s.Enable = false;
- }
-
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ enable: props.config.GitLabSettings.Enable,
+ id: props.config.GitLabSettings.Id,
+ secret: props.config.GitLabSettings.Secret,
+ userApiEndpoint: props.config.GitLabSettings.UserApiEndpoint,
+ authEndpoint: props.config.GitLabSettings.AuthEndpoint,
+ tokenEndpoint: props.config.GitLabSettings.TokenEndpoint
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
+ getConfigFromState(config) {
+ config.GitLabSettings.Enable = this.state.enable;
+ config.GitLabSettings.Id = this.state.id;
+ config.GitLabSettings.Secret = this.state.secret;
+ config.GitLabSettings.UserApiEndpoint = this.state.userApiEndpoint;
+ config.GitLabSettings.AuthEndpoint = this.state.authEndpoint;
+ config.GitLabSettings.TokenEndpoint = this.state.tokenEndpoint;
- var config = this.props.config;
- config.GitLabSettings.Enable = ReactDOM.findDOMNode(this.refs.Enable).checked;
- config.GitLabSettings.Secret = ReactDOM.findDOMNode(this.refs.Secret).value.trim();
- config.GitLabSettings.Id = ReactDOM.findDOMNode(this.refs.Id).value.trim();
- config.GitLabSettings.AuthEndpoint = ReactDOM.findDOMNode(this.refs.AuthEndpoint).value.trim();
- config.GitLabSettings.TokenEndpoint = ReactDOM.findDOMNode(this.refs.TokenEndpoint).value.trim();
- config.GitLabSettings.UserApiEndpoint = ReactDOM.findDOMNode(this.refs.UserApiEndpoint).value.trim();
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.authentication.title'
+ defaultMessage='Authentication Settings'
+ />
+ </h3>
);
}
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
+ renderSettings() {
return (
- <div className='wrapper--fixed'>
-
- <h3>
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.gitlab.settingsTitle'
- defaultMessage='GitLab Settings'
+ id='admin.authentication.gitlab'
+ defaultMessage='GitLab'
/>
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Enable'
- >
- <FormattedMessage
- id='admin.gitlab.enableTitle'
- defaultMessage='Enable Sign Up With GitLab: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='true'
- ref='Enable'
- defaultChecked={this.props.config.GitLabSettings.Enable}
- onChange={this.handleChange.bind(this, 'EnableTrue')}
- />
- <FormattedMessage
- id='admin.gitlab.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='false'
- defaultChecked={!this.props.config.GitLabSettings.Enable}
- onChange={this.handleChange.bind(this, 'EnableFalse')}
- />
- <FormattedMessage
- id='admin.gitlab.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitlab.enableDescription'
- defaultMessage='When true, Mattermost allows team creation and account signup using GitLab OAuth.'
- />
- <br/>
- </p>
- <div className='help-text'>
- <FormattedHTMLMessage
- id='admin.gitlab.EnableHtmlDesc'
- defaultMessage='<ol><li>Log in to your GitLab account and go to Profile Settings -> Applications.</li><li>Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". </li><li>Then use "Secret" and "Id" fields from GitLab to complete the options below.</li><li>Complete the Endpoint URLs below. </li></ol>'
- />
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Id'
- >
- <FormattedMessage
- id='admin.gitlab.clientIdTitle'
- defaultMessage='Id:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='Id'
- ref='Id'
- placeholder={formatMessage(holders.clientIdExample)}
- defaultValue={this.props.config.GitLabSettings.Id}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitlab.clientIdDescription'
- defaultMessage='Obtain this value via the instructions above for logging into GitLab'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Secret'
- >
- <FormattedMessage
- id='admin.gitlab.clientSecretTitle'
- defaultMessage='Secret:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='Secret'
- ref='Secret'
- placeholder={formatMessage(holders.clientSecretExample)}
- defaultValue={this.props.config.GitLabSettings.Secret}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitab.clientSecretDescription'
- defaultMessage='Obtain this value via the instructions above for logging into GitLab.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AuthEndpoint'
- >
- <FormattedMessage
- id='admin.gitlab.authTitle'
- defaultMessage='Auth Endpoint:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AuthEndpoint'
- ref='AuthEndpoint'
- placeholder={formatMessage(holders.authExample)}
- defaultValue={this.props.config.GitLabSettings.AuthEndpoint}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitlab.authDescription'
- defaultMessage='Enter https://<your-gitlab-url>/oauth/authorize (example https://example.com:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='TokenEndpoint'
- >
+ }
+ >
+ <BooleanSetting
+ id='enable'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.enableTitle'
+ defaultMessage='Enable Sign Up With GitLab: '
+ />
+ }
+ helpText={
+ <div>
<FormattedMessage
- id='admin.gitlab.tokenTitle'
- defaultMessage='Token Endpoint:'
+ id='admin.gitlab.enableDescription'
+ defaultMessage='When true, Mattermost allows team creation and account signup using GitLab OAuth.'
/>
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='TokenEndpoint'
- ref='TokenEndpoint'
- placeholder={formatMessage(holders.tokenExample)}
- defaultValue={this.props.config.GitLabSettings.TokenEndpoint}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
+ <br/>
+ <FormattedHTMLMessage
+ id='admin.gitlab.EnableHtmlDesc'
+ defaultMessage='<ol><li>Log in to your GitLab account and go to Profile Settings -> Applications.</li><li>Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". </li><li>Then use "Secret" and "Id" fields from GitLab to complete the options below.</li><li>Complete the Endpoint URLs below. </li></ol>'
/>
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitlab.tokenDescription'
- defaultMessage='Enter https://<your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
- />
- </p>
</div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='UserApiEndpoint'
- >
- <FormattedMessage
- id='admin.gitlab.userTitle'
- defaultMessage='User API Endpoint:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='UserApiEndpoint'
- ref='UserApiEndpoint'
- placeholder={formatMessage(holders.userExample)}
- defaultValue={this.props.config.GitLabSettings.UserApiEndpoint}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitlab.userDescription'
- defaultMessage='Enter https://<your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.gitlab.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ }
+ value={this.state.enable}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='id'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.clientIdTitle'
+ defaultMessage='Id:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.gitlab.clientIdExample', 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"')}
+ helpText={
+ <FormattedMessage
+ id='admin.gitlab.clientIdDescription'
+ defaultMessage='Obtain this value via the instructions above for logging into GitLab'
+ />
+ }
+ value={this.state.id}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='secret'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.clientSecretTitle'
+ defaultMessage='Secret:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.gitlab.clientSecretExample', 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"')}
+ helpText={
+ <FormattedMessage
+ id='admin.gitab.clientSecretDescription'
+ defaultMessage='Obtain this value via the instructions above for logging into GitLab.'
+ />
+ }
+ value={this.state.secret}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='userApiEndpoint'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.userTitle'
+ defaultMessage='User API Endpoint:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.gitlab.userExample', 'Ex ""')}
+ helpText={
+ <FormattedMessage
+ id='admin.gitlab.userDescription'
+ defaultMessage='Enter https://<your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ }
+ value={this.state.userApiEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='authEndpoint'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.authTitle'
+ defaultMessage='Auth Endpoint:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.gitlab.authExample', 'Ex ""')}
+ helpText={
+ <FormattedMessage
+ id='admin.gitlab.authDescription'
+ defaultMessage='Enter https://<your-gitlab-url>/oauth/authorize (example https://example.com:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ }
+ value={this.state.authEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='tokenEndpoint'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.tokenTitle'
+ defaultMessage='Token Endpoint:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.gitlab.tokenExample', 'Ex ""')}
+ helpText={
+ <FormattedMessage
+ id='admin.gitlab.tokenDescription'
+ defaultMessage='Enter https://<your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ }
+ value={this.state.tokenEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ </SettingsGroup>
);
}
-}
-
-//config.GitLabSettings.Scope = ReactDOM.findDOMNode(this.refs.Scope).value.trim();
-// <div className='form-group'>
-// <label
-// className='control-label col-sm-4'
-// htmlFor='Scope'
-// >
-// {'Scope:'}
-// </label>
-// <div className='col-sm-8'>
-// <input
-// type='text'
-// className='form-control'
-// id='Scope'
-// ref='Scope'
-// placeholder='Not currently used by GitLab. Please leave blank'
-// defaultValue={this.props.config.GitLabSettings.Scope}
-// onChange={this.handleChange}
-// disabled={!this.state.Allow}
-// />
-// <p className='help-text'>{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}</p>
-// </div>
-// </div>
-
-GitLabSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(GitLabSettings);
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/image_settings.jsx b/webapp/components/admin_console/image_settings.jsx
index 023e9af3b..86d8795cc 100644
--- a/webapp/components/admin_console/image_settings.jsx
+++ b/webapp/components/admin_console/image_settings.jsx
@@ -1,692 +1,174 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import crypto from 'crypto';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+import React from 'react';
-const holders = defineMessages({
- storeLocal: {
- id: 'admin.image.storeLocal',
- defaultMessage: 'Local File System'
- },
- storeAmazonS3: {
- id: 'admin.image.storeAmazonS3',
- defaultMessage: 'Amazon S3'
- },
- localExample: {
- id: 'admin.image.localExample',
- defaultMessage: 'Ex "./data/"'
- },
- amazonS3IdExample: {
- id: 'admin.image.amazonS3IdExample',
- defaultMessage: 'Ex "AKIADTOVBGERKLCBV"'
- },
- amazonS3SecretExample: {
- id: 'admin.image.amazonS3SecretExample',
- defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
- },
- amazonS3BucketExample: {
- id: 'admin.image.amazonS3BucketExample',
- defaultMessage: 'Ex "mattermost-media"'
- },
- amazonS3RegionExample: {
- id: 'admin.image.amazonS3RegionExample',
- defaultMessage: 'Ex "us-east-1"'
- },
- thumbWidthExample: {
- id: 'admin.image.thumbWidthExample',
- defaultMessage: 'Ex "120"'
- },
- thumbHeightExample: {
- id: 'admin.image.thumbHeightExample',
- defaultMessage: 'Ex "100"'
- },
- previewWidthExample: {
- id: 'admin.image.previewWidthExample',
- defaultMessage: 'Ex "1024"'
- },
- previewHeightExample: {
- id: 'admin.image.previewHeightExample',
- defaultMessage: 'Ex "0"'
- },
- profileWidthExample: {
- id: 'admin.image.profileWidthExample',
- defaultMessage: 'Ex "1024"'
- },
- profileHeightExample: {
- id: 'admin.image.profileHeightExample',
- defaultMessage: 'Ex "0"'
- },
- publicLinkExample: {
- id: 'admin.image.publicLinkExample',
- defaultMessage: 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
- },
- saving: {
- id: 'admin.image.saving',
- defaultMessage: 'Saving Config...'
- }
-});
+import * as Utils from 'utils/utils.jsx';
-import React from 'react';
+import AdminSettings from './admin_settings.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-class FileSettings extends React.Component {
+export default class ImageSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleGenerate = this.handleGenerate.bind(this);
-
- this.state = {
- saveNeeded: false,
- serverError: null,
- DriverName: this.props.config.FileSettings.DriverName
- };
- }
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- handleChange(action) {
- var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.renderSettings = this.renderSettings.bind(this);
- if (action === 'DriverName') {
- s.DriverName = ReactDOM.findDOMNode(this.refs.DriverName).value;
- }
-
- this.setState(s);
- }
-
- handleGenerate(e) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ thumbnailWidth: props.config.FileSettings.ThumbnailWidth,
+ thumbnailHeight: props.config.FileSettings.ThumbnailHeight,
+ profileWidth: props.config.FileSettings.ProfileWidth,
+ profileHeight: props.config.FileSettings.ProfileHeight,
+ previewWidth: props.config.FileSettings.PreviewWidth,
+ previewHeight: props.config.FileSettings.PreviewHeight
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.props.config;
- config.FileSettings.DriverName = ReactDOM.findDOMNode(this.refs.DriverName).value;
- config.FileSettings.Directory = ReactDOM.findDOMNode(this.refs.Directory).value;
- config.FileSettings.AmazonS3AccessKeyId = ReactDOM.findDOMNode(this.refs.AmazonS3AccessKeyId).value;
- config.FileSettings.AmazonS3SecretAccessKey = ReactDOM.findDOMNode(this.refs.AmazonS3SecretAccessKey).value;
- config.FileSettings.AmazonS3Bucket = ReactDOM.findDOMNode(this.refs.AmazonS3Bucket).value;
- config.FileSettings.AmazonS3Region = ReactDOM.findDOMNode(this.refs.AmazonS3Region).value;
- config.FileSettings.EnablePublicLink = ReactDOM.findDOMNode(this.refs.EnablePublicLink).checked;
-
- config.FileSettings.PublicLinkSalt = ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value.trim();
+ getConfigFromState(config) {
+ config.FileSettings.ThumbnailWidth = this.parseInt(this.state.thumbnailWidth);
+ config.FileSettings.ThumbnailHeight = this.parseInt(this.state.thumbnailHeight);
+ config.FileSettings.ProfileWidth = this.parseInt(this.state.profileWidth);
+ config.FileSettings.ProfileHeight = this.parseInt(this.state.profileHeight);
+ config.FileSettings.PreviewWidth = this.parseInt(this.state.previewWidth);
+ config.FileSettings.PreviewHeight = this.parseInt(this.state.previewHeight);
- if (config.FileSettings.PublicLinkSalt === '') {
- config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
- ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt;
- }
-
- var thumbnailWidth = 120;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value, 10))) {
- thumbnailWidth = parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value, 10);
- }
- config.FileSettings.ThumbnailWidth = thumbnailWidth;
- ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth;
-
- var thumbnailHeight = 100;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value, 10))) {
- thumbnailHeight = parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value, 10);
- }
- config.FileSettings.ThumbnailHeight = thumbnailHeight;
- ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight;
-
- var previewWidth = 1024;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PreviewWidth).value, 10))) {
- previewWidth = parseInt(ReactDOM.findDOMNode(this.refs.PreviewWidth).value, 10);
- }
- config.FileSettings.PreviewWidth = previewWidth;
- ReactDOM.findDOMNode(this.refs.PreviewWidth).value = previewWidth;
-
- var previewHeight = 0;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PreviewHeight).value, 10))) {
- previewHeight = parseInt(ReactDOM.findDOMNode(this.refs.PreviewHeight).value, 10);
- }
- config.FileSettings.PreviewHeight = previewHeight;
- ReactDOM.findDOMNode(this.refs.PreviewHeight).value = previewHeight;
-
- var profileWidth = 128;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ProfileWidth).value, 10))) {
- profileWidth = parseInt(ReactDOM.findDOMNode(this.refs.ProfileWidth).value, 10);
- }
- config.FileSettings.ProfileWidth = profileWidth;
- ReactDOM.findDOMNode(this.refs.ProfileWidth).value = profileWidth;
-
- var profileHeight = 128;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ProfileHeight).value, 10))) {
- profileHeight = parseInt(ReactDOM.findDOMNode(this.refs.ProfileHeight).value, 10);
- }
- config.FileSettings.ProfileHeight = profileHeight;
- ReactDOM.findDOMNode(this.refs.ProfileHeight).value = profileHeight;
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.files.title'
+ defaultMessage='File Settings'
+ />
+ </h3>
);
}
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
- var enableFile = false;
- var enableS3 = false;
-
- if (this.state.DriverName === 'local') {
- enableFile = true;
- }
-
- if (this.state.DriverName === 'amazons3') {
- enableS3 = true;
- }
-
+ renderSettings() {
return (
- <div className='wrapper--fixed'>
- <h3>
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.image.fileSettings'
- defaultMessage='File Settings'
+ id='admin.files.images'
+ defaultMessage='Images'
/>
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='DriverName'
- >
- <FormattedMessage
- id='admin.image.storeTitle'
- defaultMessage='Store Files In:'
- />
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='DriverName'
- ref='DriverName'
- defaultValue={this.props.config.FileSettings.DriverName}
- onChange={this.handleChange.bind(this, 'DriverName')}
- >
- <option value='local'>{formatMessage(holders.storeLocal)}</option>
- <option value='amazons3'>{formatMessage(holders.storeAmazonS3)}</option>
- </select>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Directory'
- >
- <FormattedMessage
- id='admin.image.localTitle'
- defaultMessage='Local Directory Location:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='Directory'
- ref='Directory'
- placeholder={formatMessage(holders.localExample)}
- defaultValue={this.props.config.FileSettings.Directory}
- onChange={this.handleChange}
- disabled={!enableFile}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.localDescription'
- defaultMessage='Directory to which image files are written. If blank, will be set to ./data/.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AmazonS3AccessKeyId'
- >
- <FormattedMessage
- id='admin.image.amazonS3IdTitle'
- defaultMessage='Amazon S3 Access Key Id:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AmazonS3AccessKeyId'
- ref='AmazonS3AccessKeyId'
- placeholder={formatMessage(holders.amazonS3IdExample)}
- defaultValue={this.props.config.FileSettings.AmazonS3AccessKeyId}
- onChange={this.handleChange}
- disabled={!enableS3}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.amazonS3IdDescription'
- defaultMessage='Obtain this credential from your Amazon EC2 administrator.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AmazonS3SecretAccessKey'
- >
- <FormattedMessage
- id='admin.image.amazonS3SecretTitle'
- defaultMessage='Amazon S3 Secret Access Key:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AmazonS3SecretAccessKey'
- ref='AmazonS3SecretAccessKey'
- placeholder={formatMessage(holders.amazonS3SecretExample)}
- defaultValue={this.props.config.FileSettings.AmazonS3SecretAccessKey}
- onChange={this.handleChange}
- disabled={!enableS3}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.amazonS3SecretDescription'
- defaultMessage='Obtain this credential from your Amazon EC2 administrator.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AmazonS3Bucket'
- >
- <FormattedMessage
- id='admin.image.amazonS3BucketTitle'
- defaultMessage='Amazon S3 Bucket:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AmazonS3Bucket'
- ref='AmazonS3Bucket'
- placeholder={formatMessage(holders.amazonS3BucketExample)}
- defaultValue={this.props.config.FileSettings.AmazonS3Bucket}
- onChange={this.handleChange}
- disabled={!enableS3}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.amazonS3BucketDescription'
- defaultMessage='Name you selected for your S3 bucket in AWS.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AmazonS3Region'
- >
- <FormattedMessage
- id='admin.image.amazonS3RegionTitle'
- defaultMessage='Amazon S3 Region:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AmazonS3Region'
- ref='AmazonS3Region'
- placeholder={formatMessage(holders.amazonS3RegionExample)}
- defaultValue={this.props.config.FileSettings.AmazonS3Region}
- onChange={this.handleChange}
- disabled={!enableS3}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.amazonS3RegionDescription'
- defaultMessage='AWS region you selected for creating your S3 bucket.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ThumbnailWidth'
- >
- <FormattedMessage
- id='admin.image.thumbWidthTitle'
- defaultMessage='Thumbnail Width:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ThumbnailWidth'
- ref='ThumbnailWidth'
- placeholder={formatMessage(holders.thumbWidthExample)}
- defaultValue={this.props.config.FileSettings.ThumbnailWidth}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.thumbWidthDescription'
- defaultMessage='Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ThumbnailHeight'
- >
- <FormattedMessage
- id='admin.image.thumbHeightTitle'
- defaultMessage='Thumbnail Height:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ThumbnailHeight'
- ref='ThumbnailHeight'
- placeholder={formatMessage(holders.thumbHeightExample)}
- defaultValue={this.props.config.FileSettings.ThumbnailHeight}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.thumbHeightDescription'
- defaultMessage='Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PreviewWidth'
- >
- <FormattedMessage
- id='admin.image.previewWidthTitle'
- defaultMessage='Preview Width:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PreviewWidth'
- ref='PreviewWidth'
- placeholder={formatMessage(holders.previewWidthExample)}
- defaultValue={this.props.config.FileSettings.PreviewWidth}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.previewWidthDescription'
- defaultMessage='Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PreviewHeight'
- >
- <FormattedMessage
- id='admin.image.previewHeightTitle'
- defaultMessage='Preview Height:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PreviewHeight'
- ref='PreviewHeight'
- placeholder={formatMessage(holders.previewHeightExample)}
- defaultValue={this.props.config.FileSettings.PreviewHeight}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.previewHeightDescription'
- defaultMessage='Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ProfileWidth'
- >
- <FormattedMessage
- id='admin.image.profileWidthTitle'
- defaultMessage='Profile Width:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ProfileWidth'
- ref='ProfileWidth'
- placeholder={formatMessage(holders.profileWidthExample)}
- defaultValue={this.props.config.FileSettings.ProfileWidth}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.profileWidthDescription'
- defaultMessage='Width of profile picture.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ProfileHeight'
- >
- <FormattedMessage
- id='admin.image.profileHeightTitle'
- defaultMessage='Profile Height:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ProfileHeight'
- ref='ProfileHeight'
- placeholder={formatMessage(holders.profileHeightExample)}
- defaultValue={this.props.config.FileSettings.ProfileHeight}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.profileHeightDescription'
- defaultMessage='Height of profile picture.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnablePublicLink'
- >
- <FormattedMessage
- id='admin.image.shareTitle'
- defaultMessage='Share Public File Link: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePublicLink'
- value='true'
- ref='EnablePublicLink'
- defaultChecked={this.props.config.FileSettings.EnablePublicLink}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.image.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePublicLink'
- value='false'
- defaultChecked={!this.props.config.FileSettings.EnablePublicLink}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.image.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.shareDescription'
- defaultMessage='Allow users to share public links to files and images.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PublicLinkSalt'
- >
- <FormattedMessage
- id='admin.image.publicLinkTitle'
- defaultMessage='Public Link Salt:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PublicLinkSalt'
- ref='PublicLinkSalt'
- placeholder={formatMessage(holders.publicLinkExample)}
- defaultValue={this.props.config.FileSettings.PublicLinkSalt}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.publicLinkDescription'
- defaultMessage='32-character salt added to signing of public image links. Randomly generated on install. Click "Re-Generate" to create new salt.'
- />
- </p>
- <div className='help-text'>
- <button
- className='btn btn-default'
- onClick={this.handleGenerate}
- >
- <FormattedMessage
- id='admin.image.regenerate'
- defaultMessage='Re-Generate'
- />
- </button>
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.image.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ }
+ >
+ <TextSetting
+ id='thumbnailWidth'
+ label={
+ <FormattedMessage
+ id='admin.image.thumbWidthTitle'
+ defaultMessage='Thumbnail Width:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.thumbWidthExample', 'Ex "120"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.thumbWidthDescription'
+ defaultMessage='Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'
+ />
+ }
+ value={this.state.thumbnailWidth}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='thumbnailHeight'
+ label={
+ <FormattedMessage
+ id='admin.image.thumbHeightTitle'
+ defaultMessage='Thumbnail Height:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.thumbHeightExample', 'Ex "100"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.thumbHeightDescription'
+ defaultMessage='Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'
+ />
+ }
+ value={this.state.thumbnailHeight}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='profileWidth'
+ label={
+ <FormattedMessage
+ id='admin.image.profileWidthTitle'
+ defaultMessage='Profile Width:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.profileWidthExample', 'Ex "1024"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.profileWidthDescription'
+ defaultMessage='Width of profile picture.'
+ />
+ }
+ value={this.state.profileWidth}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='profileHeight'
+ label={
+ <FormattedMessage
+ id='admin.image.profileHeightTitle'
+ defaultMessage='Profile Height:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.profileHeightExample', 'Ex "0"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.profileHeightDescription'
+ defaultMessage='Height of profile picture.'
+ />
+ }
+ value={this.state.profileHeight}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='previewWidth'
+ label={
+ <FormattedMessage
+ id='admin.image.previewWidthTitle'
+ defaultMessage='Preview Width:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.previewWidthExample', 'Ex "1024"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.previewWidthDescription'
+ defaultMessage='Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'
+ />
+ }
+ value={this.state.previewWidth}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='previewHeight'
+ label={
+ <FormattedMessage
+ id='admin.image.previewHeightTitle'
+ defaultMessage='Preview Height:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.previewHeightExample', 'Ex "0"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.previewHeightDescription'
+ defaultMessage='Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'
+ />
+ }
+ value={this.state.previewHeight}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
);
}
-}
-
-FileSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(FileSettings); \ No newline at end of file
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/ldap_settings.jsx b/webapp/components/admin_console/ldap_settings.jsx
index 3ced65e50..d47a1f8c2 100644
--- a/webapp/components/admin_console/ldap_settings.jsx
+++ b/webapp/components/admin_console/ldap_settings.jsx
@@ -1,116 +1,95 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
+import React from 'react';
+
import * as Utils from 'utils/utils.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-import ConnectionSecurityDropdownSetting from './connection_security_dropdown_setting.jsx';
+import AdminSettings from './admin_settings.jsx';
import BooleanSetting from './boolean_setting.jsx';
+import ConnectionSecurityDropdownSetting from './connection_security_dropdown_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-const DEFAULT_LDAP_PORT = 389;
-const DEFAULT_QUERY_TIMEOUT = 60;
-
-import React from 'react';
-
-class LdapSettings extends React.Component {
+export default class LdapSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleChange = this.handleChange.bind(this);
- this.handleEnable = this.handleEnable.bind(this);
- this.handleDisable = this.handleDisable.bind(this);
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- this.state = {
- saveNeeded: false,
- serverError: null,
- enable: this.props.config.LdapSettings.Enable,
- connectionSecurity: this.props.config.LdapSettings.ConnectionSecurity,
- skipCertificateVerification: this.props.config.LdapSettings.SkipCertificateVerification
- };
- }
- handleChange() {
- this.setState({saveNeeded: true});
- }
- handleEnable() {
- this.setState({saveNeeded: true, enable: true});
- }
- handleDisable() {
- this.setState({saveNeeded: true, enable: false});
- }
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
+ this.renderSettings = this.renderSettings.bind(this);
- const config = this.props.config;
- config.LdapSettings.Enable = this.refs.Enable.checked;
- config.LdapSettings.LdapServer = this.refs.LdapServer.value.trim();
-
- let LdapPort = DEFAULT_LDAP_PORT;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.LdapPort).value, 10))) {
- LdapPort = parseInt(ReactDOM.findDOMNode(this.refs.LdapPort).value, 10);
- }
- config.LdapSettings.LdapPort = LdapPort;
+ this.state = Object.assign(this.state, {
+ enable: props.config.LdapSettings.Enable,
+ ldapServer: props.config.LdapSettings.LdapServer,
+ ldapPort: props.config.LdapSettings.LdapPort,
+ connectionSecurity: props.config.LdapSettings.ConnectionSecurity,
+ baseDN: props.config.LdapSettings.BaseDN,
+ bindUsername: props.config.LdapSettings.BindUsername,
+ bindPassword: props.config.LdapSettings.BindPassword,
+ userFilter: props.config.LdapSettings.UserFilter,
+ firstNameAttribute: props.config.LdapSettings.FirstNameAttribute,
+ lastNameAttribute: props.config.LdapSettings.LastNameAttribute,
+ nicknameAttribute: props.config.LdapSettings.NicknameAttribute,
+ emailAttribute: props.config.LdapSettings.EmailAttribute,
+ usernameAttribute: props.config.LdapSettings.UsernameAttribute,
+ idAttribute: props.config.LdapSettings.IdAttribute,
+ skipCertificateVerification: props.config.LdapSettings.SkipCertificateVerification,
+ queryTimeout: props.config.LdapSettings.QueryTimeout,
+ loginFieldName: props.config.LdapSettings.LoginFieldName
+ });
+ }
- config.LdapSettings.BaseDN = this.refs.BaseDN.value.trim();
- config.LdapSettings.BindUsername = this.refs.BindUsername.value.trim();
- config.LdapSettings.BindPassword = this.refs.BindPassword.value.trim();
- config.LdapSettings.FirstNameAttribute = this.refs.FirstNameAttribute.value.trim();
- config.LdapSettings.LastNameAttribute = this.refs.LastNameAttribute.value.trim();
- config.LdapSettings.NicknameAttribute = this.refs.NicknameAttribute.value.trim();
- config.LdapSettings.EmailAttribute = this.refs.EmailAttribute.value.trim();
- config.LdapSettings.UsernameAttribute = this.refs.UsernameAttribute.value.trim();
- config.LdapSettings.IdAttribute = this.refs.IdAttribute.value.trim();
- config.LdapSettings.UserFilter = this.refs.UserFilter.value.trim();
- config.LdapSettings.ConnectionSecurity = this.state.connectionSecurity.trim();
+ getConfigFromState(config) {
+ config.LdapSettings.Enable = this.state.enable;
+ config.LdapSettings.LdapServer = this.state.ldapServer;
+ config.LdapSettings.LdapPort = this.parseIntNonZero(this.state.ldapPort);
+ config.LdapSettings.ConnectionSecurity = this.state.connectionSecurity;
+ config.LdapSettings.BaseDN = this.state.baseDN;
+ config.LdapSettings.BindUsername = this.state.bindUsername;
+ config.LdapSettings.BindPassword = this.state.bindPassword;
+ config.LdapSettings.UserFilter = this.state.userFilter;
+ config.LdapSettings.FirstNameAttribute = this.state.firstNameAttribute;
+ config.LdapSettings.LastNameAttribute = this.state.lastNameAttribute;
+ config.LdapSettings.NicknameAttribute = this.state.nicknameAttribute;
+ config.LdapSettings.EmailAttribute = this.state.emailAttribute;
+ config.LdapSettings.UsernameAttribute = this.state.usernameAttribute;
+ config.LdapSettings.IdAttribute = this.state.idAttribute;
config.LdapSettings.SkipCertificateVerification = this.state.skipCertificateVerification;
- config.LdapSettings.LoginFieldName = this.refs.LoginFieldName.value.trim();
+ config.LdapSettings.QueryTimeout = this.parseIntNonZero(this.state.queryTimeout);
+ config.LdapSettings.LoginFieldName = this.state.loginFieldName;
- let QueryTimeout = DEFAULT_QUERY_TIMEOUT;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10))) {
- QueryTimeout = parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10);
- }
- config.LdapSettings.QueryTimeout = QueryTimeout;
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.authentication.title'
+ defaultMessage='Authentication Settings'
+ />
+ </h3>
);
}
- render() {
- let serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
- let saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
+ renderSettings() {
+ const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true';
+ if (!licenseEnabled) {
+ return null;
}
- const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true';
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.authentication.ldap'
+ defaultMessage='LDAP'
+ />
- let bannerContent;
- if (licenseEnabled) {
- bannerContent = (
+ }
+ >
<div className='banner'>
<div className='banner__content'>
<h4 className='banner__heading'>
@@ -127,540 +106,310 @@ class LdapSettings extends React.Component {
</p>
</div>
</div>
- );
- } else {
- bannerContent = (
- <div className='banner warning'>
- <div className='banner__content'>
- <FormattedHTMLMessage
- id='admin.ldap.noLicense'
- defaultMessage='<h4 class="banner__heading">Note:</h4><p>LDAP is an enterprise feature. Your current license does not support LDAP. Click <a href="http://mattermost.com"target="_blank">here</a> for information and pricing on enterprise licenses.</p>'
+ <BooleanSetting
+ id='enable'
+ label={
+ <FormattedMessage
+ id='admin.ldap.enableTitle'
+ defaultMessage='Enable Login With LDAP:'
/>
- </div>
- </div>
- );
- }
-
- return (
- <div className='wrapper--fixed'>
- {bannerContent}
- <h3>
- <FormattedMessage
- id='admin.ldap.title'
- defaultMessage='LDAP Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Enable'
- >
- <FormattedMessage
- id='admin.ldap.enableTitle'
- defaultMessage='Enable Login With LDAP:'
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='true'
- ref='Enable'
- defaultChecked={this.props.config.LdapSettings.Enable}
- onChange={this.handleEnable}
- disabled={!licenseEnabled}
- />
- <FormattedMessage
- id='admin.ldap.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='false'
- defaultChecked={!this.props.config.LdapSettings.Enable}
- onChange={this.handleDisable}
- />
- <FormattedMessage
- id='admin.ldap.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.enableDesc'
- defaultMessage='When true, Mattermost allows login using LDAP'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='LdapServer'
- >
- <FormattedMessage
- id='admin.ldap.serverTitle'
- defaultMessage='LDAP Server:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='LdapServer'
- ref='LdapServer'
- placeholder={Utils.localizeMessage('admin.ldap.serverEx', 'Ex "10.0.0.23"')}
- defaultValue={this.props.config.LdapSettings.LdapServer}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.serverDesc'
- defaultMessage='The domain or IP address of LDAP server.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='LdapPort'
- >
- <FormattedMessage
- id='admin.ldap.portTitle'
- defaultMessage='LDAP Port:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='number'
- className='form-control'
- id='LdapPort'
- ref='LdapPort'
- placeholder={Utils.localizeMessage('admin.ldap.portEx', 'Ex "389"')}
- defaultValue={this.props.config.LdapSettings.LdapPort}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.portDesc'
- defaultMessage='The port Mattermost will use to connect to the LDAP server. Default is 389.'
- />
- </p>
- </div>
- </div>
- <ConnectionSecurityDropdownSetting
- currentValue={this.state.connectionSecurity}
- handleChange={(e) => this.setState({connectionSecurity: e.target.value, saveNeeded: true})}
- isDisabled={!this.state.enable}
- />
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='BaseDN'
- >
- <FormattedMessage
- id='admin.ldap.baseTitle'
- defaultMessage='BaseDN:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='BaseDN'
- ref='BaseDN'
- placeholder={Utils.localizeMessage('admin.ldap.baseEx', 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"')}
- defaultValue={this.props.config.LdapSettings.BaseDN}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.baseDesc'
- defaultMessage='The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='BindUsername'
- >
- <FormattedMessage
- id='admin.ldap.bindUserTitle'
- defaultMessage='Bind Username:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='BindUsername'
- ref='BindUsername'
- placeholder=''
- defaultValue={this.props.config.LdapSettings.BindUsername}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.bindUserDesc'
- defaultMessage='The username used to perform the LDAP search. This should typically be an account created specifically for use with Mattermost. It should have access limited to read the portion of the LDAP tree specified in the BaseDN field.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='BindPassword'
- >
- <FormattedMessage
- id='admin.ldap.bindPwdTitle'
- defaultMessage='Bind Password:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='password'
- className='form-control'
- id='BindPassword'
- ref='BindPassword'
- placeholder=''
- defaultValue={this.props.config.LdapSettings.BindPassword}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.bindPwdDesc'
- defaultMessage='Password of the user given in "Bind Username".'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='UserFilter'
- >
- <FormattedMessage
- id='admin.ldap.userFilterTitle'
- defaultMessage='User Filter:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='UserFilter'
- ref='UserFilter'
- placeholder={Utils.localizeMessage('admin.ldap.userFilterEx', 'Ex. "(objectClass=user)"')}
- defaultValue={this.props.config.LdapSettings.UserFilter}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.userFilterDisc'
- defaultMessage='Optionally enter an LDAP Filter to use when searching for user objects. Only the users selected by the query will be able to access Mattermost. For Active Directory, the query to filter out disabled users is (&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='FirstNameAttribute'
- >
- <FormattedMessage
- id='admin.ldap.firstnameAttrTitle'
- defaultMessage='First Name Attrubute'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='FirstNameAttribute'
- ref='FirstNameAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.firstnameAttrEx', 'Ex "givenName"')}
- defaultValue={this.props.config.LdapSettings.FirstNameAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.firstnameAttrDesc'
- defaultMessage='The attribute in the LDAP server that will be used to populate the first name of users in Mattermost.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='LastNameAttribute'
- >
- <FormattedMessage
- id='admin.ldap.lastnameAttrTitle'
- defaultMessage='Last Name Attribute:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='LastNameAttribute'
- ref='LastNameAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.lastnameAttrEx', 'Ex "sn"')}
- defaultValue={this.props.config.LdapSettings.LastNameAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.lastnameAttrDesc'
- defaultMessage='The attribute in the LDAP server that will be used to populate the last name of users in Mattermost.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='NicknameAttribute'
- >
- <FormattedMessage
- id='admin.ldap.nicknameAttrTitle'
- defaultMessage='Nickname Attribute:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='NicknameAttribute'
- ref='NicknameAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.nicknameAttrEx', 'Ex "nickname"')}
- defaultValue={this.props.config.LdapSettings.NicknameAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.nicknameAttrDesc'
- defaultMessage='(Optional) The attribute in the LDAP server that will be used to populate the nickname of users in Mattermost.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EmailAttribute'
- >
- <FormattedMessage
- id='admin.ldap.emailAttrTitle'
- defaultMessage='Email Attribute:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='EmailAttribute'
- ref='EmailAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.emailAttrEx', 'Ex "mail" or "userPrincipalName"')}
- defaultValue={this.props.config.LdapSettings.EmailAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.emailAttrDesc'
- defaultMessage='The attribute in the LDAP server that will be used to populate the email addresses of users in Mattermost.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='UsernameAttribute'
- >
- <FormattedMessage
- id='admin.ldap.usernameAttrTitle'
- defaultMessage='Username Attribute:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='UsernameAttribute'
- ref='UsernameAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.usernameAttrEx', 'Ex "sAMAccountName"')}
- defaultValue={this.props.config.LdapSettings.UsernameAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.uernameAttrDesc'
- defaultMessage='The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='IdAttribute'
- >
- <FormattedMessage
- id='admin.ldap.idAttrTitle'
- defaultMessage='Id Attribute: '
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='IdAttribute'
- ref='IdAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.idAttrEx', 'Ex "sAMAccountName"')}
- defaultValue={this.props.config.LdapSettings.IdAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.idAttrDesc'
- defaultMessage='The attribute in the LDAP server that will be used as a unique identifier in Mattermost. It should be an LDAP attribute with a value that does not change, such as username or uid. If a user’s Id Attribute changes, it will create a new Mattermost account unassociated with their old one. This is the value used to log in to Mattermost in the "LDAP Username" field on the sign in page. Normally this attribute is the same as the “Username Attribute” field above. If your team typically uses domain\\username to sign in to other services with LDAP, you may choose to put domain\\username in this field to maintain consistency between sites.'
- />
- </p>
- </div>
- </div>
- <BooleanSetting
- label={
- <FormattedMessage
- id='admin.ldap.skipCertificateVerification'
- defaultMessage='Skip Certificate Verification'
- />
- }
- currentValue={this.state.skipCertificateVerification}
- isDisabled={!this.state.enable}
- handleChange={(e) => this.setState({skipCertificateVerification: e.target.value.trim() === 'true', saveNeeded: true})}
- helpText={
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.skipCertificateVerificationDesc'
- defaultMessage='Skips the certificate verification step for TLS or STARTTLS connections. Not recommended for production environments where TLS is required. For testing only.'
- />
- </p>
- }
- />
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='QueryTimeout'
- >
- <FormattedMessage
- id='admin.ldap.queryTitle'
- defaultMessage='Query Timeout (seconds):'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='number'
- className='form-control'
- id='QueryTimeout'
- ref='QueryTimeout'
- placeholder={Utils.localizeMessage('admin.ldap.queryEx', 'Ex "60"')}
- defaultValue={this.props.config.LdapSettings.QueryTimeout}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.queryDesc'
- defaultMessage='The timeout value for queries to the LDAP server. Increase if you are getting timeout errors caused by a slow LDAP server.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='LoginFieldName'
- >
- <FormattedMessage
- id='admin.ldap.loginNameTitle'
- defaultMessage='Login Field Name:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='LoginFieldName'
- ref='LoginFieldName'
- placeholder={Utils.localizeMessage('admin.ldap.loginNameEx', 'Ex "LDAP Username"')}
- defaultValue={this.props.config.LdapSettings.LoginFieldName}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.loginNameDesc'
- defaultMessage='The placeholder text that appears in the login field on the login page. Defaults to "LDAP Username".'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + Utils.localizeMessage('admin.ldap.saving', 'Saving Config...')}
- >
- <FormattedMessage
- id='admin.ldap.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
- </form>
- </div>
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.enableDesc'
+ defaultMessage='When true, Mattermost allows login using LDAP'
+ />
+ }
+ value={this.state.enable}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='ldapServer'
+ label={
+ <FormattedMessage
+ id='admin.ldap.serverTitle'
+ defaultMessage='LDAP Server:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.serverEx', 'Ex "10.0.0.23"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.serverDesc'
+ defaultMessage='The domain or IP address of LDAP server.'
+ />
+ }
+ value={this.state.ldapServer}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='ldapPort'
+ label={
+ <FormattedMessage
+ id='admin.ldap.portTitle'
+ defaultMessage='LDAP Port:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.portEx', 'Ex "389"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.portDesc'
+ defaultMessage='The port Mattermost will use to connect to the LDAP server. Default is 389.'
+ />
+ }
+ value={this.state.ldapPort}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <ConnectionSecurityDropdownSetting
+ value={this.state.ldapConnectionSecurity}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='baseDN'
+ label={
+ <FormattedMessage
+ id='admin.ldap.baseTitle'
+ defaultMessage='BaseDN:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.baseEx', 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.baseDesc'
+ defaultMessage='The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.'
+ />
+ }
+ value={this.state.baseDN}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='bindUsername'
+ label={
+ <FormattedMessage
+ id='admin.ldap.bindUserTitle'
+ defaultMessage='Bind Username:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.bindUserDesc'
+ defaultMessage='The username used to perform the LDAP search. This should typically be an account created specifically for use with Mattermost. It should have access limited to read the portion of the LDAP tree specified in the BaseDN field.'
+ />
+ }
+ value={this.state.bindUsername}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='bindPassword'
+ label={
+ <FormattedMessage
+ id='admin.ldap.bindPwdTitle'
+ defaultMessage='Bind Password:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.bindPwdDesc'
+ defaultMessage='Password of the user given in "Bind Username".'
+ />
+ }
+ value={this.state.bindPassword}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='userFilter'
+ label={
+ <FormattedMessage
+ id='admin.ldap.userFilterTitle'
+ defaultMessage='User Filter:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.userFilterEx', 'Ex. "(objectClass=user)"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.userFilterDisc'
+ defaultMessage='Optionally enter an LDAP Filter to use when searching for user objects. Only the users selected by the query will be able to access Mattermost. For Active Directory, the query to filter out disabled users is (&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).'
+ />
+ }
+ value={this.state.userFilter}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='firstNameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.firstnameAttrTitle'
+ defaultMessage='First Name Attrubute'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.firstnameAttrEx', 'Ex "givenName"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.firstnameAttrDesc'
+ defaultMessage='The attribute in the LDAP server that will be used to populate the first name of users in Mattermost.'
+ />
+ }
+ value={this.state.firstNameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='lastNameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.lastnameAttrTitle'
+ defaultMessage='Last Name Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.lastnameAttrEx', 'Ex "sn"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.lastnameAttrDesc'
+ defaultMessage='The attribute in the LDAP server that will be used to populate the last name of users in Mattermost.'
+ />
+ }
+ value={this.state.lastNameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='nicknameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.nicknameAttrTitle'
+ defaultMessage='Nickname Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.nicknameAttrEx', 'Ex "nickname"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.nicknameAttrDesc'
+ defaultMessage='(Optional) The attribute in the LDAP server that will be used to populate the nickname of users in Mattermost.'
+ />
+ }
+ value={this.state.nicknameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='emailAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.emailAttrTitle'
+ defaultMessage='Email Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.emailAttrEx', 'Ex "mail" or "userPrincipalName"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.emailAttrDesc'
+ defaultMessage='The attribute in the LDAP server that will be used to populate the email addresses of users in Mattermost.'
+ />
+ }
+ value={this.state.emailAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='usernameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.usernameAttrTitle'
+ defaultMessage='Username Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.usernameAttrEx', 'Ex "sAMAccountName"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.uernameAttrDesc'
+ defaultMessage='The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.'
+ />
+ }
+ value={this.state.usernameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='idAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.idAttrTitle'
+ defaultMessage='Id Attribute: '
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.idAttrEx', 'Ex "sAMAccountName"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.idAttrDesc'
+ defaultMessage='The attribute in the LDAP server that will be used as a unique identifier in Mattermost. It should be an LDAP attribute with a value that does not change, such as username or uid. If a user’s Id Attribute changes, it will create a new Mattermost account unassociated with their old one. This is the value used to log in to Mattermost in the "LDAP Username" field on the sign in page. Normally this attribute is the same as the “Username Attribute” field above. If your team typically uses domain\\username to sign in to other services with LDAP, you may choose to put domain\\username in this field to maintain consistency between sites.'
+ />
+ }
+ value={this.state.idAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <BooleanSetting
+ id='skipCertificateVerification'
+ label={
+ <FormattedMessage
+ id='admin.ldap.skipCertificateVerification'
+ defaultMessage='Skip Certificate Verification'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.skipCertificateVerificationDesc'
+ defaultMessage='Skips the certificate verification step for TLS or STARTTLS connections. Not recommended for production environments where TLS is required. For testing only.'
+ />
+ }
+ value={this.state.skipCertificateVerification}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='queryTimeout'
+ label={
+ <FormattedMessage
+ id='admin.ldap.queryTitle'
+ defaultMessage='Query Timeout (seconds):'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.queryEx', 'Ex "60"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.queryDesc'
+ defaultMessage='The timeout value for queries to the LDAP server. Increase if you are getting timeout errors caused by a slow LDAP server.'
+ />
+ }
+ value={this.state.queryTimeout}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='loginFieldName'
+ label={
+ <FormattedMessage
+ id='admin.ldap.loginNameTitle'
+ defaultMessage='Login Field Name:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.loginNameEx', 'Ex "LDAP Username"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.loginNameDesc'
+ defaultMessage='The placeholder text that appears in the login field on the login page. Defaults to "LDAP Username".'
+ />
+ }
+ value={this.state.loginFieldName}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ </SettingsGroup>
);
}
-}
-LdapSettings.defaultProps = {
-};
-
-LdapSettings.propTypes = {
- config: React.PropTypes.object
-};
-
-export default LdapSettings;
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/legal_and_support_settings.jsx b/webapp/components/admin_console/legal_and_support_settings.jsx
index 9f72f5fdf..cb152e414 100644
--- a/webapp/components/admin_console/legal_and_support_settings.jsx
+++ b/webapp/components/admin_console/legal_and_support_settings.jsx
@@ -1,309 +1,166 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-
-var holders = defineMessages({
- saving: {
- id: 'admin.support.saving',
- defaultMessage: 'Saving Config...'
- }
-});
-
import React from 'react';
-class LegalAndSupportSettings extends React.Component {
+import AdminSettings from './admin_settings.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class LegalAndSupportSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- this.state = {
- saveNeeded: false,
- serverError: null
- };
- }
+ this.renderSettings = this.renderSettings.bind(this);
- handleChange() {
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ termsOfServiceLink: props.config.SupportSettings.TermsOfServiceLink,
+ privacyPolicyLink: props.config.SupportSettings.PrivacyPolicyLink,
+ aboutLink: props.config.SupportSettings.AboutLink,
+ helpLink: props.config.SupportSettings.HelpLink,
+ reportAProblemLink: props.config.SupportSettings.ReportAProblemLink,
+ supportEmail: props.config.SupportSettings.SupportEmail
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
+ getConfigFromState(config) {
+ config.SupportSettings.TermsOfServiceLink = this.state.termsOfServiceLink;
+ config.SupportSettings.PrivacyPolicyLink = this.state.privacyPolicyLink;
+ config.SupportSettings.AboutLink = this.state.aboutLink;
+ config.SupportSettings.HelpLink = this.state.helpLink;
+ config.SupportSettings.ReportAProblemLink = this.state.reportAProblemLink;
+ config.SupportSettings.SupportEmail = this.state.supportEmail;
- var config = this.props.config;
-
- config.SupportSettings.TermsOfServiceLink = ReactDOM.findDOMNode(this.refs.TermsOfServiceLink).value.trim();
- config.SupportSettings.PrivacyPolicyLink = ReactDOM.findDOMNode(this.refs.PrivacyPolicyLink).value.trim();
- config.SupportSettings.AboutLink = ReactDOM.findDOMNode(this.refs.AboutLink).value.trim();
- config.SupportSettings.HelpLink = ReactDOM.findDOMNode(this.refs.HelpLink).value.trim();
- config.SupportSettings.ReportAProblemLink = ReactDOM.findDOMNode(this.refs.ReportAProblemLink).value.trim();
- config.SupportSettings.SupportEmail = ReactDOM.findDOMNode(this.refs.SupportEmail).value.trim();
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.customization.title'
+ defaultMessage='Customization Settings'
+ />
+ </h3>
);
}
- render() {
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
+ renderSettings() {
return (
- <div className='wrapper--fixed'>
- <div className='banner'>
- <div className='banner__content'>
- <h4 className='banner__heading'>
- <FormattedMessage
- id='admin.support.noteTitle'
- defaultMessage='Note:'
- />
- </h4>
- <p>
- <FormattedMessage
- id='admin.support.noteDescription'
- defaultMessage='If linking to an external site, URLs should begin with http:// or https://.'
- />
- </p>
- </div>
- </div>
- <h3>
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.support.title'
- defaultMessage='Legal and Support Settings'
+ id='admin.customization.support'
+ defaultMessage='Legal and Support'
/>
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='TermsOfServiceLink'
- >
- <FormattedMessage
- id='admin.support.termsTitle'
- defaultMessage='Terms of Service link:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='TermsOfServiceLink'
- ref='TermsOfServiceLink'
- defaultValue={this.props.config.SupportSettings.TermsOfServiceLink}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.termsDesc'
- defaultMessage='Link to Terms of Service available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PrivacyPolicyLink'
- >
- <FormattedMessage
- id='admin.support.privacyTitle'
- defaultMessage='Privacy Policy link:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PrivacyPolicyLink'
- ref='PrivacyPolicyLink'
- defaultValue={this.props.config.SupportSettings.PrivacyPolicyLink}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.privacyDesc'
- defaultMessage='Link to Privacy Policy available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AboutLink'
- >
- <FormattedMessage
- id='admin.support.aboutTitle'
- defaultMessage='About link:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AboutLink'
- ref='AboutLink'
- defaultValue={this.props.config.SupportSettings.AboutLink}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.aboutDesc'
- defaultMessage='Link to About page for more information on your Mattermost deployment, for example its purpose and audience within your organization. Defaults to Mattermost information page.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='HelpLink'
- >
- <FormattedMessage
- id='admin.support.helpTitle'
- defaultMessage='Help link:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='HelpLink'
- ref='HelpLink'
- defaultValue={this.props.config.SupportSettings.HelpLink}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.helpDesc'
- defaultMessage='Link to help documentation from team site main menu. Typically not changed unless your organization chooses to create custom documentation.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ReportAProblemLink'
- >
- <FormattedMessage
- id='admin.support.problemTitle'
- defaultMessage='Report a Problem link:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ReportAProblemLink'
- ref='ReportAProblemLink'
- defaultValue={this.props.config.SupportSettings.ReportAProblemLink}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.problemDesc'
- defaultMessage='Link to help documentation from team site main menu. By default this points to the peer-to-peer troubleshooting forum where users can search for, find and request help with technical issues.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SupportEmail'
- >
- <FormattedMessage
- id='admin.support.emailTitle'
- defaultMessage='Support email:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SupportEmail'
- ref='SupportEmail'
- defaultValue={this.props.config.SupportSettings.SupportEmail}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.emailHelp'
- defaultMessage='Email shown during tutorial for end users to ask support questions.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.support.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ }
+ >
+ <TextSetting
+ id='termsOfServiceLink'
+ label={
+ <FormattedMessage
+ id='admin.support.termsTitle'
+ defaultMessage='Terms of Service link:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.termsDesc'
+ defaultMessage='Link to Terms of Service available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'
+ />
+ }
+ value={this.state.termsOfServiceLink}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='privacyPolicyLink'
+ label={
+ <FormattedMessage
+ id='admin.support.privacyTitle'
+ defaultMessage='Privacy Policy link:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.privacyDesc'
+ defaultMessage='Link to Privacy Policy available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'
+ />
+ }
+ value={this.state.privacyPolicyLink}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='aboutLink'
+ label={
+ <FormattedMessage
+ id='admin.support.aboutTitle'
+ defaultMessage='About link:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.aboutDesc'
+ defaultMessage='Link to About page for more information on your Mattermost deployment, for example its purpose and audience within your organization. Defaults to Mattermost information page.'
+ />
+ }
+ value={this.state.aboutLink}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='helpLink'
+ label={
+ <FormattedMessage
+ id='admin.support.helpTitle'
+ defaultMessage='Help link:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.helpDesc'
+ defaultMessage='Link to help documentation from team site main menu. Typically not changed unless your organization chooses to create custom documentation.'
+ />
+ }
+ value={this.state.helpLink}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='reportAProblemLink'
+ label={
+ <FormattedMessage
+ id='admin.support.problemTitle'
+ defaultMessage='Report a Problem link:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.problemDesc'
+ defaultMessage='Link to help documentation from team site main menu. By default this points to the peer-to-peer troubleshooting forum where users can search for, find and request help with technical issues.'
+ />
+ }
+ value={this.state.reportAProblemLink}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='supportEmail'
+ label={
+ <FormattedMessage
+ id='admin.support.emailTitle'
+ defaultMessage='Support email:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.emailHelp'
+ defaultMessage='Email shown during tutorial for end users to ask support questions.'
+ />
+ }
+ value={this.state.supportEmail}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
);
}
-}
-
-LegalAndSupportSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(LegalAndSupportSettings); \ No newline at end of file
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx
index f2c511e44..14934a3e5 100644
--- a/webapp/components/admin_console/license_settings.jsx
+++ b/webapp/components/admin_console/license_settings.jsx
@@ -152,6 +152,7 @@ class LicenseSettings extends React.Component {
{'Mattermost Enterprise Edition. Unlock enterprise features in this software through the purchase of a subscription from '}
<a
target='_blank'
+ rel='noopener noreferrer'
href='https://mattermost.com/'
>
{'https://mattermost.com/'}
diff --git a/webapp/components/admin_console/log_settings.jsx b/webapp/components/admin_console/log_settings.jsx
index 061c2b6e3..fa29074d8 100644
--- a/webapp/components/admin_console/log_settings.jsx
+++ b/webapp/components/admin_console/log_settings.jsx
@@ -1,418 +1,246 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+import React from 'react';
-const holders = defineMessages({
- locationPlaceholder: {
- id: 'admin.log.locationPlaceholder',
- defaultMessage: 'Enter your file location'
- },
- formatPlaceholder: {
- id: 'admin.log.formatPlaceholder',
- defaultMessage: 'Enter your file format'
- },
- saving: {
- id: 'admin.log.saving',
- defaultMessage: 'Saving Config...'
- }
-});
+import * as Utils from 'utils/utils.jsx';
-import React from 'react';
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import DropdownSetting from './dropdown_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-class LogSettings extends React.Component {
+export default class LogSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
-
- this.state = {
- consoleEnable: this.props.config.LogSettings.EnableConsole,
- fileEnable: this.props.config.LogSettings.EnableFile,
- saveNeeded: false,
- serverError: null
- };
- }
-
- handleChange(action) {
- var s = {saveNeeded: true, serverError: this.state.serverError};
-
- if (action === 'console_true') {
- s.consoleEnable = true;
- }
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- if (action === 'console_false') {
- s.consoleEnable = false;
- }
+ this.renderSettings = this.renderSettings.bind(this);
- if (action === 'file_true') {
- s.fileEnable = true;
- }
-
- if (action === 'file_false') {
- s.fileEnable = false;
- }
-
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ enableConsole: props.config.LogSettings.EnableConsole,
+ consoleLevel: props.config.LogSettings.ConsoleLevel,
+ enableFile: props.config.LogSettings.EnableFile,
+ fileLevel: props.config.LogSettings.FileLevel,
+ fileLocation: props.config.LogSettings.FileLocation,
+ fileFormat: props.config.LogSettings.FileFormat
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
+ getConfigFromState(config) {
+ config.LogSettings.EnableConsole = this.state.enableConsole;
+ config.LogSettings.ConsoleLevel = this.state.consoleLevel;
+ config.LogSettings.EnableFile = this.state.enableFile;
+ config.LogSettings.FileLevel = this.state.fileLevel;
+ config.LogSettings.FileLocation = this.state.fileLocation;
+ config.LogSettings.FileFormat = this.state.fileFormat;
- var config = this.props.config;
- config.LogSettings.EnableConsole = ReactDOM.findDOMNode(this.refs.consoleEnable).checked;
- config.LogSettings.ConsoleLevel = ReactDOM.findDOMNode(this.refs.consoleLevel).value;
- config.LogSettings.EnableFile = ReactDOM.findDOMNode(this.refs.fileEnable).checked;
- config.LogSettings.FileLevel = ReactDOM.findDOMNode(this.refs.fileLevel).value;
- config.LogSettings.FileLocation = ReactDOM.findDOMNode(this.refs.fileLocation).value.trim();
- config.LogSettings.FileFormat = ReactDOM.findDOMNode(this.refs.fileFormat).value.trim();
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- consoleEnable: config.LogSettings.EnableConsole,
- fileEnable: config.LogSettings.EnableFile,
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- consoleEnable: config.LogSettings.EnableConsole,
- fileEnable: config.LogSettings.EnableFile,
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.general.title'
+ defaultMessage='General Settings'
+ />
+ </h3>
);
}
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
+ renderSettings() {
+ const logLevels = [
+ {value: 'DEBUG', text: 'DEBUG'},
+ {value: 'INFO', text: 'INFO'},
+ {value: 'ERROR', text: 'ERROR'}
+ ];
return (
- <div className='wrapper--fixed'>
- <h3>
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.log.logSettings'
- defaultMessage='Log Settings'
+ id='admin.general.log'
+ defaultMessage='Logging'
/>
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='consoleEnable'
- >
- <FormattedMessage
- id='admin.log.consoleTitle'
- defaultMessage='Log To The Console: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='consoleEnable'
- value='true'
- ref='consoleEnable'
- defaultChecked={this.props.config.LogSettings.EnableConsole}
- onChange={this.handleChange.bind(this, 'console_true')}
- />
- <FormattedMessage
- id='admin.log.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='consoleEnable'
- value='false'
- defaultChecked={!this.props.config.LogSettings.EnableConsole}
- onChange={this.handleChange.bind(this, 'console_false')}
- />
- <FormattedMessage
- id='admin.log.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.log.consoleDescription'
- defaultMessage='Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true, server writes messages to the standard output stream (stdout).'
- />
- </p>
- </div>
- </div>
+ }
+ >
+ <BooleanSetting
+ id='enableConsole'
+ label={
+ <FormattedMessage
+ id='admin.log.consoleTitle'
+ defaultMessage='Log To The Console: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.log.consoleDescription'
+ defaultMessage='Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true, server writes messages to the standard output stream (stdout).'
+ />
+ }
+ value={this.state.enableConsole}
+ onChange={this.handleChange}
+ />
+ <DropdownSetting
+ id='consoleLevel'
+ values={logLevels}
+ label={
+ <FormattedMessage
+ id='admin.log.levelTitle'
+ defaultMessage='Console Log Level:'
+ />
+ }
+ value={this.state.consoleLevel}
+ onChange={this.handleChange}
+ disabled={!this.state.enableConsole}
+ helpText={
+ <FormattedMessage
+ id='admin.log.levelDescription'
+ defaultMessage='This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'
+ />
+ }
+ />
+ <BooleanSetting
+ id='enableFile'
+ label={
+ <FormattedMessage
+ id='admin.log.fileTitle'
+ defaultMessage='Log To File: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.log.fileDescription'
+ defaultMessage='Typically set to true in production. When true, log files are written to the log file specified in file location field below.'
+ />
+ }
+ value={this.state.enableFile}
+ onChange={this.handleChange}
+ />
+ <DropdownSetting
+ id='fileLevel'
+ values={logLevels}
+ label={
+ <FormattedMessage
+ id='admin.log.fileLevelTitle'
+ defaultMessage='File Log Level:'
+ />
+ }
+ value={this.state.fileLevel}
+ onChange={this.handleChange}
+ disabled={!this.state.enableFile}
+ helpText={
+ <FormattedMessage
+ id='admin.log.fileLevelDescription'
+ defaultMessage='This setting determines the level of detail at which log events are written to the log file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'
+ />
+ }
+ />
+ <TextSetting
+ id='fileLocation'
+ label={
+ <FormattedMessage
+ id='admin.log.locationTitle'
+ defaultMessage='File Location:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.log.locationPlaceholder', 'Enter your file location')}
+ helpText={
+ <FormattedMessage
+ id='admin.log.locationDescription'
+ defaultMessage='File to which log files are written. If blank, will be set to ./logs/mattermost, which writes logs to mattermost.log. Log rotation is enabled and every 10,000 lines of log information is written to new files stored in the same directory, for example mattermost.2015-09-23.001, mattermost.2015-09-23.002, and so forth.'
+ />
+ }
+ value={this.state.fileLocation}
+ onChange={this.handleChange}
+ disabled={!this.state.enableFile}
+ />
+ <TextSetting
+ id='fileFormat'
+ label={
+ <FormattedMessage
+ id='admin.log.formatTitle'
+ defaultMessage='File Format:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.log.formatPlaceholder', 'Enter your file format')}
+ helpText={this.renderFileFormatHelpText()}
+ value={this.state.fileFormat}
+ onChange={this.handleChange}
+ disabled={!this.state.enableFile}
+ />
+ </SettingsGroup>
+ );
+ }
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='consoleLevel'
- >
- <FormattedMessage
- id='admin.log.levelTitle'
- defaultMessage='Console Log Level:'
- />
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='consoleLevel'
- ref='consoleLevel'
- defaultValue={this.props.config.LogSettings.consoleLevel}
- onChange={this.handleChange}
- disabled={!this.state.consoleEnable}
- >
- <option value='DEBUG'>{'DEBUG'}</option>
- <option value='INFO'>{'INFO'}</option>
- <option value='ERROR'>{'ERROR'}</option>
- </select>
- <p className='help-text'>
+ renderFileFormatHelpText() {
+ return (
+ <div>
+ <FormattedMessage
+ id='admin.log.formatDescription'
+ defaultMessage='Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'
+ />
+ <table
+ className='table table-bordered'
+ cellPadding='5'
+ >
+ <tbody>
+ <tr>
+ <td className='help-text'>{'%T'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.levelDescription'
- defaultMessage='This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- >
- <FormattedMessage
- id='admin.log.fileTitle'
- defaultMessage='Log To File: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='fileEnable'
- ref='fileEnable'
- value='true'
- defaultChecked={this.props.config.LogSettings.EnableFile}
- onChange={this.handleChange.bind(this, 'file_true')}
+ id='admin.log.formatTime'
+ defaultMessage='Time (15:04:05 MST)'
/>
- <FormattedMessage
- id='admin.log.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='fileEnable'
- value='false'
- defaultChecked={!this.props.config.LogSettings.EnableFile}
- onChange={this.handleChange.bind(this, 'file_false')}
- />
- <FormattedMessage
- id='admin.log.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>{'%D'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.fileDescription'
- defaultMessage='Typically set to true in production. When true, log files are written to the log file specified in file location field below.'
+ id='admin.log.formatDateLong'
+ defaultMessage='Date (2006/01/02)'
/>
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='fileLevel'
- >
- <FormattedMessage
- id='admin.log.fileLevelTitle'
- defaultMessage='File Log Level:'
- />
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='fileLevel'
- ref='fileLevel'
- defaultValue={this.props.config.LogSettings.FileLevel}
- onChange={this.handleChange}
- disabled={!this.state.fileEnable}
- >
- <option value='DEBUG'>{'DEBUG'}</option>
- <option value='INFO'>{'INFO'}</option>
- <option value='ERROR'>{'ERROR'}</option>
- </select>
- <p className='help-text'>
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>{'%d'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.fileLevelDescription'
- defaultMessage='This setting determines the level of detail at which log events are written to the log file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'
+ id='admin.log.formatDateShort'
+ defaultMessage='Date (01/02/06)'
/>
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='fileLocation'
- >
- <FormattedMessage
- id='admin.log.locationTitle'
- defaultMessage='File Location:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='fileLocation'
- ref='fileLocation'
- placeholder={formatMessage(holders.locationPlaceholder)}
- defaultValue={this.props.config.LogSettings.FileLocation}
- onChange={this.handleChange}
- disabled={!this.state.fileEnable}
- />
- <p className='help-text'>
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>{'%L'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.locationDescription'
- defaultMessage='File to which log files are written. If blank, will be set to ./logs/mattermost, which writes logs to mattermost.log. Log rotation is enabled and every 10,000 lines of log information is written to new files stored in the same directory, for example mattermost.2015-09-23.001, mattermost.2015-09-23.002, and so forth.'
+ id='admin.log.formatLevel'
+ defaultMessage='Level (DEBG, INFO, EROR)'
/>
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='fileFormat'
- >
- <FormattedMessage
- id='admin.log.formatTitle'
- defaultMessage='File Format:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='fileFormat'
- ref='fileFormat'
- placeholder={formatMessage(holders.formatPlaceholder)}
- defaultValue={this.props.config.LogSettings.FileFormat}
- onChange={this.handleChange}
- disabled={!this.state.fileEnable}
- />
- <div className='help-text'>
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>{'%S'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.formatDescription'
- defaultMessage='Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'
+ id='admin.log.formatSource'
+ defaultMessage='Source'
/>
- <div className='help-text'>
- <table
- className='table table-bordered'
- cellPadding='5'
- >
- <tbody>
- <tr><td className='help-text'>{'%T'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatTime'
- defaultMessage='Time (15:04:05 MST)'
- />
- </td></tr>
- <tr><td className='help-text'>{'%D'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatDateLong'
- defaultMessage='Date (2006/01/02)'
- />
- </td></tr>
- <tr><td className='help-text'>{'%d'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatDateShort'
- defaultMessage='Date (01/02/06)'
- />
- </td></tr>
- <tr><td className='help-text'>{'%L'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatLevel'
- defaultMessage='Level (DEBG, INFO, EROR)'
- />
- </td></tr>
- <tr><td className='help-text'>{'%S'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatSource'
- defaultMessage='Source'
- />
- </td></tr>
- <tr><td className='help-text'>{'%M'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatMessage'
- defaultMessage='Message'
- />
- </td></tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>{'%M'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.save'
- defaultMessage='Save'
+ id='admin.log.formatMessage'
+ defaultMessage='Message'
/>
- </button>
- </div>
- </div>
-
- </form>
+ </td>
+ </tr>
+ </tbody>
+ </table>
</div>
);
}
-}
-
-LogSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(LogSettings); \ No newline at end of file
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/login_settings.jsx b/webapp/components/admin_console/login_settings.jsx
new file mode 100644
index 000000000..f473d8f56
--- /dev/null
+++ b/webapp/components/admin_console/login_settings.jsx
@@ -0,0 +1,130 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import GeneratedSetting from './generated_setting.jsx';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class LoginSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ passwordResetSalt: props.config.EmailSettings.PasswordResetSalt,
+ maximumLoginAttempts: props.config.ServiceSettings.MaximumLoginAttempts,
+ enableMultifactorAuthentication: props.config.ServiceSettings.EnableMultifactorAuthentication
+ });
+ }
+
+ getConfigFromState(config) {
+ config.EmailSettings.PasswordResetSalt = this.state.passwordResetSalt;
+ config.ServiceSettings.MaximumLoginAttempts = this.parseIntNonZero(this.state.maximumLoginAttempts);
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
+ config.ServiceSettings.EnableMultifactorAuthentication = this.state.enableMultifactorAuthentication;
+ }
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.security.title'
+ defaultMessage='Security Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ let mfaSetting = null;
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
+ mfaSetting = (
+ <BooleanSetting
+ id='enableMultifactorAuthentication'
+ label={
+ <FormattedMessage
+ id='admin.service.mfaTitle'
+ defaultMessage='Enable Multi-factor Authentication:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.mfaDesc'
+ defaultMessage='When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.'
+ />
+ }
+ value={this.state.enableMultifactorAuthentication}
+ onChange={this.handleChange}
+ />
+ );
+ }
+
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.security.login'
+ defaultMessage='Login'
+ />
+ }
+ >
+ <GeneratedSetting
+ id='passwordResetSalt'
+ label={
+ <FormattedMessage
+ id='admin.email.passwordSaltTitle'
+ defaultMessage='Password Reset Salt:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.passwordSaltDescription'
+ defaultMessage='32-character salt added to signing of password reset emails. Randomly generated on install. Click "Re-Generate" to create new salt.'
+ />
+ }
+ value={this.state.passwordResetSalt}
+ onChange={this.handleChange}
+ disabled={this.state.sendEmailNotifications}
+ disabledText={
+ <FormattedMessage
+ id='admin.security.passwordResetSalt.disabled'
+ defaultMessage='Password reset salt cannot be changed while sending emails is disabled.'
+ />
+ }
+ />
+ <TextSetting
+ id='maximumLoginAttempts'
+ label={
+ <FormattedMessage
+ id='admin.service.attemptTitle'
+ defaultMessage='Maximum Login Attempts:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.attemptExample', 'Ex "10"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.attemptDescription'
+ defaultMessage='Login attempts allowed before user is locked out and required to reset password via email.'
+ />
+ }
+ value={this.state.maximumLoginAttempts}
+ onChange={this.handleChange}
+ />
+ {mfaSetting}
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/logs.jsx b/webapp/components/admin_console/logs.jsx
index f2c6d92c3..ad0277b7f 100644
--- a/webapp/components/admin_console/logs.jsx
+++ b/webapp/components/admin_console/logs.jsx
@@ -99,4 +99,4 @@ export default class Logs extends React.Component {
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/webapp/components/admin_console/privacy_settings.jsx b/webapp/components/admin_console/privacy_settings.jsx
index 5045a6d31..8905e57ef 100644
--- a/webapp/components/admin_console/privacy_settings.jsx
+++ b/webapp/components/admin_console/privacy_settings.jsx
@@ -1,215 +1,90 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-
-const holders = defineMessages({
- saving: {
- id: 'admin.privacy.saving',
- defaultMessage: 'Saving Config...'
- }
-});
-
import React from 'react';
-class PrivacySettings extends React.Component {
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+
+export default class PrivacySettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- this.state = {
- saveNeeded: false,
- serverError: null
- };
- }
-
- handleChange() {
- var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.renderSettings = this.renderSettings.bind(this);
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ showEmailAddress: props.config.PrivacySettings.ShowEmailAddress,
+ showFullName: props.config.PrivacySettings.ShowFullName
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
+ getConfigFromState(config) {
+ config.PrivacySettings.ShowEmailAddress = this.state.showEmailAddress;
+ config.PrivacySettings.ShowFullName = this.state.showFullName;
- var config = this.props.config;
- config.PrivacySettings.ShowEmailAddress = ReactDOM.findDOMNode(this.refs.ShowEmailAddress).checked;
- config.PrivacySettings.ShowFullName = ReactDOM.findDOMNode(this.refs.ShowFullName).checked;
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.general.title'
+ defaultMessage='General Settings'
+ />
+ </h3>
);
}
- render() {
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
+ renderSettings() {
return (
- <div className='wrapper--fixed'>
- <h3>
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.privacy.title'
- defaultMessage='Privacy Settings'
+ id='admin.general.privacy'
+ defaultMessage='Privacy'
/>
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ShowEmailAddress'
- >
- <FormattedMessage
- id='admin.privacy.showEmailTitle'
- defaultMessage='Show Email Address: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='ShowEmailAddress'
- value='true'
- ref='ShowEmailAddress'
- defaultChecked={this.props.config.PrivacySettings.ShowEmailAddress}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.privacy.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='ShowEmailAddress'
- value='false'
- defaultChecked={!this.props.config.PrivacySettings.ShowEmailAddress}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.privacy.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.privacy.showEmailDescription'
- defaultMessage='When false, hides email address of users from other users in the user interface, including team owners and team administrators. Used when system is set up for managing teams where some users choose to keep their contact information private.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ShowFullName'
- >
- <FormattedMessage
- id='admin.privacy.showFullNameTitle'
- defaultMessage='Show Full Name: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='ShowFullName'
- value='true'
- ref='ShowFullName'
- defaultChecked={this.props.config.PrivacySettings.ShowFullName}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.privacy.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='ShowFullName'
- value='false'
- defaultChecked={!this.props.config.PrivacySettings.ShowFullName}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.privacy.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.privacy.showFullNameDescription'
- defaultMessage='When false, hides full name of users from other users, including team owners and team administrators. Username is shown in place of full name.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.privacy.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ }
+ >
+ <BooleanSetting
+ id='showEmailAddress'
+ label={
+ <FormattedMessage
+ id='admin.privacy.showEmailTitle'
+ defaultMessage='Show Email Address: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.privacy.showEmailDescription'
+ defaultMessage='When false, hides email address of users from other users in the user interface, including team owners and team administrators. Used when system is set up for managing teams where some users choose to keep their contact information private.'
+ />
+ }
+ value={this.state.showEmailAddress}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='showFullName'
+ label={
+ <FormattedMessage
+ id='admin.privacy.showFullNameTitle'
+ defaultMessage='Show Full Name: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.privacy.showFullNameDescription'
+ defaultMessage='When false, hides full name of users from other users, including team owners and team administrators. Username is shown in place of full name.'
+ />
+ }
+ value={this.state.showFullName}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
);
}
-}
-
-PrivacySettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(PrivacySettings); \ No newline at end of file
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/public_link_settings.jsx b/webapp/components/admin_console/public_link_settings.jsx
new file mode 100644
index 000000000..9024261fa
--- /dev/null
+++ b/webapp/components/admin_console/public_link_settings.jsx
@@ -0,0 +1,91 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import GeneratedSetting from './generated_setting.jsx';
+import SettingsGroup from './settings_group.jsx';
+
+export default class PublicLinkSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enablePublicLink: props.config.FileSettings.EnablePublicLink,
+ publicLinkSalt: props.config.FileSettings.PublicLinkSalt
+ });
+ }
+
+ getConfigFromState(config) {
+ config.FileSettings.EnablePublicLink = this.state.enablePublicLink;
+ config.FileSettings.PublicLinkSalt = this.state.publicLinkSalt;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.security.title'
+ defaultMessage='Security Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.security.public_links'
+ defaultMessage='Public Links'
+ />
+ }
+ >
+ <BooleanSetting
+ id='enablePublicLink'
+ label={
+ <FormattedMessage
+ id='admin.image.shareTitle'
+ defaultMessage='Share Public File Link: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.image.shareDescription'
+ defaultMessage='Allow users to share public links to files and images.'
+ />
+ }
+ value={this.state.enablePublicLink}
+ onChange={this.handleChange}
+ />
+ <GeneratedSetting
+ id='publicLinkSalt'
+ label={
+ <FormattedMessage
+ id='admin.image.publicLinkTitle'
+ defaultMessage='Public Link Salt:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.image.publicLinkDescription'
+ defaultMessage='32-character salt added to signing of public image links. Randomly generated on install. Click "Re-Generate" to create new salt.'
+ />
+ }
+ value={this.state.publicLinkSalt}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/push_settings.jsx b/webapp/components/admin_console/push_settings.jsx
new file mode 100644
index 000000000..660c23e97
--- /dev/null
+++ b/webapp/components/admin_console/push_settings.jsx
@@ -0,0 +1,235 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Constants from 'utils/constants.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import DropdownSetting from './dropdown_setting.jsx';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+const PUSH_NOTIFICATIONS_OFF = 'off';
+const PUSH_NOTIFICATIONS_MHPNS = 'mhpns';
+const PUSH_NOTIFICATIONS_MTPNS = 'mtpns';
+const PUSH_NOTIFICATIONS_CUSTOM = 'custom';
+
+export default class PushSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.canSave = this.canSave.bind(this);
+
+ this.handleAgreeChange = this.handleAgreeChange.bind(this);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ let pushNotificationServerType = PUSH_NOTIFICATIONS_CUSTOM;
+ let agree = false;
+ if (!props.config.EmailSettings.SendPushNotifications) {
+ pushNotificationServerType = PUSH_NOTIFICATIONS_OFF;
+ } else if (props.config.EmailSettings.PushNotificationServer === Constants.MHPNS &&
+ global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MHPNS === 'true') {
+ pushNotificationServerType = PUSH_NOTIFICATIONS_MHPNS;
+ agree = true;
+ } else if (props.config.EmailSettings.PushNotificationServer === Constants.MTPNS) {
+ pushNotificationServerType = PUSH_NOTIFICATIONS_MTPNS;
+ } else {
+ pushNotificationServerType = PUSH_NOTIFICATIONS_CUSTOM;
+ }
+
+ let pushNotificationServer = this.props.config.EmailSettings.PushNotificationServer;
+ if (pushNotificationServerType === PUSH_NOTIFICATIONS_MTPNS) {
+ pushNotificationServer = Constants.MTPNS;
+ } else if (pushNotificationServerType === PUSH_NOTIFICATIONS_MHPNS) {
+ pushNotificationServer = Constants.MHPNS;
+ }
+
+ this.state = Object.assign(this.state, {
+ pushNotificationServerType,
+ pushNotificationServer,
+ pushNotificationContents: props.config.EmailSettings.PushNotificationContents,
+ agree
+ });
+ }
+
+ canSave() {
+ return this.state.pushNotificationServerType !== PUSH_NOTIFICATIONS_MHPNS || this.state.agree;
+ }
+
+ handleAgreeChange(e) {
+ this.setState({
+ agree: e.target.checked
+ });
+ }
+
+ handleChange(id, value) {
+ if (id === 'pushNotificationServerType') {
+ this.setState({
+ agree: false
+ });
+
+ if (value === PUSH_NOTIFICATIONS_MHPNS) {
+ this.setState({
+ pushNotificationServer: Constants.MHPNS
+ });
+ } else if (value === PUSH_NOTIFICATIONS_MTPNS) {
+ this.setState({
+ pushNotificationServer: Constants.MTPNS
+ });
+ }
+ }
+
+ super.handleChange(id, value);
+ }
+
+ getConfigFromState(config) {
+ config.EmailSettings.SendPushNotifications = this.state.pushNotificationServerType !== PUSH_NOTIFICATIONS_OFF;
+ config.EmailSettings.PushNotificationServer = this.state.pushNotificationServer.trim();
+ config.EmailSettings.PushNotificationContents = this.state.pushNotificationContents;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.notifications.title'
+ defaultMessage='Notification Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ const pushNotificationServerTypes = [];
+ pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_OFF, text: Utils.localizeMessage('admin.email.pushOff', 'Do not send push notifications')});
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MHPNS === 'true') {
+ pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_MHPNS, text: Utils.localizeMessage('admin.email.mhpns', 'Use encrypted, production-quality HPNS connection to iOS and Android apps')});
+ }
+ pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_MTPNS, text: Utils.localizeMessage('admin.email.mtpns', 'Use iOS and Android apps on iTunes and Google Play with TPNS')});
+ pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_CUSTOM, text: Utils.localizeMessage('admin.email.selfPush', 'Manually enter Push Notification Service location')});
+
+ let sendHelpText = null;
+ let pushServerHelpText = null;
+ if (this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_OFF) {
+ sendHelpText = (
+ <FormattedHTMLMessage
+ id='admin.email.pushOffHelp'
+ defaultMessage='Please see <a href="http://docs.mattermost.com/deployment/push.html#push-notifications-and-mobile-devices" target="_blank">documentation on push notifications</a> to learn more about setup options.'
+ />
+ );
+ } else if (this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_MHPNS) {
+ pushServerHelpText = (
+ <FormattedHTMLMessage
+ id='admin.email.mhpnsHelp'
+ defaultMessage='Download <a href="https://itunes.apple.com/us/app/mattermost/id984966508?mt=8" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns" target="_blank">Mattermost Hosted Push Notification Service</a>.'
+ />
+ );
+ } else if (this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_MTPNS) {
+ pushServerHelpText = (
+ <FormattedHTMLMessage
+ id='admin.email.mtpnsHelp'
+ defaultMessage='Download <a href="https://itunes.apple.com/us/app/mattermost/id984966508?mt=8" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns" target="_blank">Mattermost Test Push Notification Service</a>.'
+ />
+ );
+ } else {
+ pushServerHelpText = (
+ <FormattedHTMLMessage
+ id='admin.email.easHelp'
+ defaultMessage='Learn more about compiling and deploying your own mobile apps from an <a href="http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas" target="_blank">Enterprise App Store</a>.'
+ />
+ );
+ }
+
+ let tosCheckbox;
+ if (this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_MHPNS) {
+ tosCheckbox = (
+ <div className='form-group'>
+ <div className='col-sm-4'/>
+ <div className='col-sm-8'>
+ <input
+ type='checkbox'
+ ref='agree'
+ checked={this.state.agree}
+ onChange={this.handleAgreeChange}
+ />
+ <FormattedHTMLMessage
+ id='admin.email.agreeHPNS'
+ defaultMessage=' I understand and accept the Mattermost Hosted Push Notification Service <a href="https://about.mattermost.com/hpns-terms/" target="_blank">Terms of Service</a> and <a href="https://about.mattermost.com/hpns-privacy/" target="_blank">Privacy Policy</a>.'
+ />
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.notifications.push'
+ defaultMessage='Mobile Push'
+ />
+ }
+ >
+ <DropdownSetting
+ id='pushNotificationServerType'
+ values={pushNotificationServerTypes}
+ label={
+ <FormattedMessage
+ id='admin.email.pushTitle'
+ defaultMessage='Send Push Notifications: '
+ />
+ }
+ value={this.state.pushNotificationServerType}
+ onChange={this.handleChange}
+ helpText={sendHelpText}
+ />
+ {tosCheckbox}
+ <TextSetting
+ id='pushNotificationServer'
+ label={
+ <FormattedMessage
+ id='admin.email.pushServerTitle'
+ defaultMessage='Push Notification Server:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.email.pushServerEx', 'E.g.: "http://push-test.mattermost.com"')}
+ helpText={pushServerHelpText}
+ value={this.state.pushNotificationServer}
+ onChange={this.handleChange}
+ disabled={this.state.pushNotificationServerType !== PUSH_NOTIFICATIONS_CUSTOM}
+ />
+ <DropdownSetting
+ id='pushNotificationContents'
+ values={[
+ {value: 'generic', text: Utils.localizeMessage('admin.email.genericPushNotification', 'Send generic description with user and channel names')},
+ {value: 'full', text: Utils.localizeMessage('admin.email.fullPushNotification', 'Send full message snippet')}
+ ]}
+ label={
+ <FormattedMessage
+ id='admin.email.pushContentTitle'
+ defaultMessage='Push Notification Contents:'
+ />
+ }
+ value={this.state.pushNotificationContents}
+ onChange={this.handleChange}
+ disabled={this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_OFF}
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.email.pushContentDesc'
+ defaultMessage='Selecting "Send generic description with user and channel names" provides push notifications with generic messages, including names of users and channels but no specific details from the message text.<br /><br />
+ Selecting "Send full message snippet" sends excerpts from messages triggering notifications with specifics and may include confidential information sent in messages. If your Push Notification Service is outside your firewall, it is HIGHLY RECOMMENDED this option only be used with an "https" protocol to encrypt the connection.'
+ />
+ }
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/rate_settings.jsx b/webapp/components/admin_console/rate_settings.jsx
index de7a40e6b..60818aaf9 100644
--- a/webapp/components/admin_console/rate_settings.jsx
+++ b/webapp/components/admin_console/rate_settings.jsx
@@ -1,371 +1,158 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
+import React from 'react';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+import * as Utils from 'utils/utils.jsx';
-const holders = defineMessages({
- queriesExample: {
- id: 'admin.rate.queriesExample',
- defaultMessage: 'Ex "10"'
- },
- memoryExample: {
- id: 'admin.rate.memoryExample',
- defaultMessage: 'Ex "10000"'
- },
- httpHeaderExample: {
- id: 'admin.rate.httpHeaderExample',
- defaultMessage: 'Ex "X-Real-IP", "X-Forwarded-For"'
- },
- saving: {
- id: 'admin.rate.saving',
- defaultMessage: 'Saving Config...'
- }
-});
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-import React from 'react';
-
-class RateSettings extends React.Component {
+export default class RateSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
-
- this.state = {
- EnableRateLimiter: this.props.config.RateLimitSettings.EnableRateLimiter,
- VaryByRemoteAddr: this.props.config.RateLimitSettings.VaryByRemoteAddr,
- saveNeeded: false,
- serverError: null
- };
- }
-
- handleChange(action) {
- var s = {saveNeeded: true, serverError: this.state.serverError};
-
- if (action === 'EnableRateLimiterTrue') {
- s.EnableRateLimiter = true;
- }
-
- if (action === 'EnableRateLimiterFalse') {
- s.EnableRateLimiter = false;
- }
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- if (action === 'VaryByRemoteAddrTrue') {
- s.VaryByRemoteAddr = true;
- }
+ this.renderSettings = this.renderSettings.bind(this);
- if (action === 'VaryByRemoteAddrFalse') {
- s.VaryByRemoteAddr = false;
- }
-
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ enableRateLimiter: props.config.RateLimitSettings.EnableRateLimiter,
+ perSec: props.config.RateLimitSettings.PerSec,
+ memoryStoreSize: props.config.RateLimitSettings.MemoryStoreSize,
+ varyByRemoteAddr: props.config.RateLimitSettings.VaryByRemoteAddr,
+ varyByHeader: props.config.RateLimitSettings.VaryByHeader
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.props.config;
- config.RateLimitSettings.EnableRateLimiter = ReactDOM.findDOMNode(this.refs.EnableRateLimiter).checked;
- config.RateLimitSettings.VaryByRemoteAddr = ReactDOM.findDOMNode(this.refs.VaryByRemoteAddr).checked;
- config.RateLimitSettings.VaryByHeader = ReactDOM.findDOMNode(this.refs.VaryByHeader).value.trim();
+ getConfigFromState(config) {
+ config.RateLimitSettings.EnableRateLimiter = this.state.enableRateLimiter;
+ config.RateLimitSettings.PerSec = this.parseIntNonZero(this.state.perSec);
+ config.RateLimitSettings.MemoryStoreSize = this.parseIntNonZero(this.state.memoryStoreSize);
+ config.RateLimitSettings.VaryByRemoteAddr = this.state.varyByRemoteAddr;
+ config.RateLimitSettings.VaryByHeader = this.state.varyByHeader;
- var PerSec = 10;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PerSec).value, 10))) {
- PerSec = parseInt(ReactDOM.findDOMNode(this.refs.PerSec).value, 10);
- }
- config.RateLimitSettings.PerSec = PerSec;
- ReactDOM.findDOMNode(this.refs.PerSec).value = PerSec;
-
- var MemoryStoreSize = 10000;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value, 10))) {
- MemoryStoreSize = parseInt(ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value, 10);
- }
- config.RateLimitSettings.MemoryStoreSize = MemoryStoreSize;
- ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value = MemoryStoreSize;
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.rate.title'
+ defaultMessage='Rate Limit Settings'
+ />
+ </h3>
);
}
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
+ renderSettings() {
return (
- <div className='wrapper--fixed'>
-
+ <SettingsGroup>
<div className='banner'>
<div className='banner__content'>
- <h4 className='banner__heading'>
- <FormattedMessage
- id='admin.rate.noteTitle'
- defaultMessage='Note:'
- />
- </h4>
- <p>
- <FormattedMessage
- id='admin.rate.noteDescription'
- defaultMessage='Changing properties in this section will require a server restart before taking effect.'
- />
- </p>
+ <FormattedMessage
+ id='admin.rate.noteDescription'
+ defaultMessage='Changing properties in this section will require a server restart before taking effect.'
+ />
</div>
</div>
-
- <h3>
- <FormattedMessage
- id='admin.rate.title'
- defaultMessage='Rate Limit Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableRateLimiter'
- >
- <FormattedMessage
- id='admin.rate.enableLimiterTitle'
- defaultMessage='Enable Rate Limiter: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableRateLimiter'
- value='true'
- ref='EnableRateLimiter'
- defaultChecked={this.props.config.RateLimitSettings.EnableRateLimiter}
- onChange={this.handleChange.bind(this, 'EnableRateLimiterTrue')}
- />
- <FormattedMessage
- id='admin.rate.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableRateLimiter'
- value='false'
- defaultChecked={!this.props.config.RateLimitSettings.EnableRateLimiter}
- onChange={this.handleChange.bind(this, 'EnableRateLimiterFalse')}
- />
- <FormattedMessage
- id='admin.rate.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.rate.enableLimiterDescription'
- defaultMessage='When true, APIs are throttled at rates specified below.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PerSec'
- >
- <FormattedMessage
- id='admin.rate.queriesTitle'
- defaultMessage='Number Of Queries Per Second:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PerSec'
- ref='PerSec'
- placeholder={formatMessage(holders.queriesExample)}
- defaultValue={this.props.config.RateLimitSettings.PerSec}
- onChange={this.handleChange}
- disabled={!this.state.EnableRateLimiter}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.rate.queriesDescription'
- defaultMessage='Throttles API at this number of requests per second.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='MemoryStoreSize'
- >
- <FormattedMessage
- id='admin.rate.memoryTitle'
- defaultMessage='Memory Store Size:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='MemoryStoreSize'
- ref='MemoryStoreSize'
- placeholder={formatMessage(holders.memoryExample)}
- defaultValue={this.props.config.RateLimitSettings.MemoryStoreSize}
- onChange={this.handleChange}
- disabled={!this.state.EnableRateLimiter}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.rate.memoryDescription'
- defaultMessage='Maximum number of users sessions connected to the system as determined by "Vary By Remote Address" and "Vary By Header" settings below.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='VaryByRemoteAddr'
- >
- <FormattedMessage
- id='admin.rate.remoteTitle'
- defaultMessage='Vary By Remote Address: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='VaryByRemoteAddr'
- value='true'
- ref='VaryByRemoteAddr'
- defaultChecked={this.props.config.RateLimitSettings.VaryByRemoteAddr}
- onChange={this.handleChange.bind(this, 'VaryByRemoteAddrTrue')}
- disabled={!this.state.EnableRateLimiter}
- />
- <FormattedMessage
- id='admin.rate.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='VaryByRemoteAddr'
- value='false'
- defaultChecked={!this.props.config.RateLimitSettings.VaryByRemoteAddr}
- onChange={this.handleChange.bind(this, 'VaryByRemoteAddrFalse')}
- disabled={!this.state.EnableRateLimiter}
- />
- <FormattedMessage
- id='admin.rate.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.rate.remoteDescription'
- defaultMessage='When true, rate limit API access by IP address.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='VaryByHeader'
- >
- <FormattedMessage
- id='admin.rate.httpHeaderTitle'
- defaultMessage='Vary By HTTP Header:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='VaryByHeader'
- ref='VaryByHeader'
- placeholder={formatMessage(holders.httpHeaderExample)}
- defaultValue={this.props.config.RateLimitSettings.VaryByHeader}
- onChange={this.handleChange}
- disabled={!this.state.EnableRateLimiter || this.state.VaryByRemoteAddr}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.rate.httpHeaderDescription'
- defaultMessage='When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring NGINX set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.rate.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ <BooleanSetting
+ id='enableRateLimiter'
+ label={
+ <FormattedMessage
+ id='admin.rate.enableLimiterTitle'
+ defaultMessage='Enable Rate Limiter: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.rate.enableLimiterDescription'
+ defaultMessage='When true, APIs are throttled at rates specified below.'
+ />
+ }
+ value={this.state.enableRateLimiter}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='perSec'
+ label={
+ <FormattedMessage
+ id='admin.rate.queriesTitle'
+ defaultMessage='Number Of Queries Per Second:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.rate.queriesExample', 'Ex "10"')}
+ helpText={
+ <FormattedMessage
+ id='admin.rate.queriesDescription'
+ defaultMessage='Throttles API at this number of requests per second.'
+ />
+ }
+ value={this.state.perSec}
+ onChange={this.handleChange}
+ disabled={!this.state.enableRateLimiter}
+ />
+ <TextSetting
+ id='memoryStoreSize'
+ label={
+ <FormattedMessage
+ id='admin.rate.memoryTitle'
+ defaultMessage='Memory Store Size:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.rate.memoryExample', 'Ex "10000"')}
+ helpText={
+ <FormattedMessage
+ id='admin.rate.memoryDescription'
+ defaultMessage='Maximum number of users sessions connected to the system as determined by "Vary By Remote Address" and "Vary By Header" settings below.'
+ />
+ }
+ value={this.state.memoryStoreSize}
+ onChange={this.handleChange}
+ disabled={!this.state.enableRateLimiter}
+ />
+ <BooleanSetting
+ id='varyByRemoteAddr'
+ label={
+ <FormattedMessage
+ id='admin.rate.remoteTitle'
+ defaultMessage='Vary By Remote Address: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.rate.remoteDescription'
+ defaultMessage='When true, rate limit API access by IP address.'
+ />
+ }
+ value={this.state.varyByRemoteAddr}
+ onChange={this.handleChange}
+ disabled={!this.state.enableRateLimiter}
+ />
+ <TextSetting
+ id='varyByHeader'
+ label={
+ <FormattedMessage
+ id='admin.rate.httpHeaderTitle'
+ defaultMessage='Vary By HTTP Header:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.rate.httpHeaderExample', 'Ex "X-Real-IP", "X-Forwarded-For"')}
+ helpText={
+ <FormattedMessage
+ id='admin.rate.httpHeaderDescription'
+ defaultMessage='When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring NGINX set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'
+ />
+ }
+ value={this.state.varyByHeader}
+ onChange={this.handleChange}
+ disabled={!this.state.enableRateLimiter || this.state.varyByRemoteAddr}
+ />
+ </SettingsGroup>
);
}
-}
-
-RateSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(RateSettings); \ No newline at end of file
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/recycle_db.jsx b/webapp/components/admin_console/recycle_db.jsx
new file mode 100644
index 000000000..47ef2b0bf
--- /dev/null
+++ b/webapp/components/admin_console/recycle_db.jsx
@@ -0,0 +1,96 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Client from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class RecycleDbButton extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleRecycle = this.handleRecycle.bind(this);
+
+ this.state = {
+ loading: false,
+ fail: null
+ };
+ }
+
+ handleRecycle(e) {
+ e.preventDefault();
+
+ this.setState({
+ loading: true,
+ fail: null
+ });
+
+ Client.recycleDatabaseConnection(
+ () => {
+ this.setState({
+ loading: false
+ });
+ },
+ (err) => {
+ this.setState({
+ loading: false,
+ fail: err.message + ' - ' + err.detailed_error
+ });
+ }
+ );
+ }
+
+ render() {
+ let testMessage = null;
+ if (this.state.fail) {
+ testMessage = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-warning'></i>
+ <FormattedMessage
+ id='admin.recycle.reloadFail'
+ defaultMessage='Recycling unsuccessful: {error}'
+ values={{
+ error: this.state.fail
+ }}
+ />
+ </div>
+ );
+ }
+
+ let contents = null;
+ if (this.state.loading) {
+ contents = (
+ <span>
+ <span className='glyphicon glyphicon-refresh glyphicon-refresh-animate'/>
+ {Utils.localizeMessage('admin.recycle.loading', ' Recycling...')}
+ </span>
+ );
+ } else {
+ contents = (
+ <FormattedMessage
+ id='admin.recycle.button'
+ defaultMessage='Recycle Database Connections'
+ />
+ );
+ }
+
+ return (
+ <div className='form-group recycle-db'>
+ <div className='col-sm-offset-4 col-sm-8'>
+ <div>
+ <button
+ className='btn btn-default'
+ onClick={this.handleRecycle}
+ >
+ {contents}
+ </button>
+ {testMessage}
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/reload_config.jsx b/webapp/components/admin_console/reload_config.jsx
new file mode 100644
index 000000000..c137afaf9
--- /dev/null
+++ b/webapp/components/admin_console/reload_config.jsx
@@ -0,0 +1,96 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Client from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class ReloadConfigButton extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleReloadConfig = this.handleReloadConfig.bind(this);
+
+ this.state = {
+ loading: false,
+ fail: null
+ };
+ }
+
+ handleReloadConfig(e) {
+ e.preventDefault();
+
+ this.setState({
+ loading: true,
+ fail: null
+ });
+
+ Client.reloadConfig(
+ () => {
+ this.setState({
+ loading: false
+ });
+ },
+ (err) => {
+ this.setState({
+ loading: false,
+ fail: err.message + ' - ' + err.detailed_error
+ });
+ }
+ );
+ }
+
+ render() {
+ let testMessage = null;
+ if (this.state.fail) {
+ testMessage = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-warning'></i>
+ <FormattedMessage
+ id='admin.reload.reloadFail'
+ defaultMessage='Reload unsuccessful: {error}'
+ values={{
+ error: this.state.fail
+ }}
+ />
+ </div>
+ );
+ }
+
+ let contents = null;
+ if (this.state.loading) {
+ contents = (
+ <span>
+ <span className='glyphicon glyphicon-refresh glyphicon-refresh-animate'/>
+ {Utils.localizeMessage('admin.reload.loading', ' Loading...')}
+ </span>
+ );
+ } else {
+ contents = (
+ <FormattedMessage
+ id='admin.reload.button'
+ defaultMessage='Reload Configuration From Disk'
+ />
+ );
+ }
+
+ return (
+ <div className='form-group reload-config'>
+ <div className='col-sm-offset-4 col-sm-8'>
+ <div>
+ <button
+ className='btn btn-default'
+ onClick={this.handleReloadConfig}
+ >
+ {contents}
+ </button>
+ {testMessage}
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/save_button.jsx b/webapp/components/admin_console/save_button.jsx
new file mode 100644
index 000000000..18bb6e96d
--- /dev/null
+++ b/webapp/components/admin_console/save_button.jsx
@@ -0,0 +1,61 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class SaveButton extends React.Component {
+ static get propTypes() {
+ return {
+ saving: React.PropTypes.bool.isRequired,
+ disabled: React.PropTypes.bool
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ disabled: false
+ };
+ }
+
+ render() {
+ const {saving, disabled, ...props} = this.props; // eslint-disable-line no-use-before-define
+
+ let contents;
+ if (saving) {
+ contents = (
+ <span>
+ <span className='glyphicon glyphicon-refresh glyphicon-refresh-animate'/>
+ <FormattedMessage
+ id='admin.saving'
+ defaultMessage='Saving Config...'
+ />
+ </span>
+ );
+ } else {
+ contents = (
+ <FormattedMessage
+ id='admin.save'
+ defaultMessage='Save'
+ />
+ );
+ }
+
+ let className = 'save-button btn';
+ if (!disabled) {
+ className += ' btn-primary';
+ }
+
+ return (
+ <button
+ type='submit'
+ className={className}
+ disabled={disabled}
+ {...props}
+ >
+ {contents}
+ </button>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/service_settings.jsx b/webapp/components/admin_console/service_settings.jsx
deleted file mode 100644
index 90b6a39b4..000000000
--- a/webapp/components/admin_console/service_settings.jsx
+++ /dev/null
@@ -1,1042 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-
-const DefaultSessionLength = 30;
-const DefaultMaximumLoginAttempts = 10;
-const DefaultSessionCacheInMinutes = 10;
-
-var holders = defineMessages({
- listenExample: {
- id: 'admin.service.listenExample',
- defaultMessage: 'Ex ":8065"'
- },
- attemptExample: {
- id: 'admin.service.attemptExample',
- defaultMessage: 'Ex "10"'
- },
- segmentExample: {
- id: 'admin.service.segmentExample',
- defaultMessage: 'Ex "g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs"'
- },
- googleExample: {
- id: 'admin.service.googleExample',
- defaultMessage: 'Ex "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"'
- },
- sessionDaysEx: {
- id: 'admin.service.sessionDaysEx',
- defaultMessage: 'Ex "30"'
- },
- corsExample: {
- id: 'admin.service.corsEx',
- defaultMessage: 'http://example.com'
- },
- saving: {
- id: 'admin.service.saving',
- defaultMessage: 'Saving Config...'
- }
-});
-
-import React from 'react';
-
-class ServiceSettings extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
-
- this.state = {
- saveNeeded: false,
- serverError: null
- };
- }
-
- handleChange() {
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
- }
-
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.props.config;
- config.ServiceSettings.ListenAddress = ReactDOM.findDOMNode(this.refs.ListenAddress).value.trim();
- if (config.ServiceSettings.ListenAddress === '') {
- config.ServiceSettings.ListenAddress = ':8065';
- ReactDOM.findDOMNode(this.refs.ListenAddress).value = config.ServiceSettings.ListenAddress;
- }
-
- config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
- config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
- config.ServiceSettings.EnableIncomingWebhooks = ReactDOM.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
- config.ServiceSettings.EnableOutgoingWebhooks = ReactDOM.findDOMNode(this.refs.EnableOutgoingWebhooks).checked;
- config.ServiceSettings.EnablePostUsernameOverride = ReactDOM.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
- config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked;
- config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked;
- config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked;
- config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
- config.ServiceSettings.EnableInsecureOutgoingConnections = ReactDOM.findDOMNode(this.refs.EnableInsecureOutgoingConnections).checked;
- config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked;
- config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked;
-
- if (this.refs.EnableMultifactorAuthentication) {
- config.ServiceSettings.EnableMultifactorAuthentication = ReactDOM.findDOMNode(this.refs.EnableMultifactorAuthentication).checked;
- }
-
- //config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
-
- var MaximumLoginAttempts = DefaultMaximumLoginAttempts;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) {
- MaximumLoginAttempts = parseInt(ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value, 10);
- }
- if (MaximumLoginAttempts < 1) {
- MaximumLoginAttempts = 1;
- }
- config.ServiceSettings.MaximumLoginAttempts = MaximumLoginAttempts;
- ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value = MaximumLoginAttempts;
-
- var SessionLengthWebInDays = DefaultSessionLength;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value, 10))) {
- SessionLengthWebInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value, 10);
- }
- if (SessionLengthWebInDays < 1) {
- SessionLengthWebInDays = 1;
- }
- config.ServiceSettings.SessionLengthWebInDays = SessionLengthWebInDays;
- ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value = SessionLengthWebInDays;
-
- var SessionLengthMobileInDays = DefaultSessionLength;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value, 10))) {
- SessionLengthMobileInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value, 10);
- }
- if (SessionLengthMobileInDays < 1) {
- SessionLengthMobileInDays = 1;
- }
- config.ServiceSettings.SessionLengthMobileInDays = SessionLengthMobileInDays;
- ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value = SessionLengthMobileInDays;
-
- var SessionLengthSSOInDays = DefaultSessionLength;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value, 10))) {
- SessionLengthSSOInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value, 10);
- }
- if (SessionLengthSSOInDays < 1) {
- SessionLengthSSOInDays = 1;
- }
- config.ServiceSettings.SessionLengthSSOInDays = SessionLengthSSOInDays;
- ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value = SessionLengthSSOInDays;
-
- var SessionCacheInMinutes = DefaultSessionCacheInMinutes;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value, 10))) {
- SessionCacheInMinutes = parseInt(ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value, 10);
- }
- if (SessionCacheInMinutes < -1) {
- SessionCacheInMinutes = -1;
- }
- config.ServiceSettings.SessionCacheInMinutes = SessionCacheInMinutes;
- ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value = SessionCacheInMinutes;
-
- config.ServiceSettings.AllowCorsFrom = ReactDOM.findDOMNode(this.refs.AllowCorsFrom).value.trim();
-
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
- );
- }
-
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
- let mfaSetting;
- if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
- mfaSetting = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableMultifactorAuthentication'
- >
- <FormattedMessage
- id='admin.service.mfaTitle'
- defaultMessage='Enable Multi-factor Authentication:'
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableMultifactorAuthentication'
- value='true'
- ref='EnableMultifactorAuthentication'
- defaultChecked={this.props.config.ServiceSettings.EnableMultifactorAuthentication}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableMultifactorAuthentication'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableMultifactorAuthentication}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.mfaDesc'
- defaultMessage='When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.'
- />
- </p>
- </div>
- </div>
- );
- }
-
- return (
- <div className='wrapper--fixed'>
-
- <h3>
- <FormattedMessage
- id='admin.service.title'
- defaultMessage='Service Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ListenAddress'
- >
- <FormattedMessage
- id='admin.service.listenAddress'
- defaultMessage='Listen Address:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ListenAddress'
- ref='ListenAddress'
- placeholder={formatMessage(holders.listenExample)}
- defaultValue={this.props.config.ServiceSettings.ListenAddress}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.listenDescription'
- defaultMessage='The address to which to bind and listen. Entering ":8065" will bind to all interfaces or you can choose one like "127.0.0.1:8065". Changing this will require a server restart before taking effect.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='MaximumLoginAttempts'
- >
- <FormattedMessage
- id='admin.service.attemptTitle'
- defaultMessage='Maximum Login Attempts:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='MaximumLoginAttempts'
- ref='MaximumLoginAttempts'
- placeholder={formatMessage(holders.attemptExample)}
- defaultValue={this.props.config.ServiceSettings.MaximumLoginAttempts}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.attemptDescription'
- defaultMessage='Login attempts allowed before user is locked out and required to reset password via email.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SegmentDeveloperKey'
- >
- <FormattedMessage
- id='admin.service.segmentTitle'
- defaultMessage='Segment Developer Key:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SegmentDeveloperKey'
- ref='SegmentDeveloperKey'
- placeholder={formatMessage(holders.segmentExample)}
- defaultValue={this.props.config.ServiceSettings.SegmentDeveloperKey}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.segmentDescription'
- defaultMessage='For users running a SaaS services, sign up for a key at Segment.com to track metrics.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='GoogleDeveloperKey'
- >
- <FormattedMessage
- id='admin.service.googleTitle'
- defaultMessage='Google Developer Key:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='GoogleDeveloperKey'
- ref='GoogleDeveloperKey'
- placeholder={formatMessage(holders.googleExample)}
- defaultValue={this.props.config.ServiceSettings.GoogleDeveloperKey}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedHTMLMessage
- id='admin.service.googleDescription'
- defaultMessage='Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Leaving the field blank disables the automatic generation of YouTube video previews from links.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableIncomingWebhooks'
- >
- <FormattedMessage
- id='admin.service.webhooksTitle'
- defaultMessage='Enable Incoming Webhooks: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableIncomingWebhooks'
- value='true'
- ref='EnableIncomingWebhooks'
- defaultChecked={this.props.config.ServiceSettings.EnableIncomingWebhooks}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableIncomingWebhooks'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableIncomingWebhooks}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.webhooksDescription'
- defaultMessage='When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableOutgoingWebhooks'
- >
- <FormattedMessage
- id='admin.service.outWebhooksTitle'
- defaultMessage='Enable Outgoing Webhooks: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOutgoingWebhooks'
- value='true'
- ref='EnableOutgoingWebhooks'
- defaultChecked={this.props.config.ServiceSettings.EnableOutgoingWebhooks}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOutgoingWebhooks'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableOutgoingWebhooks}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.outWebhooksDesc'
- defaultMessage='When true, outgoing webhooks will be allowed.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableCommands'
- >
- <FormattedMessage
- id='admin.service.cmdsTitle'
- defaultMessage='Enable Slash Commands: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableCommands'
- value='true'
- ref='EnableCommands'
- defaultChecked={this.props.config.ServiceSettings.EnableCommands}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableCommands'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableCommands}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.cmdsDesc'
- defaultMessage='When true, user created slash commands will be allowed.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableOnlyAdminIntegrations'
- >
- <FormattedMessage
- id='admin.service.integrationAdmin'
- defaultMessage='Enable Integrations for Admin Only: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOnlyAdminIntegrations'
- value='true'
- ref='EnableOnlyAdminIntegrations'
- defaultChecked={this.props.config.ServiceSettings.EnableOnlyAdminIntegrations}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOnlyAdminIntegrations'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableOnlyAdminIntegrations}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.integrationAdminDesc'
- defaultMessage='When true, user created integrations can only be created by admins.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnablePostUsernameOverride'
- >
- <FormattedMessage
- id='admin.service.overrideTitle'
- defaultMessage='Enable Overriding Usernames from Webhooks and Slash Commands: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePostUsernameOverride'
- value='true'
- ref='EnablePostUsernameOverride'
- defaultChecked={this.props.config.ServiceSettings.EnablePostUsernameOverride}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePostUsernameOverride'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnablePostUsernameOverride}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.overrideDescription'
- defaultMessage='When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnablePostIconOverride'
- >
- <FormattedMessage
- id='admin.service.iconTitle'
- defaultMessage='Enable Overriding Icon from Webhooks and Slash Commands: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePostIconOverride'
- value='true'
- ref='EnablePostIconOverride'
- defaultChecked={this.props.config.ServiceSettings.EnablePostIconOverride}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePostIconOverride'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnablePostIconOverride}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.iconDescription'
- defaultMessage='When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableTesting'
- >
- <FormattedMessage
- id='admin.service.testingTitle'
- defaultMessage='Enable Testing: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableTesting'
- value='true'
- ref='EnableTesting'
- defaultChecked={this.props.config.ServiceSettings.EnableTesting}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableTesting'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableTesting}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.testingDescription'
- defaultMessage='(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableDeveloper'
- >
- <FormattedMessage
- id='admin.service.developerTitle'
- defaultMessage='Enable Developer Mode: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableDeveloper'
- value='true'
- ref='EnableDeveloper'
- defaultChecked={this.props.config.ServiceSettings.EnableDeveloper}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableDeveloper'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableDeveloper}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.developerDesc'
- defaultMessage='(Developer Option) When true, extra information around errors will be displayed in the UI.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableSecurityFixAlert'
- >
- <FormattedMessage
- id='admin.service.securityTitle'
- defaultMessage='Enable Security Alerts: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableSecurityFixAlert'
- value='true'
- ref='EnableSecurityFixAlert'
- defaultChecked={this.props.config.ServiceSettings.EnableSecurityFixAlert}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableSecurityFixAlert'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableSecurityFixAlert}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.securityDesc'
- defaultMessage='When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableInsecureOutgoingConnections'
- >
- <FormattedMessage
- id='admin.service.insecureTlsTitle'
- defaultMessage='Enable Insecure Outgoing Connections: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableInsecureOutgoingConnections'
- value='true'
- ref='EnableInsecureOutgoingConnections'
- defaultChecked={this.props.config.ServiceSettings.EnableInsecureOutgoingConnections}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableInsecureOutgoingConnections'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableInsecureOutgoingConnections}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.insecureTlsDesc'
- defaultMessage='When true, any outgoing HTTPS requests will accept unverified, self-signed certificates. For example, outgoing webhooks to a server with a self-signed TLS certificate, using any domain, will be allowed. Note that this makes these connections susceptible to man-in-the-middle attacks.'
- />
- </p>
- </div>
- </div>
-
- {mfaSetting}
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AllowCorsFrom'
- >
- <FormattedMessage
- id='admin.service.corsTitle'
- defaultMessage='Allow Cross-origin Requests from:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AllowCorsFrom'
- ref='AllowCorsFrom'
- placeholder={formatMessage(holders.corsExample)}
- defaultValue={this.props.config.ServiceSettings.AllowCorsFrom}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.corsDescription'
- defaultMessage='Enable HTTP Cross origin request from a specific domain. Use "*" if you want to allow CORS from any domain or leave it blank to disable it.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SessionLengthWebInDays'
- >
- <FormattedMessage
- id='admin.service.webSessionDays'
- defaultMessage='Session Length for Web in Days:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SessionLengthWebInDays'
- ref='SessionLengthWebInDays'
- placeholder={formatMessage(holders.sessionDaysEx)}
- defaultValue={this.props.config.ServiceSettings.SessionLengthWebInDays}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.webSessionDaysDesc'
- defaultMessage='The web session will expire after the number of days specified and will require a user to login again.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SessionLengthMobileInDays'
- >
- <FormattedMessage
- id='admin.service.mobileSessionDays'
- defaultMessage='Session Length for Mobile Device in Days:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SessionLengthMobileInDays'
- ref='SessionLengthMobileInDays'
- placeholder={formatMessage(holders.sessionDaysEx)}
- defaultValue={this.props.config.ServiceSettings.SessionLengthMobileInDays}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.mobileSessionDaysDesc'
- defaultMessage='The native mobile session will expire after the number of days specified and will require a user to login again.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SessionLengthSSOInDays'
- >
- <FormattedMessage
- id='admin.service.ssoSessionDays'
- defaultMessage='Session Length for SSO in Days:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SessionLengthSSOInDays'
- ref='SessionLengthSSOInDays'
- placeholder={formatMessage(holders.sessionDaysEx)}
- defaultValue={this.props.config.ServiceSettings.SessionLengthSSOInDays}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.ssoSessionDaysDesc'
- defaultMessage='The SSO session will expire after the number of days specified and will require a user to login again.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SessionCacheInMinutes'
- >
- <FormattedMessage
- id='admin.service.sessionCache'
- defaultMessage='Session Cache in Minutes:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SessionCacheInMinutes'
- ref='SessionCacheInMinutes'
- placeholder={formatMessage(holders.sessionDaysEx)}
- defaultValue={this.props.config.ServiceSettings.SessionCacheInMinutes}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.sessionCacheDesc'
- defaultMessage='The number of minutes to cache a session in memory.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.service.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
- );
- }
-}
-
-// <div className='form-group'>
-// <label
-// className='control-label col-sm-4'
-// htmlFor='EnableOAuthServiceProvider'
-// >
-// {'Enable OAuth Service Provider: '}
-// </label>
-// <div className='col-sm-8'>
-// <label className='radio-inline'>
-// <input
-// type='radio'
-// name='EnableOAuthServiceProvider'
-// value='true'
-// ref='EnableOAuthServiceProvider'
-// defaultChecked={this.props.config.ServiceSettings.EnableOAuthServiceProvider}
-// onChange={this.handleChange}
-// />
-// {'true'}
-// </label>
-// <label className='radio-inline'>
-// <input
-// type='radio'
-// name='EnableOAuthServiceProvider'
-// value='false'
-// defaultChecked={!this.props.config.ServiceSettings.EnableOAuthServiceProvider}
-// onChange={this.handleChange}
-// />
-// {'false'}
-// </label>
-// <p className='help-text'>{'When enabled Mattermost will act as an OAuth2 Provider. Changing this will require a server restart before taking effect.'}</p>
-// </div>
-// </div>
-
-ServiceSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(ServiceSettings);
diff --git a/webapp/components/admin_console/session_settings.jsx b/webapp/components/admin_console/session_settings.jsx
new file mode 100644
index 000000000..79f3c7ee5
--- /dev/null
+++ b/webapp/components/admin_console/session_settings.jsx
@@ -0,0 +1,134 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class SessionSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ sessionLengthWebInDays: props.config.ServiceSettings.SessionLengthWebInDays,
+ sessionLengthMobileInDays: props.config.ServiceSettings.SessionLengthMobileInDays,
+ sessionLengthSSOInDays: props.config.ServiceSettings.SessionLengthSSOInDays,
+ sessionCacheInMinutes: props.config.ServiceSettings.SessionCacheInMinutes
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.SessionLengthWebInDays = this.parseIntNonZero(this.state.sessionLengthWebInDays);
+ config.ServiceSettings.SessionLengthMobileInDays = this.parseIntNonZero(this.state.sessionLengthMobileInDays);
+ config.ServiceSettings.SessionLengthSSOInDays = this.parseIntNonZero(this.state.sessionLengthSSOInDays);
+ config.ServiceSettings.SessionCacheInMinutes = this.parseIntNonZero(this.state.sessionCacheInMinutes);
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.security.title'
+ defaultMessage='Security Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.security.session'
+ defaultMessage='Sessions'
+ />
+ }
+ >
+ <TextSetting
+ id='sessionLengthWebInDays'
+ label={
+ <FormattedMessage
+ id='admin.service.webSessionDays'
+ defaultMessage='Session Length for Web in Days:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.sessionDaysEx', 'Ex "30"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.webSessionDaysDesc'
+ defaultMessage='The web session will expire after the number of days specified and will require a user to login again.'
+ />
+ }
+ value={this.state.sessionLengthWebInDays}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='sessionLengthMobileInDays'
+ label={
+ <FormattedMessage
+ id='admin.service.mobileSessionDays'
+ defaultMessage='Session Length for Mobile Device in Days:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.sessionDaysEx', 'Ex "30"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.mobileSessionDaysDesc'
+ defaultMessage='The native mobile session will expire after the number of days specified and will require a user to login again.'
+ />
+ }
+ value={this.state.sessionLengthMobileInDays}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='sessionLengthSSOInDays'
+ label={
+ <FormattedMessage
+ id='admin.service.ssoSessionDays'
+ defaultMessage='Session Length for SSO in Days:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.sessionDaysEx', 'Ex "30"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.ssoSessionDaysDesc'
+ defaultMessage='The SSO session will expire after the number of days specified and will require a user to login again.'
+ />
+ }
+ value={this.state.sessionLengthSSOInDays}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='sessionCacheInMinutes'
+ label={
+ <FormattedMessage
+ id='admin.service.sessionCache'
+ defaultMessage='Session Cache in Minutes:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.sessionDaysEx', 'Ex "30"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.sessionCacheDesc'
+ defaultMessage='The number of minutes to cache a session in memory.'
+ />
+ }
+ value={this.state.sessionCacheInMinutes}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/setting.jsx b/webapp/components/admin_console/setting.jsx
index 7dee6c8dc..024111fa5 100644
--- a/webapp/components/admin_console/setting.jsx
+++ b/webapp/components/admin_console/setting.jsx
@@ -5,20 +5,19 @@ import React from 'react';
export default class Setting extends React.Component {
render() {
- let marginClass = '';
- if (this.props.margin === 'small') {
- marginClass = ' form-group--small';
- }
-
return (
- <div className={'form-group' + marginClass}>
+ <div className='form-group'>
<label
className='control-label col-sm-4'
+ htmlFor={this.props.inputId}
>
{this.props.label}
</label>
<div className='col-sm-8'>
{this.props.children}
+ <div className='help-text'>
+ {this.props.helpText}
+ </div>
</div>
</div>
);
@@ -28,7 +27,8 @@ Setting.defaultProps = {
};
Setting.propTypes = {
+ inputId: React.PropTypes.string,
label: React.PropTypes.node.isRequired,
children: React.PropTypes.node.isRequired,
- margin: React.PropTypes.oneOf(['', 'small'])
+ helpText: React.PropTypes.node
};
diff --git a/webapp/components/admin_console/settings_group.jsx b/webapp/components/admin_console/settings_group.jsx
new file mode 100644
index 000000000..10b3444d8
--- /dev/null
+++ b/webapp/components/admin_console/settings_group.jsx
@@ -0,0 +1,42 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+export default class SettingsGroup extends React.Component {
+ static get propTypes() {
+ return {
+ show: React.PropTypes.bool.isRequired,
+ header: React.PropTypes.node,
+ children: React.PropTypes.node
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ show: true
+ };
+ }
+
+ render() {
+ if (!this.props.show) {
+ return null;
+ }
+
+ let header = null;
+ if (this.props.header) {
+ header = (
+ <h4>
+ {this.props.header}
+ </h4>
+ );
+ }
+
+ return (
+ <div className='admin-settings__group'>
+ {header}
+ {this.props.children}
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/signup_settings.jsx b/webapp/components/admin_console/signup_settings.jsx
new file mode 100644
index 000000000..fd64e4ea5
--- /dev/null
+++ b/webapp/components/admin_console/signup_settings.jsx
@@ -0,0 +1,124 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import GeneratedSetting from './generated_setting.jsx';
+import SettingsGroup from './settings_group.jsx';
+
+export default class SignupSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ requireEmailVerification: props.config.EmailSettings.RequireEmailVerification,
+ inviteSalt: props.config.EmailSettings.InviteSalt,
+ enableOpenServer: props.config.TeamSettings.EnableOpenServer
+ });
+ }
+
+ getConfigFromState(config) {
+ config.EmailSettings.RequireEmailVerification = this.state.requireEmailVerification;
+ config.EmailSettings.InviteSalt = this.state.inviteSalt;
+ config.TeamSettings.EnableOpenServer = this.state.enableOpenServer;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.security.title'
+ defaultMessage='Security Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.security.signup'
+ defaultMessage='Signup'
+ />
+ }
+ >
+ <BooleanSetting
+ id='requireEmailVerification'
+ label={
+ <FormattedMessage
+ id='admin.email.requireVerificationTitle'
+ defaultMessage='Require Email Verification: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.requireVerificationDescription'
+ defaultMessage='Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.'
+ />
+ }
+ value={this.state.requireEmailVerification}
+ onChange={this.handleChange}
+ disabled={this.state.sendEmailNotifications}
+ disabledText={
+ <FormattedMessage
+ id='admin.security.requireEmailVerification.disabled'
+ defaultMessage='Email verification cannot be changed while sending emails is disabled.'
+ />
+ }
+ />
+ <GeneratedSetting
+ id='inviteSalt'
+ label={
+ <FormattedMessage
+ id='admin.email.inviteSaltTitle'
+ defaultMessage='Invite Salt:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.inviteSaltDescription'
+ defaultMessage='32-character salt added to signing of email invites. Randomly generated on install. Click "Re-Generate" to create new salt.'
+ />
+ }
+ value={this.state.inviteSalt}
+ onChange={this.handleChange}
+ disabled={this.state.sendEmailNotifications}
+ disabledText={
+ <FormattedMessage
+ id='admin.security.inviteSalt.disabled'
+ defaultMessage='Invite salt cannot be changed while sending emails is disabled.'
+ />
+ }
+ />
+ <BooleanSetting
+ id='enableOpenServer'
+ label={
+ <FormattedMessage
+ id='admin.team.openServerTitle'
+ defaultMessage='Enable Open Server: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.openServerDescription'
+ defaultMessage='When true, anyone can signup for a user account on this server without the need to be invited.'
+ />
+ }
+ value={this.state.enableOpenServer}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/sql_settings.jsx b/webapp/components/admin_console/sql_settings.jsx
deleted file mode 100644
index f2e005b83..000000000
--- a/webapp/components/admin_console/sql_settings.jsx
+++ /dev/null
@@ -1,390 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import crypto from 'crypto';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-
-const holders = defineMessages({
- warning: {
- id: 'admin.sql.warning',
- defaultMessage: 'Warning: re-generating this salt may cause some columns in the database to return empty results.'
- },
- maxConnectionsExample: {
- id: 'admin.sql.maxConnectionsExample',
- defaultMessage: 'Ex "10"'
- },
- maxOpenExample: {
- id: 'admin.sql.maxOpenExample',
- defaultMessage: 'Ex "10"'
- },
- keyExample: {
- id: 'admin.sql.keyExample',
- defaultMessage: 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
- },
- saving: {
- id: 'admin.sql.saving',
- defaultMessage: 'Saving Config...'
- }
-});
-
-import React from 'react';
-
-class SqlSettings extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleGenerate = this.handleGenerate.bind(this);
-
- this.state = {
- saveNeeded: false,
- serverError: null
- };
- }
-
- handleChange() {
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
- }
-
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.props.config;
- config.SqlSettings.Trace = ReactDOM.findDOMNode(this.refs.Trace).checked;
- config.SqlSettings.AtRestEncryptKey = ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value.trim();
-
- if (config.SqlSettings.AtRestEncryptKey === '') {
- config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32);
- ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey;
- }
-
- var MaxOpenConns = 10;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10))) {
- MaxOpenConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10);
- }
- config.SqlSettings.MaxOpenConns = MaxOpenConns;
- ReactDOM.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns;
-
- var MaxIdleConns = 10;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10))) {
- MaxIdleConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10);
- }
- config.SqlSettings.MaxIdleConns = MaxIdleConns;
- ReactDOM.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns;
-
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
- );
- }
-
- handleGenerate(e) {
- e.preventDefault();
-
- var cfm = global.window.confirm(this.props.intl.formatMessage(holders.warning));
- if (cfm === false) {
- return;
- }
-
- ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
- }
-
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
- var dataSource = '**********' + this.props.config.SqlSettings.DataSource.substring(this.props.config.SqlSettings.DataSource.indexOf('@'));
-
- var dataSourceReplicas = '';
- this.props.config.SqlSettings.DataSourceReplicas.forEach((replica) => {
- dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] ';
- });
-
- if (this.props.config.SqlSettings.DataSourceReplicas.length === 0) {
- dataSourceReplicas = 'none';
- }
-
- return (
- <div className='wrapper--fixed'>
-
- <div className='banner'>
- <div className='banner__content'>
- <h4 className='banner__heading'>
- <FormattedMessage
- id='admin.sql.noteTitle'
- defaultMessage='Note:'
- />
- </h4>
- <p>
- <FormattedMessage
- id='admin.sql.noteDescription'
- defaultMessage='Changing properties in this section will require a server restart before taking effect.'
- />
- </p>
- </div>
- </div>
-
- <h3>
- <FormattedMessage
- id='admin.sql.title'
- defaultMessage='SQL Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='DriverName'
- >
- <FormattedMessage
- id='admin.sql.driverName'
- defaultMessage='Driver Name:'
- />
- </label>
- <div className='col-sm-8'>
- <p className='help-text'>{this.props.config.SqlSettings.DriverName}</p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='DataSource'
- >
- <FormattedMessage
- id='admin.sql.dataSource'
- defaultMessage='Data Source:'
- />
- </label>
- <div className='col-sm-8'>
- <p className='help-text'>{dataSource}</p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='DataSourceReplicas'
- >
- <FormattedMessage
- id='admin.sql.replicas'
- defaultMessage='Data Source Replicas:'
- />
- </label>
- <div className='col-sm-8'>
- <p className='help-text'>{dataSourceReplicas}</p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='MaxIdleConns'
- >
- <FormattedMessage
- id='admin.sql.maxConnectionsTitle'
- defaultMessage='Maximum Idle Connections:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='MaxIdleConns'
- ref='MaxIdleConns'
- placeholder={formatMessage(holders.maxConnectionsExample)}
- defaultValue={this.props.config.SqlSettings.MaxIdleConns}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.sql.maxConnectionsDescription'
- defaultMessage='Maximum number of idle connections held open to the database.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='MaxOpenConns'
- >
- <FormattedMessage
- id='admin.sql.maxOpenTitle'
- defaultMessage='Maximum Open Connections:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='MaxOpenConns'
- ref='MaxOpenConns'
- placeholder={formatMessage(holders.maxOpenExample)}
- defaultValue={this.props.config.SqlSettings.MaxOpenConns}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.sql.maxOpenDescription'
- defaultMessage='Maximum number of open connections held open to the database.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AtRestEncryptKey'
- >
- <FormattedMessage
- id='admin.sql.keyTitle'
- defaultMessage='At Rest Encrypt Key:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AtRestEncryptKey'
- ref='AtRestEncryptKey'
- placeholder={formatMessage(holders.keyExample)}
- defaultValue={this.props.config.SqlSettings.AtRestEncryptKey}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.sql.keyDescription'
- defaultMessage='32-character salt available to encrypt and decrypt sensitive fields in database.'
- />
- </p>
- <div className='help-text'>
- <button
- className='btn btn-default'
- onClick={this.handleGenerate}
- >
- <FormattedMessage
- id='admin.sql.regenerate'
- defaultMessage='Re-Generate'
- />
- </button>
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Trace'
- >
- <FormattedMessage
- id='admin.sql.traceTitle'
- defaultMessage='Trace: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Trace'
- value='true'
- ref='Trace'
- defaultChecked={this.props.config.SqlSettings.Trace}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.sql.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Trace'
- value='false'
- defaultChecked={!this.props.config.SqlSettings.Trace}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.sql.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.sql.traceDescription'
- defaultMessage='(Development Mode) When true, executing SQL statements are written to the log.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.sql.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
- );
- }
-}
-
-SqlSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(SqlSettings); \ No newline at end of file
diff --git a/webapp/components/admin_console/storage_settings.jsx b/webapp/components/admin_console/storage_settings.jsx
new file mode 100644
index 000000000..7cfa9cf3b
--- /dev/null
+++ b/webapp/components/admin_console/storage_settings.jsx
@@ -0,0 +1,200 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import DropdownSetting from './dropdown_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+const DRIVER_LOCAL = 'local';
+const DRIVER_S3 = 'amazons3';
+
+export default class StorageSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ maxFileSize: props.config.FileSettings.MaxFileSize,
+ driverName: props.config.FileSettings.DriverName,
+ directory: props.config.FileSettings.Directory,
+ amazonS3AccessKeyId: props.config.FileSettings.AmazonS3AccessKeyId,
+ amazonS3SecretAccessKey: props.config.FileSettings.AmazonS3SecretAccessKey,
+ amazonS3Bucket: props.config.FileSettings.AmazonS3Bucket,
+ amazonS3Region: props.config.FileSettings.AmazonS3Region
+ });
+ }
+
+ getConfigFromState(config) {
+ config.FileSettings.MaxFileSize = this.parseInt(this.state.maxFileSize);
+ config.FileSettings.DriverName = this.state.driverName;
+ config.FileSettings.Directory = this.state.directory;
+ config.FileSettings.AmazonS3AccessKeyId = this.state.amazonS3AccessKeyId;
+ config.FileSettings.AmazonS3SecretAccessKey = this.state.amazonS3SecretAccessKey;
+ config.FileSettings.AmazonS3Bucket = this.state.amazonS3Bucket;
+ config.FileSettings.AmazonS3Region = this.state.amazonS3Region;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.files.title'
+ defaultMessage='File Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.files.storage'
+ defaultMessage='Storage'
+ />
+ }
+ >
+ <TextSetting
+ id='maxFileSize'
+ label={
+ <FormattedMessage
+ id='admin.image.maxFileSizeTitle'
+ defaultMessage='Max File Size:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.maxFileSizeExample', 'Ex "52428800"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.maxFileSizeDescription'
+ defaultMessage='Max File Size in bytes. If blank, will be set to 52428800 (50MB).'
+ />
+ }
+ value={this.state.maxFileSize}
+ onChange={this.handleChange}
+ />
+ <DropdownSetting
+ id='driverName'
+ values={[
+ {value: DRIVER_LOCAL, text: Utils.localizeMessage('admin.image.storeLocal', 'Local File System')},
+ {value: DRIVER_S3, text: Utils.localizeMessage('admin.image.storeAmazonS3', 'Amazon S3')}
+ ]}
+ label={
+ <FormattedMessage
+ id='admin.image.storeTitle'
+ defaultMessage='Store Files In:'
+ />
+ }
+ value={this.state.driverName}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='directory'
+ label={
+ <FormattedMessage
+ id='admin.image.localTitle'
+ defaultMessage='Local Directory Location:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.localExample', 'Ex "./data/"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.localDescription'
+ defaultMessage='Directory to which image files are written. If blank, will be set to ./data/.'
+ />
+ }
+ value={this.state.directory}
+ onChange={this.handleChange}
+ disabled={this.state.driverName !== DRIVER_LOCAL}
+ />
+ <TextSetting
+ id='amazonS3AccessKeyId'
+ label={
+ <FormattedMessage
+ id='admin.image.amazonS3IdTitle'
+ defaultMessage='Amazon S3 Access Key Id:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.amazonS3IdExample', 'Ex "AKIADTOVBGERKLCBV"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.amazonS3IdDescription'
+ defaultMessage='Obtain this credential from your Amazon EC2 administrator.'
+ />
+ }
+ value={this.state.amazonS3AccessKeyId}
+ onChange={this.handleChange}
+ disabled={this.state.driverName !== DRIVER_S3}
+ />
+ <TextSetting
+ id='amazonS3SecretAccessKey'
+ label={
+ <FormattedMessage
+ id='admin.image.amazonS3SecretTitle'
+ defaultMessage='Amazon S3 Secret Access Key:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.amazonS3SecretExample', 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.amazonS3SecretDescription'
+ defaultMessage='Obtain this credential from your Amazon EC2 administrator.'
+ />
+ }
+ value={this.state.amazonS3SecretAccessKey}
+ onChange={this.handleChange}
+ disabled={this.state.driverName !== DRIVER_S3}
+ />
+ <TextSetting
+ id='amazonS3Bucket'
+ label={
+ <FormattedMessage
+ id='admin.image.amazonS3BucketTitle'
+ defaultMessage='Amazon S3 Bucket:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.amazonS3BucketExample', 'Ex "mattermost-media"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.amazonS3BucketDescription'
+ defaultMessage='Name you selected for your S3 bucket in AWS.'
+ />
+ }
+ value={this.state.amazonS3Bucket}
+ onChange={this.handleChange}
+ disabled={this.state.driverName !== DRIVER_S3}
+ />
+ <TextSetting
+ id='amazonS3Region'
+ label={
+ <FormattedMessage
+ id='admin.image.amazonS3RegionTitle'
+ defaultMessage='Amazon S3 Region:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.amazonS3RegionExample', 'Ex "us-east-1"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.amazonS3RegionDescription'
+ defaultMessage='AWS region you selected for creating your S3 bucket.'
+ />
+ }
+ value={this.state.amazonS3Region}
+ onChange={this.handleChange}
+ disabled={this.state.driverName !== DRIVER_S3}
+ />
+ </SettingsGroup>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/team_settings.jsx b/webapp/components/admin_console/team_settings.jsx
deleted file mode 100644
index e7bfcd74a..000000000
--- a/webapp/components/admin_console/team_settings.jsx
+++ /dev/null
@@ -1,735 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import * as Utils from 'utils/utils.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-
-const holders = defineMessages({
- siteNameExample: {
- id: 'admin.team.siteNameExample',
- defaultMessage: 'Ex "Mattermost"'
- },
- maxUsersExample: {
- id: 'admin.team.maxUsersExample',
- defaultMessage: 'Ex "25"'
- },
- restrictExample: {
- id: 'admin.team.restrictExample',
- defaultMessage: 'Ex "corp.mattermost.com, mattermost.org"'
- },
- saving: {
- id: 'admin.team.saving',
- defaultMessage: 'Saving Config...'
- },
- restrictDirectMessageAny: {
- id: 'admin.team.restrict_direct_message_any',
- defaultMessage: 'Any user on the Mattermost server'
- },
- restrictDirectMessageTeam: {
- id: 'admin.team.restrict_direct_message_team',
- defaultMessage: 'Any member of the team'
- }
-});
-
-import React from 'react';
-
-const ENABLE_BRAND_ACTION = 'enable_brand_action';
-const DISABLE_BRAND_ACTION = 'disable_brand_action';
-
-class TeamSettings extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleImageChange = this.handleImageChange.bind(this);
- this.handleImageSubmit = this.handleImageSubmit.bind(this);
-
- this.uploading = false;
- this.timestamp = 0;
-
- this.state = {
- saveNeeded: false,
- brandImageExists: false,
- enableCustomBrand: this.props.config.TeamSettings.EnableCustomBrand,
- restrictDirectMessage: this.props.config.TeamSettings.RestrictDirectMessage,
- serverError: null
- };
- }
-
- componentWillMount() {
- if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true') {
- $.get(Client.getAdminRoute() + '/get_brand_image').done(() => this.setState({brandImageExists: true}));
- }
- }
-
- componentDidUpdate() {
- if (this.refs.image) {
- const reader = new FileReader();
-
- const img = this.refs.image;
- reader.onload = (e) => {
- $(img).attr('src', e.target.result);
- };
-
- reader.readAsDataURL(this.state.brandImage);
- }
- }
-
- handleChange(action) {
- var s = {saveNeeded: true};
-
- if (action === ENABLE_BRAND_ACTION) {
- s.enableCustomBrand = true;
- }
-
- if (action === DISABLE_BRAND_ACTION) {
- s.enableCustomBrand = false;
- }
-
- this.setState(s);
- }
-
- handleImageChange() {
- const element = $(this.refs.fileInput);
- if (element.prop('files').length > 0) {
- this.setState({fileSelected: true, brandImage: element.prop('files')[0]});
- }
- $('#upload-button').button('reset');
- }
-
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.props.config;
- config.TeamSettings.SiteName = this.refs.SiteName.value.trim();
- config.TeamSettings.RestrictCreationToDomains = this.refs.RestrictCreationToDomains.value.trim();
- config.TeamSettings.EnableTeamCreation = this.refs.EnableTeamCreation.checked;
- config.TeamSettings.EnableUserCreation = this.refs.EnableUserCreation.checked;
- config.TeamSettings.EnableOpenServer = this.refs.EnableOpenServer.checked;
- config.TeamSettings.RestrictTeamNames = this.refs.RestrictTeamNames.checked;
- config.TeamSettings.RestrictDirectMessage = this.refs.RestrictDirectMessage.value.trim();
-
- if (this.refs.EnableCustomBrand) {
- config.TeamSettings.EnableCustomBrand = this.refs.EnableCustomBrand.checked;
- }
-
- if (this.refs.CustomBrandText) {
- config.TeamSettings.CustomBrandText = this.refs.CustomBrandText.value;
- }
-
- var MaxUsersPerTeam = 50;
- if (!isNaN(parseInt(this.refs.MaxUsersPerTeam.value, 10))) {
- MaxUsersPerTeam = parseInt(this.refs.MaxUsersPerTeam.value, 10);
- }
- config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam;
- this.refs.MaxUsersPerTeam.value = MaxUsersPerTeam;
-
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
- );
- }
-
- handleImageSubmit(e) {
- e.preventDefault();
-
- if (!this.state.brandImage) {
- return;
- }
-
- if (this.uploading) {
- return;
- }
-
- $('#upload-button').button('loading');
- this.uploading = true;
-
- Client.uploadBrandImage(this.state.brandImage,
- () => {
- $('#upload-button').button('complete');
- this.timestamp = Utils.getTimestamp();
- this.setState({brandImageExists: true, brandImage: null});
- this.uploading = false;
- },
- (err) => {
- $('#upload-button').button('reset');
- this.uploading = false;
- this.setState({serverImageError: err.message});
- }
- );
- }
-
- createBrandSettings() {
- var btnClass = 'btn';
- if (this.state.fileSelected) {
- btnClass = 'btn btn-primary';
- }
-
- var serverImageError = '';
- if (this.state.serverImageError) {
- serverImageError = <div className='form-group has-error'><label className='control-label'>{this.state.serverImageError}</label></div>;
- }
-
- let uploadImage;
- let uploadText;
- if (this.state.enableCustomBrand) {
- let img;
- if (this.state.brandImage) {
- img = (
- <img
- ref='image'
- className='brand-img'
- src=''
- />
- );
- } else if (this.state.brandImageExists) {
- img = (
- <img
- className='brand-img'
- src={Client.getAdminRoute() + '/get_brand_image?t=' + this.timestamp}
- />
- );
- } else {
- img = (
- <p>
- <FormattedMessage
- id='admin.team.noBrandImage'
- defaultMessage='No brand image uploaded'
- />
- </p>
- );
- }
-
- uploadImage = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='CustomBrandImage'
- >
- <FormattedMessage
- id='admin.team.brandImageTitle'
- defaultMessage='Custom Brand Image:'
- />
- </label>
- <div className='col-sm-8'>
- {img}
- </div>
- <div className='col-sm-4'/>
- <div className='col-sm-8'>
- <div className='file__upload'>
- <button className='btn btn-default'>
- <FormattedMessage
- id='admin.team.chooseImage'
- defaultMessage='Choose New Image'
- />
- </button>
- <input
- ref='fileInput'
- type='file'
- accept='.jpg,.png,.bmp'
- onChange={this.handleImageChange}
- />
- </div>
- <button
- className={btnClass}
- disabled={!this.state.fileSelected}
- onClick={this.handleImageSubmit}
- id='upload-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + Utils.localizeMessage('admin.team.uploading', 'Uploading..')}
- data-complete-text={'<span class=\'glyphicon glyphicon-ok\'></span> ' + Utils.localizeMessage('admin.team.uploaded', 'Uploaded!')}
- >
- <FormattedMessage
- id='admin.team.upload'
- defaultMessage='Upload'
- />
- </button>
- <br/>
- {serverImageError}
- <p className='help-text no-margin'>
- <FormattedHTMLMessage
- id='admin.team.uploadDesc'
- defaultMessage='Customize your user experience by adding a custom image to your login screen. See examples at <a href="http://docs.mattermost.com/administration/config-settings.html#custom-branding" target="_blank">docs.mattermost.com/administration/config-settings.html#custom-branding</a>.'
- />
- </p>
- </div>
- </div>
- );
-
- uploadText = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='CustomBrandText'
- >
- <FormattedMessage
- id='admin.team.brandTextTitle'
- defaultMessage='Custom Brand Text:'
- />
- </label>
- <div className='col-sm-8'>
- <textarea
- type='text'
- rows='5'
- maxLength='1024'
- className='form-control admin-textarea'
- id='CustomBrandText'
- ref='CustomBrandText'
- onChange={this.handleChange}
- >
- {this.props.config.TeamSettings.CustomBrandText}
- </textarea>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.brandTextDescription'
- defaultMessage='The custom branding Markdown-formatted text you would like to appear below your custom brand image on your login sreen.'
- />
- </p>
- </div>
- </div>
- );
- }
-
- return (
- <div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableCustomBrand'
- >
- <FormattedMessage
- id='admin.team.brandTitle'
- defaultMessage='Enable Custom Branding: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableCustomBrand'
- value='true'
- ref='EnableCustomBrand'
- defaultChecked={this.props.config.TeamSettings.EnableCustomBrand}
- onChange={this.handleChange.bind(this, ENABLE_BRAND_ACTION)}
- />
- <FormattedMessage
- id='admin.team.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableCustomBrand'
- value='false'
- defaultChecked={!this.props.config.TeamSettings.EnableCustomBrand}
- onChange={this.handleChange.bind(this, DISABLE_BRAND_ACTION)}
- />
- <FormattedMessage
- id='admin.team.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.brandDesc'
- defaultMessage='Enable custom branding to show an image of your choice, uploaded below, and some help text, written below, on the login page.'
- />
- </p>
- </div>
- </div>
-
- {uploadImage}
- {uploadText}
- </div>
- );
- }
-
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
- let brand;
- if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true') {
- brand = this.createBrandSettings();
- }
-
- return (
- <div className='wrapper--fixed'>
-
- <h3>
- <FormattedMessage
- id='admin.team.title'
- defaultMessage='Team Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SiteName'
- >
- <FormattedMessage
- id='admin.team.siteNameTitle'
- defaultMessage='Site Name:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SiteName'
- ref='SiteName'
- placeholder={formatMessage(holders.siteNameExample)}
- defaultValue={this.props.config.TeamSettings.SiteName}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.siteNameDescription'
- defaultMessage='Name of service shown in login screens and UI.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='MaxUsersPerTeam'
- >
- <FormattedMessage
- id='admin.team.maxUsersTitle'
- defaultMessage='Max Users Per Team:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='MaxUsersPerTeam'
- ref='MaxUsersPerTeam'
- placeholder={formatMessage(holders.maxUsersExample)}
- defaultValue={this.props.config.TeamSettings.MaxUsersPerTeam}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.maxUsersDescription'
- defaultMessage='Maximum total number of users per team, including both active and inactive users.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableTeamCreation'
- >
- <FormattedMessage
- id='admin.team.teamCreationTitle'
- defaultMessage='Enable Team Creation: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableTeamCreation'
- value='true'
- ref='EnableTeamCreation'
- defaultChecked={this.props.config.TeamSettings.EnableTeamCreation}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableTeamCreation'
- value='false'
- defaultChecked={!this.props.config.TeamSettings.EnableTeamCreation}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.teamCreationDescription'
- defaultMessage='When false, the ability to create teams is disabled. The create team button displays error when pressed.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableUserCreation'
- >
- <FormattedMessage
- id='admin.team.userCreationTitle'
- defaultMessage='Enable User Creation: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableUserCreation'
- value='true'
- ref='EnableUserCreation'
- defaultChecked={this.props.config.TeamSettings.EnableUserCreation}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableUserCreation'
- value='false'
- defaultChecked={!this.props.config.TeamSettings.EnableUserCreation}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.userCreationDescription'
- defaultMessage='When false, the ability to create accounts is disabled. The create account button displays error when pressed.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableOpenServer'
- >
- <FormattedMessage
- id='admin.team.openServerTitle'
- defaultMessage='Enable Open Server: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOpenServer'
- value='true'
- ref='EnableOpenServer'
- defaultChecked={this.props.config.TeamSettings.EnableOpenServer}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOpenServer'
- value='false'
- defaultChecked={!this.props.config.TeamSettings.EnableOpenServer}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.openServerDescription'
- defaultMessage='When true, anyone can signup for a user account on this server without the need to be invited.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='RestrictCreationToDomains'
- >
- <FormattedMessage
- id='admin.team.restrictTitle'
- defaultMessage='Restrict Creation To Domains:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='RestrictCreationToDomains'
- ref='RestrictCreationToDomains'
- placeholder={formatMessage(holders.restrictExample)}
- defaultValue={this.props.config.TeamSettings.RestrictCreationToDomains}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.restrictDescription'
- defaultMessage='Teams and user accounts can only be created from a specific domain (e.g. "mattermost.org") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='RestrictTeamNames'
- >
- <FormattedMessage
- id='admin.team.restrictNameTitle'
- defaultMessage='Restrict Team Names: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='RestrictTeamNames'
- value='true'
- ref='RestrictTeamNames'
- defaultChecked={this.props.config.TeamSettings.RestrictTeamNames}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='RestrictTeamNames'
- value='false'
- defaultChecked={!this.props.config.TeamSettings.RestrictTeamNames}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.restrictNameDesc'
- defaultMessage='When true, You cannot create a team name with reserved words like www, admin, support, test, channel, etc'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='restrictDirectMessage'
- >
- <FormattedMessage
- id='admin.team.restrictDirectMessage'
- defaultMessage='Enable users to open Direct Message channels with:'
- />
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='restrictDirectMessage'
- ref='RestrictDirectMessage'
- defaultValue={this.props.config.TeamSettings.RestrictDirectMessage}
- onChange={this.handleChange.bind(this, 'restrictDirectMessage')}
- >
- <option value='any'>{formatMessage(holders.restrictDirectMessageAny)}</option>
- <option value='team'>{formatMessage(holders.restrictDirectMessageTeam)}</option>
- </select>
- <p className='help-text'>
- <FormattedHTMLMessage
- id='admin.team.restrictDirectMessageDesc'
- defaultMessage='"Any user on the Mattermost server" enables users to open a Direct Message channel with any user on the server, even if they are not on any teams together. "Any member of the team" limits the ability to open Direct Message channels to only users who are in the same team.'
- />
- </p>
- </div>
- </div>
-
- {brand}
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.team.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
- );
- }
-}
-
-TeamSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(TeamSettings);
diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx
index 00aa1a832..89fbd0e3a 100644
--- a/webapp/components/admin_console/team_users.jsx
+++ b/webapp/components/admin_console/team_users.jsx
@@ -1,7 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import AdminStore from 'stores/admin_store.jsx';
import Client from 'utils/web_client.jsx';
+import FormError from 'components/form_error.jsx';
import LoadingScreen from '../loading_screen.jsx';
import UserItem from './user_item.jsx';
import ResetPasswordModal from './reset_password_modal.jsx';
@@ -11,9 +13,17 @@ import {FormattedMessage} from 'react-intl';
import React from 'react';
export default class UserList extends React.Component {
+ static get propTypes() {
+ return {
+ params: React.PropTypes.object.isRequired
+ };
+ }
+
constructor(props) {
super(props);
+ this.onAllTeamsChange = this.onAllTeamsChange.bind(this);
+
this.getTeamProfiles = this.getTeamProfiles.bind(this);
this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
this.doPasswordReset = this.doPasswordReset.bind(this);
@@ -22,7 +32,7 @@ export default class UserList extends React.Component {
this.getTeamMemberForUser = this.getTeamMemberForUser.bind(this);
this.state = {
- teamId: props.team.id,
+ team: AdminStore.getTeam(this.props.params.team),
users: null,
teamMembers: null,
serverError: null,
@@ -35,8 +45,14 @@ export default class UserList extends React.Component {
this.getCurrentTeamProfiles();
}
+ onAllTeamsChange() {
+ this.setState({
+ team: AdminStore.getTeam(this.props.params.team)
+ });
+ }
+
getCurrentTeamProfiles() {
- this.getTeamProfiles(this.props.team.id);
+ this.getTeamProfiles(this.props.params.team);
}
getTeamProfiles(teamId) {
@@ -133,9 +149,8 @@ export default class UserList extends React.Component {
}
render() {
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ if (!this.state.team) {
+ return null;
}
if (this.state.users == null || this.state.teamMembers == null) {
@@ -146,11 +161,11 @@ export default class UserList extends React.Component {
id='admin.userList.title'
defaultMessage='Users for {team}'
values={{
- team: this.props.team.name
+ team: this.state.team.name
}}
/>
</h3>
- {serverError}
+ <FormError error={this.state.serverError}/>
<LoadingScreen/>
</div>
);
@@ -161,7 +176,7 @@ export default class UserList extends React.Component {
return (
<UserItem
- team={this.props.team}
+ team={this.state.team}
key={'user_' + user.id}
user={user}
teamMember={teamMember}
@@ -177,12 +192,12 @@ export default class UserList extends React.Component {
id='admin.userList.title2'
defaultMessage='Users for {team} ({count})'
values={{
- team: this.props.team.name,
+ team: this.state.team.name,
count: this.state.users.length
}}
/>
</h3>
- {serverError}
+ <FormError error={this.state.serverError}/>
<form
className='form-horizontal'
role='form'
@@ -194,7 +209,7 @@ export default class UserList extends React.Component {
<ResetPasswordModal
user={this.state.user}
show={this.state.showPasswordModal}
- team={this.props.team}
+ team={this.state.team}
onModalSubmit={this.doPasswordResetSubmit}
onModalDismissed={this.doPasswordResetDismiss}
/>
@@ -202,7 +217,3 @@ export default class UserList extends React.Component {
);
}
}
-
-UserList.propTypes = {
- team: React.PropTypes.object
-};
diff --git a/webapp/components/admin_console/text_setting.jsx b/webapp/components/admin_console/text_setting.jsx
new file mode 100644
index 000000000..bb37f8e29
--- /dev/null
+++ b/webapp/components/admin_console/text_setting.jsx
@@ -0,0 +1,83 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Setting from './setting.jsx';
+
+export default class TextSetting extends React.Component {
+ static get propTypes() {
+ return {
+ id: React.PropTypes.string.isRequired,
+ label: React.PropTypes.node.isRequired,
+ placeholder: React.PropTypes.string,
+ helpText: React.PropTypes.node,
+ value: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ React.PropTypes.number
+ ]).isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
+ type: React.PropTypes.oneOf([
+ 'input',
+ 'textarea'
+ ])
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ type: 'input'
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value);
+ }
+
+ render() {
+ let input = null;
+ if (this.props.type === 'input') {
+ input = (
+ <input
+ id={this.props.id}
+ className='form-control'
+ type='text'
+ placeholder={this.props.placeholder}
+ value={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ );
+ } else if (this.props.type === 'textarea') {
+ input = (
+ <textarea
+ id={this.props.id}
+ className='form-control'
+ rows='5'
+ maxLength='1024'
+ placeholder={this.props.placeholder}
+ value={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ );
+ }
+
+ return (
+ <Setting
+ label={this.props.label}
+ helpText={this.props.helpText}
+ inputId={this.props.id}
+ >
+ {input}
+ </Setting>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx
index ef6bd9f45..affd4b5a4 100644
--- a/webapp/components/admin_console/user_item.jsx
+++ b/webapp/components/admin_console/user_item.jsx
@@ -224,8 +224,8 @@ export default class UserItem extends React.Component {
let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin';
let showMakeActive = false;
let showMakeNotActive = user.roles !== 'system_admin';
- let mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true';
- let showMfaReset = mfaEnabled && user.mfa_active;
+ const mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true';
+ const showMfaReset = mfaEnabled && user.mfa_active;
if (user.delete_at > 0) {
currentRoles = (
diff --git a/webapp/components/admin_console/users_and_teams_settings.jsx b/webapp/components/admin_console/users_and_teams_settings.jsx
new file mode 100644
index 000000000..a7f703820
--- /dev/null
+++ b/webapp/components/admin_console/users_and_teams_settings.jsx
@@ -0,0 +1,179 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import DropdownSetting from './dropdown_setting.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+const RESTRICT_DIRECT_MESSAGE_ANY = 'any';
+const RESTRICT_DIRECT_MESSAGE_TEAM = 'team';
+
+export default class UsersAndTeamsSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enableUserCreation: props.config.TeamSettings.EnableUserCreation,
+ enableTeamCreation: props.config.TeamSettings.EnableTeamCreation,
+ maxUsersPerTeam: props.config.TeamSettings.MaxUsersPerTeam,
+ restrictCreationToDomains: props.config.TeamSettings.RestrictCreationToDomains,
+ restrictTeamNames: props.config.TeamSettings.RestrictTeamNames,
+ restrictDirectMessage: props.config.TeamSettings.RestrictDirectMessage
+ });
+ }
+
+ getConfigFromState(config) {
+ config.TeamSettings.EnableUserCreation = this.state.enableUserCreation;
+ config.TeamSettings.EnableTeamCreation = this.state.enableTeamCreation;
+ config.TeamSettings.MaxUsersPerTeam = this.parseIntNonZero(this.state.maxUsersPerTeam);
+ config.TeamSettings.RestrictCreationToDomains = this.state.restrictCreationToDomains;
+ config.TeamSettings.RestrictTeamNames = this.state.restrictTeamNames;
+ config.TeamSettings.RestrictDirectMessage = this.state.restrictDirectMessage;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.general.title'
+ defaultMessage='General Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.general.usersAndTeams'
+ defaultMessage='Users and Teams'
+ />
+ }
+ >
+ <BooleanSetting
+ id='enableUserCreation'
+ label={
+ <FormattedMessage
+ id='admin.team.userCreationTitle'
+ defaultMessage='Enable User Creation: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.userCreationDescription'
+ defaultMessage='When false, the ability to create accounts is disabled. The create account button displays error when pressed.'
+ />
+ }
+ value={this.state.enableUserCreation}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableTeamCreation'
+ label={
+ <FormattedMessage
+ id='admin.team.teamCreationTitle'
+ defaultMessage='Enable Team Creation: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.teamCreationDescription'
+ defaultMessage='When false, the ability to create teams is disabled. The create team button displays error when pressed.'
+ />
+ }
+ value={this.state.enableTeamCreation}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='maxUsersPerTeam'
+ label={
+ <FormattedMessage
+ id='admin.team.maxUsersTitle'
+ defaultMessage='Max Users Per Team:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.team.maxUsersExample', 'Ex "25"')}
+ helpText={
+ <FormattedMessage
+ id='admin.team.maxUsersDescription'
+ defaultMessage='Maximum total number of users per team, including both active and inactive users.'
+ />
+ }
+ value={this.state.maxUsersPerTeam}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='restrictCreationToDomains'
+ label={
+ <FormattedMessage
+ id='admin.team.restrictTitle'
+ defaultMessage='Restrict Creation To Domains:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.team.restrictExample', 'Ex "corp.mattermost.com, mattermost.org"')}
+ helpText={
+ <FormattedMessage
+ id='admin.team.restrictDescription'
+ defaultMessage='Teams and user accounts can only be created from a specific domain (e.g. "mattermost.org") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").'
+ />
+ }
+ value={this.state.restrictCreationToDomains}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='restrictTeamNames'
+ label={
+ <FormattedMessage
+ id='admin.team.restrictNameTitle'
+ defaultMessage='Restrict Team Names: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.restrictNameDesc'
+ defaultMessage='When true, You cannot create a team name with reserved words like www, admin, support, test, channel, etc'
+ />
+ }
+ value={this.state.restrictTeamNames}
+ onChange={this.handleChange}
+ />
+ <DropdownSetting
+ id='restrictDirectMessage'
+ values={[
+ {value: RESTRICT_DIRECT_MESSAGE_ANY, text: Utils.localizeMessage('admin.team.restrict_direct_message_any', 'Any user on the Mattermost server')},
+ {value: RESTRICT_DIRECT_MESSAGE_TEAM, text: Utils.localizeMessage('admin.team.restrict_direct_message_team', 'Any member of the team')}
+ ]}
+ label={
+ <FormattedMessage
+ id='admin.team.restrictDirectMessage'
+ defaultMessage='Enable users to open Direct Message channels with:'
+ />
+ }
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.team.restrictDirectMessageDesc'
+ defaultMessage='"Any user on the Mattermost server" enables users to open a Direct Message channel with any user on the server, even if they are not on any teams together. "Any member of the team" limits the ability to open Direct Message channels to only users who are in the same team.'
+ />
+ }
+ value={this.state.restrictDirectMessage}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/webhook_settings.jsx b/webapp/components/admin_console/webhook_settings.jsx
new file mode 100644
index 000000000..1c125cd0f
--- /dev/null
+++ b/webapp/components/admin_console/webhook_settings.jsx
@@ -0,0 +1,166 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+
+export default class WebhookSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enableIncomingWebhooks: props.config.ServiceSettings.EnableIncomingWebhooks,
+ enableOutgoingWebhooks: props.config.ServiceSettings.EnableOutgoingWebhooks,
+ enableCommands: props.config.ServiceSettings.EnableCommands,
+ enableOnlyAdminIntegrations: props.config.ServiceSettings.EnableOnlyAdminIntegrations,
+ enablePostUsernameOverride: props.config.ServiceSettings.EnablePostUsernameOverride,
+ enablePostIconOverride: props.config.ServiceSettings.EnablePostIconOverride
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.EnableIncomingWebhooks = this.state.enableIncomingWebhooks;
+ config.ServiceSettings.EnableOutgoingWebhooks = this.state.enableOutgoingWebhooks;
+ config.ServiceSettings.EnableCommands = this.state.enableCommands;
+ config.ServiceSettings.EnableOnlyAdminIntegrations = this.state.enableOnlyAdminIntegrations;
+ config.ServiceSettings.EnablePostUsernameOverride = this.state.enablePostUsernameOverride;
+ config.ServiceSettings.EnablePostIconOverride = this.state.enablePostIconOverride;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.integration.title'
+ defaultMessage='Integration Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.integrations.webhook'
+ defaultMessage='Webhooks and Commands'
+ />
+ }
+ >
+ <BooleanSetting
+ id='enableIncomingWebhooks'
+ label={
+ <FormattedMessage
+ id='admin.service.webhooksTitle'
+ defaultMessage='Enable Incoming Webhooks: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.webhooksDescription'
+ defaultMessage='When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'
+ />
+ }
+ value={this.state.enableIncomingWebhooks}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableOutgoingWebhooks'
+ label={
+ <FormattedMessage
+ id='admin.service.outWebhooksTitle'
+ defaultMessage='Enable Outgoing Webhooks: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.outWebhooksDesc'
+ defaultMessage='When true, outgoing webhooks will be allowed.'
+ />
+ }
+ value={this.state.enableOutgoingWebhooks}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableCommands'
+ label={
+ <FormattedMessage
+ id='admin.service.cmdsTitle'
+ defaultMessage='Enable Slash Commands: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.cmdsDesc'
+ defaultMessage='When true, user created slash commands will be allowed.'
+ />
+ }
+ value={this.state.enableCommands}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableOnlyAdminIntegrations'
+ label={
+ <FormattedMessage
+ id='admin.service.integrationAdmin'
+ defaultMessage='Enable Integrations for Admin Only: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.integrationAdminDesc'
+ defaultMessage='When true, user created integrations can only be created by admins.'
+ />
+ }
+ value={this.state.enableOnlyAdminIntegrations}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enablePostUsernameOverride'
+ label={
+ <FormattedMessage
+ id='admin.service.overrideTitle'
+ defaultMessage='Enable Overriding Usernames from Webhooks and Slash Commands: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.overrideDescription'
+ defaultMessage='When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'
+ />
+ }
+ value={this.state.enablePostUsernameOverride}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enablePostIconOverride'
+ label={
+ <FormattedMessage
+ id='admin.service.iconTitle'
+ defaultMessage='Enable Overriding Icon from Webhooks and Slash Commands: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.iconDescription'
+ defaultMessage='When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'
+ />
+ }
+ value={this.state.enablePostIconOverride}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/analytics/statistic_count.jsx b/webapp/components/analytics/statistic_count.jsx
index cbb8935dd..89e0cc8df 100644
--- a/webapp/components/analytics/statistic_count.jsx
+++ b/webapp/components/analytics/statistic_count.jsx
@@ -7,7 +7,7 @@ import React from 'react';
export default class StatisticCount extends React.Component {
render() {
- let loading = (
+ const loading = (
<FormattedMessage
id='analytics.chart.loading'
defaultMessage='Loading...'
diff --git a/webapp/components/analytics/system_analytics.jsx b/webapp/components/analytics/system_analytics.jsx
index 77f5efaa6..1625a919e 100644
--- a/webapp/components/analytics/system_analytics.jsx
+++ b/webapp/components/analytics/system_analytics.jsx
@@ -245,8 +245,7 @@ class SystemAnalytics extends React.Component {
}
SystemAnalytics.propTypes = {
- intl: intlShape.isRequired,
- team: React.PropTypes.object
+ intl: intlShape.isRequired
};
export default injectIntl(SystemAnalytics);
diff --git a/webapp/components/analytics/team_analytics.jsx b/webapp/components/analytics/team_analytics.jsx
index 9b4eb1f94..ffca9199a 100644
--- a/webapp/components/analytics/team_analytics.jsx
+++ b/webapp/components/analytics/team_analytics.jsx
@@ -5,6 +5,7 @@ import LineChart from './line_chart.jsx';
import StatisticCount from './statistic_count.jsx';
import TableChart from './table_chart.jsx';
+import AdminStore from 'stores/admin_store.jsx';
import AnalyticsStore from 'stores/analytics_store.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -13,23 +14,34 @@ import Constants from 'utils/constants.jsx';
const StatTypes = Constants.StatTypes;
import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from './system_analytics.jsx';
-import {injectIntl, intlShape, FormattedMessage, FormattedDate} from 'react-intl';
+import {FormattedMessage, FormattedDate} from 'react-intl';
import React from 'react';
-class TeamAnalytics extends React.Component {
+export default class TeamAnalytics extends React.Component {
+ static get propTypes() {
+ return {
+ params: React.PropTypes.object.isRequired
+ };
+ }
+
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
+ this.onAllTeamsChange = this.onAllTeamsChange.bind(this);
- this.state = {stats: AnalyticsStore.getAllTeam(this.props.team.id)};
+ this.state = {
+ team: AdminStore.getTeam(this.props.params.team),
+ stats: AnalyticsStore.getAllTeam(this.props.params.team)
+ };
}
componentDidMount() {
AnalyticsStore.addChangeListener(this.onChange);
+ AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange);
- this.getData(this.props.team.id);
+ this.getData(this.props.params.team);
}
getData(id) {
@@ -41,11 +53,14 @@ class TeamAnalytics extends React.Component {
componentWillUnmount() {
AnalyticsStore.removeChangeListener(this.onChange);
+ AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange);
}
componentWillReceiveProps(nextProps) {
- this.getData(nextProps.team.id);
- this.setState({stats: AnalyticsStore.getAllTeam(nextProps.team.id)});
+ this.getData(nextProps.params.team);
+ this.setState({
+ stats: AnalyticsStore.getAllTeam(nextProps.params.team)
+ });
}
shouldComponentUpdate(nextProps, nextState) {
@@ -53,7 +68,7 @@ class TeamAnalytics extends React.Component {
return true;
}
- if (!Utils.areObjectsEqual(nextProps.team, this.props.team)) {
+ if (!Utils.areObjectsEqual(nextProps.params.team, this.props.params.team)) {
return true;
}
@@ -61,10 +76,22 @@ class TeamAnalytics extends React.Component {
}
onChange() {
- this.setState({stats: AnalyticsStore.getAllTeam(this.props.team.id)});
+ this.setState({
+ stats: AnalyticsStore.getAllTeam(this.props.params.team)
+ });
+ }
+
+ onAllTeamsChange() {
+ this.setState({
+ team: AdminStore.getTeam(this.props.params.team)
+ });
}
render() {
+ if (!this.state.team || !this.state.stats) {
+ return null;
+ }
+
const stats = this.state.stats;
const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]);
const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]);
@@ -78,7 +105,7 @@ class TeamAnalytics extends React.Component {
id='analytics.team.title'
defaultMessage='Team Statistics for {team}'
values={{
- team: this.props.team.name
+ team: this.state.team.name
}}
/>
</h3>
@@ -175,13 +202,6 @@ class TeamAnalytics extends React.Component {
}
}
-TeamAnalytics.propTypes = {
- intl: intlShape.isRequired,
- team: React.PropTypes.object.isRequired
-};
-
-export default injectIntl(TeamAnalytics);
-
export function formatRecentUsersData(data) {
if (data == null) {
return [];
diff --git a/webapp/components/backstage/add_command.jsx b/webapp/components/backstage/add_command.jsx
index ba9ac4e79..c817764aa 100644
--- a/webapp/components/backstage/add_command.jsx
+++ b/webapp/components/backstage/add_command.jsx
@@ -4,14 +4,14 @@
import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
-import {browserHistory} from 'react-router';
import * as Utils from 'utils/utils.jsx';
import BackstageHeader from './backstage_header.jsx';
import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
-import {Link} from 'react-router';
+import {browserHistory, Link} from 'react-router';
import SpinnerButton from 'components/spinner_button.jsx';
+import Constants from 'utils/constants.jsx';
const REQUEST_POST = 'P';
const REQUEST_GET = 'G';
@@ -93,6 +93,51 @@ export default class AddCommand extends React.Component {
return;
}
+ if (command.trigger.indexOf('/') === 0) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidSlash'
+ defaultMessage='A trigger word cannot begin with a /'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (command.trigger.indexOf(' ') !== -1) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidSpace'
+ defaultMessage='A trigger word must not contain spaces'
+ />
+ )
+ });
+ return;
+ }
+
+ if (command.trigger.length < Constants.MIN_TRIGGER_LENGTH || command.trigger.length > Constants.MAX_TRIGGER_LENGTH) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidLength'
+ defaultMessage='A trigger word must contain between {min} and {max} characters'
+ values={{
+ min: Constants.MIN_TRIGGER_LENGTH,
+ max: Constants.MAX_TRIGGER_LENGTH
+ }}
+ />
+ )
+ });
+
+ return;
+ }
+
if (!command.url) {
this.setState({
saving: false,
@@ -324,7 +369,7 @@ export default class AddCommand extends React.Component {
<input
id='trigger'
type='text'
- maxLength='128'
+ maxLength={Constants.MAX_TRIGGER_LENGTH}
className='form-control'
value={this.state.trigger}
onChange={this.updateTrigger}
diff --git a/webapp/components/backstage/add_incoming_webhook.jsx b/webapp/components/backstage/add_incoming_webhook.jsx
index 445d370b5..f698f2b13 100644
--- a/webapp/components/backstage/add_incoming_webhook.jsx
+++ b/webapp/components/backstage/add_incoming_webhook.jsx
@@ -4,14 +4,13 @@
import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
-import {browserHistory} from 'react-router';
import * as Utils from 'utils/utils.jsx';
import BackstageHeader from './backstage_header.jsx';
import ChannelSelect from 'components/channel_select.jsx';
import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
-import {Link} from 'react-router';
+import {browserHistory, Link} from 'react-router';
import SpinnerButton from 'components/spinner_button.jsx';
export default class AddIncomingWebhook extends React.Component {
@@ -173,6 +172,8 @@ export default class AddIncomingWebhook extends React.Component {
id='channelId'
value={this.state.channelId}
onChange={this.updateChannelId}
+ selectOpen={true}
+ selectPrivate={true}
/>
</div>
</div>
diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/backstage/add_outgoing_webhook.jsx
index 245df1604..2fefd5965 100644
--- a/webapp/components/backstage/add_outgoing_webhook.jsx
+++ b/webapp/components/backstage/add_outgoing_webhook.jsx
@@ -4,14 +4,13 @@
import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
-import {browserHistory} from 'react-router';
import * as Utils from 'utils/utils.jsx';
import BackstageHeader from './backstage_header.jsx';
import ChannelSelect from 'components/channel_select.jsx';
import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
-import {Link} from 'react-router';
+import {browserHistory, Link} from 'react-router';
import SpinnerButton from 'components/spinner_button.jsx';
export default class AddOutgoingWebhook extends React.Component {
@@ -225,6 +224,7 @@ export default class AddOutgoingWebhook extends React.Component {
id='channelId'
value={this.state.channelId}
onChange={this.updateChannelId}
+ selectOpen={true}
/>
</div>
</div>
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 992244915..91060f583 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -47,6 +47,7 @@ export default class ChannelHeader extends React.Component {
this.searchMentions = this.searchMentions.bind(this);
this.showRenameChannelModal = this.showRenameChannelModal.bind(this);
this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this);
+ this.openRecentMentions = this.openRecentMentions.bind(this);
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
@@ -82,6 +83,7 @@ export default class ChannelHeader extends React.Component {
PreferenceStore.addChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
$('.sidebar--left .dropdown-menu').perfectScrollbar();
+ document.addEventListener('keydown', this.openRecentMentions);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
@@ -89,6 +91,7 @@ export default class ChannelHeader extends React.Component {
SearchStore.removeSearchChangeListener(this.onListenerChange);
PreferenceStore.removeChangeListener(this.onListenerChange);
UserStore.removeChangeListener(this.onListenerChange);
+ document.removeEventListener('keydown', this.openRecentMentions);
}
onListenerChange() {
const newState = this.getStateFromStores();
@@ -139,6 +142,12 @@ export default class ChannelHeader extends React.Component {
is_mention_search: true
});
}
+ openRecentMentions(e) {
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.keyCode === Constants.KeyCodes.M) {
+ e.preventDefault();
+ this.searchMentions(e);
+ }
+ }
showRenameChannelModal(e) {
e.preventDefault();
@@ -409,7 +418,8 @@ export default class ChannelHeader extends React.Component {
}
}
- if (!ChannelStore.isDefault(channel)) {
+ const canLeave = channel.type === Constants.PRIVATE_CHANNEL ? this.state.userCount > 1 : true;
+ if (!ChannelStore.isDefault(channel) && canLeave) {
dropdownContents.push(
<li
key='leave_channel'
@@ -469,11 +479,11 @@ export default class ChannelHeader extends React.Component {
overlay={popoverContent}
ref='headerOverlay'
>
- <div
- onClick={TextFormatting.handleClick}
- className='description'
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
- />
+ <div
+ onClick={TextFormatting.handleClick}
+ className='description'
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
+ />
</OverlayTrigger>
</div>
</th>
diff --git a/webapp/components/channel_notifications_modal.jsx b/webapp/components/channel_notifications_modal.jsx
index 112c07ad0..74ec2376b 100644
--- a/webapp/components/channel_notifications_modal.jsx
+++ b/webapp/components/channel_notifications_modal.jsx
@@ -182,7 +182,6 @@ export default class ChannelNotificationsModal extends React.Component {
const handleUpdateSection = function updateSection(e) {
this.updateSection('');
- this.onListenerChange();
e.preventDefault();
}.bind(this);
@@ -289,10 +288,10 @@ export default class ChannelNotificationsModal extends React.Component {
checked={this.state.unreadLevel === 'all'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
/>
- <FormattedMessage
- id='channel_notifications.allUnread'
- defaultMessage='For all unread messages'
- />
+ <FormattedMessage
+ id='channel_notifications.allUnread'
+ defaultMessage='For all unread messages'
+ />
</label>
<br/>
</div>
@@ -312,7 +311,6 @@ export default class ChannelNotificationsModal extends React.Component {
const handleUpdateSection = function handleUpdateSection(e) {
this.updateSection('');
- this.onListenerChange();
e.preventDefault();
}.bind(this);
diff --git a/webapp/components/channel_select.jsx b/webapp/components/channel_select.jsx
index 238cfa1ae..59bf2f15a 100644
--- a/webapp/components/channel_select.jsx
+++ b/webapp/components/channel_select.jsx
@@ -6,12 +6,24 @@ import React from 'react';
import Constants from 'utils/constants.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
export default class ChannelSelect extends React.Component {
static get propTypes() {
return {
onChange: React.PropTypes.func,
- value: React.PropTypes.string
+ value: React.PropTypes.string,
+ selectOpen: React.PropTypes.bool.isRequired,
+ selectPrivate: React.PropTypes.bool.isRequired,
+ selectDm: React.PropTypes.bool.isRequired
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ selectOpen: false,
+ selectPrivate: false,
+ selectDm: false
};
}
@@ -19,17 +31,16 @@ export default class ChannelSelect extends React.Component {
super(props);
this.handleChannelChange = this.handleChannelChange.bind(this);
+ this.compareByDisplayName = this.compareByDisplayName.bind(this);
+
+ AsyncClient.getMoreChannels(true);
this.state = {
- channels: []
+ channels: ChannelStore.getAll().sort(this.compareByDisplayName)
};
}
- componentWillMount() {
- this.setState({
- channels: ChannelStore.getAll()
- });
-
+ componentDidMount() {
ChannelStore.addChangeListener(this.handleChannelChange);
}
@@ -39,10 +50,14 @@ export default class ChannelSelect extends React.Component {
handleChannelChange() {
this.setState({
- channels: ChannelStore.getAll()
+ channels: ChannelStore.getAll().concat(ChannelStore.getMoreAll()).sort(this.compareByDisplayName)
});
}
+ compareByDisplayName(channelA, channelB) {
+ return channelA.display_name.localeCompare(channelB.display_name);
+ }
+
render() {
const options = [
<option
@@ -54,7 +69,25 @@ export default class ChannelSelect extends React.Component {
];
this.state.channels.forEach((channel) => {
- if (channel.type === Constants.OPEN_CHANNEL) {
+ if (channel.type === Constants.OPEN_CHANNEL && this.props.selectOpen) {
+ options.push(
+ <option
+ key={channel.id}
+ value={channel.id}
+ >
+ {channel.display_name}
+ </option>
+ );
+ } else if (channel.type === Constants.PRIVATE_CHANNEL && this.props.selectPrivate) {
+ options.push(
+ <option
+ key={channel.id}
+ value={channel.id}
+ >
+ {channel.display_name}
+ </option>
+ );
+ } else if (channel.type === Constants.DM_CHANNEL && this.props.selectDm) {
options.push(
<option
key={channel.id}
diff --git a/webapp/components/claim/claim.jsx b/webapp/components/claim/claim_controller.jsx
index 0197e1677..dbb944bb9 100644
--- a/webapp/components/claim/claim.jsx
+++ b/webapp/components/claim/claim_controller.jsx
@@ -7,7 +7,7 @@ import {Link} from 'react-router';
import logoImage from 'images/logo.png';
-export default class Claim extends React.Component {
+export default class ClaimController extends React.Component {
constructor(props) {
super(props);
@@ -51,9 +51,9 @@ export default class Claim extends React.Component {
}
}
-Claim.defaultProps = {
+ClaimController.defaultProps = {
};
-Claim.propTypes = {
+ClaimController.propTypes = {
location: React.PropTypes.object.isRequired,
children: React.PropTypes.node
};
diff --git a/webapp/components/claim/components/email_to_ldap.jsx b/webapp/components/claim/components/email_to_ldap.jsx
index fbf26cade..f7bb02c6e 100644
--- a/webapp/components/claim/components/email_to_ldap.jsx
+++ b/webapp/components/claim/components/email_to_ldap.jsx
@@ -20,7 +20,7 @@ export default class EmailToLDAP extends React.Component {
e.preventDefault();
var state = {};
- const password = ReactDOM.findDOMNode(this.refs.emailpassword).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.emailpassword).value;
if (!password) {
state.error = Utils.localizeMessage('claim.email_to_ldap.pwdError', 'Please enter your password.');
this.setState(state);
@@ -34,7 +34,7 @@ export default class EmailToLDAP extends React.Component {
return;
}
- const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value.trim();
+ const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value;
if (!ldapPassword) {
state.error = Utils.localizeMessage('claim.email_to_ldap.ldapPasswordError', 'Please enter your LDAP password.');
this.setState(state);
diff --git a/webapp/components/claim/components/email_to_oauth.jsx b/webapp/components/claim/components/email_to_oauth.jsx
index 1fd284bed..171ebe8a4 100644
--- a/webapp/components/claim/components/email_to_oauth.jsx
+++ b/webapp/components/claim/components/email_to_oauth.jsx
@@ -20,7 +20,7 @@ export default class EmailToOAuth extends React.Component {
e.preventDefault();
var state = {};
- var password = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ var password = ReactDOM.findDOMNode(this.refs.password).value;
if (!password) {
state.error = Utils.localizeMessage('claim.email_to_oauth.pwdError', 'Please enter your password.');
this.setState(state);
diff --git a/webapp/components/claim/components/ldap_to_email.jsx b/webapp/components/claim/components/ldap_to_email.jsx
index a10cefd6f..fbc8bcebf 100644
--- a/webapp/components/claim/components/ldap_to_email.jsx
+++ b/webapp/components/claim/components/ldap_to_email.jsx
@@ -2,7 +2,8 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import Client from 'utils/web_client.jsx';
+
+import {switchFromLdapToEmail} from 'actions/user_actions.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -16,25 +17,26 @@ export default class LDAPToEmail extends React.Component {
this.state = {};
}
+
submit(e) {
e.preventDefault();
var state = {};
- const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.password).value;
if (!password) {
state.error = Utils.localizeMessage('claim.ldap_to_email.pwdError', 'Please enter your password.');
this.setState(state);
return;
}
- const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim();
+ const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value;
if (!confirmPassword || password !== confirmPassword) {
state.error = Utils.localizeMessage('claim.ldap_to_email.pwdNotMatch', 'Passwords do not match.');
this.setState(state);
return;
}
- const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value.trim();
+ const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value;
if (!ldapPassword) {
state.error = Utils.localizeMessage('claim.ldap_to_email.ldapPasswordError', 'Please enter your LDAP password.');
this.setState(state);
@@ -44,20 +46,15 @@ export default class LDAPToEmail extends React.Component {
state.error = null;
this.setState(state);
- Client.ldapToEmail(
+ switchFromLdapToEmail(
this.props.email,
password,
ldapPassword,
- (data) => {
- if (data.follow_link) {
- window.location.href = data.follow_link;
- }
- },
- (error) => {
- this.setState({error});
- }
+ null,
+ (err) => this.setState({error: err.message})
);
}
+
render() {
var error = null;
if (this.state.error) {
diff --git a/webapp/components/claim/components/oauth_to_email.jsx b/webapp/components/claim/components/oauth_to_email.jsx
index 7fd18aaa6..1a3b962a2 100644
--- a/webapp/components/claim/components/oauth_to_email.jsx
+++ b/webapp/components/claim/components/oauth_to_email.jsx
@@ -21,14 +21,14 @@ export default class OAuthToEmail extends React.Component {
e.preventDefault();
const state = {};
- const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.password).value;
if (!password) {
state.error = Utils.localizeMessage('claim.oauth_to_email.enterPwd', 'Please enter a password.');
this.setState(state);
return;
}
- const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim();
+ const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value;
if (!confirmPassword || password !== confirmPassword) {
state.error = Utils.localizeMessage('claim.oauth_to_email.pwdNotMatch', 'Password do not match.');
this.setState(state);
diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx
index 30e89e500..616257f37 100644
--- a/webapp/components/create_comment.jsx
+++ b/webapp/components/create_comment.jsx
@@ -16,7 +16,7 @@ import MsgTyping from './msg_typing.jsx';
import FileUpload from './file_upload.jsx';
import FilePreview from './file_preview.jsx';
import * as Utils from 'utils/utils.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -72,6 +72,7 @@ class CreateComment extends React.Component {
const draft = PostStore.getCommentDraft(this.props.rootId);
this.state = {
messageText: draft.message,
+ lastMessage: '',
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
@@ -111,7 +112,7 @@ class CreateComment extends React.Component {
return;
}
- let post = {};
+ const post = {};
post.filenames = [];
post.message = this.state.messageText;
@@ -144,7 +145,7 @@ class CreateComment extends React.Component {
AsyncClient.getPosts(this.props.channelId);
const channel = ChannelStore.get(this.props.channelId);
- let member = ChannelStore.getMember(this.props.channelId);
+ const member = ChannelStore.getMember(this.props.channelId);
member.msg_count = channel.total_msg_count;
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
@@ -172,6 +173,7 @@ class CreateComment extends React.Component {
this.setState({
messageText: '',
+ lastMessage: this.state.messageText,
submitting: false,
postError: null,
previews: [],
@@ -190,7 +192,7 @@ class CreateComment extends React.Component {
GlobalActions.emitLocalUserTypingEvent(this.props.channelId, this.props.rootId);
}
handleUserInput(messageText) {
- let draft = PostStore.getCommentDraft(this.props.rootId);
+ const draft = PostStore.getCommentDraft(this.props.rootId);
draft.message = messageText;
PostStore.storeCommentDraft(this.props.rootId, draft);
@@ -203,7 +205,7 @@ class CreateComment extends React.Component {
return;
}
- if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
+ if (!e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
const lastPost = PostStore.getCurrentUsersLatestPost(this.props.channelId, this.props.rootId);
@@ -221,12 +223,25 @@ class CreateComment extends React.Component {
comments: PostStore.getCommentCount(lastPost)
});
}
+
+ if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.keyCode === KeyCodes.UP) {
+ e.preventDefault();
+ const lastPost = PostStore.getCurrentUsersLatestPost(this.props.channelId, this.props.rootId);
+ if (!lastPost) {
+ return;
+ }
+ let message = lastPost.message;
+ if (this.state.lastMessage !== '') {
+ message = this.state.lastMessage;
+ }
+ this.setState({messageText: message});
+ }
}
handleUploadClick() {
this.focusTextbox();
}
handleUploadStart(clientIds) {
- let draft = PostStore.getCommentDraft(this.props.rootId);
+ const draft = PostStore.getCommentDraft(this.props.rootId);
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
PostStore.storeCommentDraft(this.props.rootId, draft);
@@ -238,7 +253,7 @@ class CreateComment extends React.Component {
this.focusTextbox();
}
handleFileUploadComplete(filenames, clientIds) {
- let draft = PostStore.getCommentDraft(this.props.rootId);
+ const draft = PostStore.getCommentDraft(this.props.rootId);
// remove each finished file from uploads
for (let i = 0; i < clientIds.length; i++) {
@@ -258,7 +273,7 @@ class CreateComment extends React.Component {
if (clientId === -1) {
this.setState({serverError: err});
} else {
- let draft = PostStore.getCommentDraft(this.props.rootId);
+ const draft = PostStore.getCommentDraft(this.props.rootId);
const index = draft.uploadsInProgress.indexOf(clientId);
if (index !== -1) {
@@ -271,8 +286,8 @@ class CreateComment extends React.Component {
}
}
removePreview(id) {
- let previews = this.state.previews;
- let uploadsInProgress = this.state.uploadsInProgress;
+ const previews = this.state.previews;
+ const uploadsInProgress = this.state.uploadsInProgress;
// id can either be the path of an uploaded file or the client id of an in progress upload
let index = previews.indexOf(id);
@@ -287,7 +302,7 @@ class CreateComment extends React.Component {
previews.splice(index, 1);
}
- let draft = PostStore.getCommentDraft(this.props.rootId);
+ const draft = PostStore.getCommentDraft(this.props.rootId);
draft.previews = previews;
draft.uploadsInProgress = uploadsInProgress;
PostStore.storeCommentDraft(this.props.rootId, draft);
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index d173fe42b..75c75f09d 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -10,7 +10,7 @@ import PostDeletedModal from './post_deleted_modal.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Client from 'utils/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -77,6 +77,7 @@ class CreatePost extends React.Component {
this.state = {
channelId: ChannelStore.getCurrentId(),
messageText: draft.messageText,
+ lastMessage: '',
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
@@ -126,7 +127,7 @@ class CreatePost extends React.Component {
}
this.setState({submitting: true, serverError: null});
-
+ this.setState({lastMessage: this.state.messageText});
if (post.message.indexOf('/') === 0) {
Client.executeCommand(
this.state.channelId,
@@ -350,7 +351,7 @@ class CreatePost extends React.Component {
return;
}
- if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
+ if (!e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
const channelId = ChannelStore.getCurrentId();
@@ -371,6 +372,20 @@ class CreatePost extends React.Component {
comments: PostStore.getCommentCount(lastPost)
});
}
+
+ if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.keyCode === KeyCodes.UP) {
+ e.preventDefault();
+ const channelId = ChannelStore.getCurrentId();
+ const lastPost = PostStore.getCurrentUsersLatestPost(channelId);
+ if (!lastPost) {
+ return;
+ }
+ let message = lastPost.message;
+ if (this.state.lastMessage !== '') {
+ message = this.state.lastMessage;
+ }
+ this.setState({messageText: message});
+ }
}
showPostDeletedModal() {
this.setState({
diff --git a/webapp/components/create_team/components/display_name.jsx b/webapp/components/create_team/components/display_name.jsx
index e8f1717bb..e6dcd221a 100644
--- a/webapp/components/create_team/components/display_name.jsx
+++ b/webapp/components/create_team/components/display_name.jsx
@@ -1,29 +1,19 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ReactDOM from 'react-dom';
-import * as utils from 'utils/utils.jsx';
-import Client from 'utils/web_client.jsx';
-import {Link} from 'react-router';
+import {track} from 'actions/analytics_actions.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+import * as Utils from 'utils/utils.jsx';
+import Constants from 'utils/constants.jsx';
import logoImage from 'images/logo.png';
-const holders = defineMessages({
- required: {
- id: 'create_team.display_name.required',
- defaultMessage: 'This field is required'
- },
- charLength: {
- id: 'create_team.display_name.charLength',
- defaultMessage: 'Name must be 4 or more characters up to a maximum of 15'
- }
-});
-
import React from 'react';
+import ReactDOM from 'react-dom';
+import {Link} from 'react-router';
+import {FormattedMessage} from 'react-intl';
-class TeamSignupDisplayNamePage extends React.Component {
+export default class TeamSignupDisplayNamePage extends React.Component {
constructor(props) {
super(props);
@@ -35,19 +25,18 @@ class TeamSignupDisplayNamePage extends React.Component {
submitNext(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
var displayName = ReactDOM.findDOMNode(this.refs.name).value.trim();
if (!displayName) {
- this.setState({nameError: formatMessage(holders.required)});
+ this.setState({nameError: Utils.localizeMessage('create_team.display_name.required', 'This field is required')});
return;
- } else if (displayName.length < 4 || displayName.length > 15) {
- this.setState({nameError: formatMessage(holders.charLength)});
+ } else if (displayName.length < Constants.MIN_TEAMNAME_LENGTH || displayName.length > Constants.MAX_TEAMNAME_LENGTH) {
+ this.setState({nameError: Utils.localizeMessage('create_team.display_name.charLength', 'Name must be 4 or more characters up to a maximum of 15')});
return;
}
this.props.state.wizard = 'team_url';
this.props.state.team.display_name = displayName;
- this.props.state.team.name = utils.cleanUpUrlable(displayName);
+ this.props.state.team.name = Utils.cleanUpUrlable(displayName);
this.props.updateParent(this.props.state);
}
@@ -57,7 +46,7 @@ class TeamSignupDisplayNamePage extends React.Component {
}
render() {
- Client.track('signup', 'signup_team_02_name');
+ track('signup', 'signup_team_02_name');
var nameError = null;
var nameDivClass = 'form-group';
@@ -128,9 +117,6 @@ class TeamSignupDisplayNamePage extends React.Component {
}
TeamSignupDisplayNamePage.propTypes = {
- intl: intlShape.isRequired,
state: React.PropTypes.object,
updateParent: React.PropTypes.func
};
-
-export default injectIntl(TeamSignupDisplayNamePage);
diff --git a/webapp/components/create_team/components/team_url.jsx b/webapp/components/create_team/components/team_url.jsx
index 34e696938..b6c634816 100644
--- a/webapp/components/create_team/components/team_url.jsx
+++ b/webapp/components/create_team/components/team_url.jsx
@@ -2,45 +2,20 @@
// See License.txt for license information.
import $ from 'jquery';
-import ReactDOM from 'react-dom';
+
import * as Utils from 'utils/utils.jsx';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import TeamStore from 'stores/team_store.jsx';
-import UserStore from 'stores/user_store.jsx';
-import Constants from 'utils/constants.jsx';
-import {browserHistory} from 'react-router';
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {checkIfTeamExists, createTeam} from 'actions/team_actions.jsx';
+import {track} from 'actions/analytics_actions.jsx';
+import Constants from 'utils/constants.jsx';
import logoImage from 'images/logo.png';
-const holders = defineMessages({
- required: {
- id: 'create_team.team_url.required',
- defaultMessage: 'This field is required'
- },
- regex: {
- id: 'create_team.team_url.regex',
- defaultMessage: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."
- },
- charLength: {
- id: 'create_team.team_url.charLength',
- defaultMessage: 'Name must be 4 or more characters up to a maximum of 15'
- },
- taken: {
- id: 'create_team.team_url.taken',
- defaultMessage: 'URL is taken or contains a reserved word'
- },
- unavailable: {
- id: 'create_team.team_url.unavailable',
- defaultMessage: 'This URL is unavailable. Please try another.'
- }
-});
-
import React from 'react';
+import ReactDOM from 'react-dom';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-class TeamUrl extends React.Component {
+export default class TeamUrl extends React.Component {
constructor(props) {
super(props);
@@ -58,10 +33,9 @@ class TeamUrl extends React.Component {
submitNext(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
const name = ReactDOM.findDOMNode(this.refs.name).value.trim();
if (!name) {
- this.setState({nameError: formatMessage(holders.required)});
+ this.setState({nameError: Utils.localizeMessage('create_team.team_url.required', 'This field is required')});
return;
}
@@ -69,17 +43,17 @@ class TeamUrl extends React.Component {
const urlRegex = /^[a-z]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g;
if (cleanedName !== name || !urlRegex.test(name)) {
- this.setState({nameError: formatMessage(holders.regex)});
+ this.setState({nameError: Utils.localizeMessage('create_team.team_url.regex', "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash.")});
return;
- } else if (cleanedName.length < 4 || cleanedName.length > 15) {
- this.setState({nameError: formatMessage(holders.charLength)});
+ } else if (cleanedName.length < Constants.MIN_TEAMNAME_LENGTH || cleanedName.length > Constants.MAX_TEAMNAME_LENGTH) {
+ this.setState({nameError: Utils.localizeMessage('create_team.team_url.charLength', 'Name must be 4 or more characters up to a maximum of 15')});
return;
}
if (global.window.mm_config.RestrictTeamNames === 'true') {
for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
- this.setState({nameError: formatMessage(holders.taken)});
+ this.setState({nameError: Utils.localizeMessage('create_team.team_url.taken', 'URL is taken or contains a reserved word')});
return;
}
}
@@ -90,30 +64,24 @@ class TeamUrl extends React.Component {
teamSignup.team.type = 'O';
teamSignup.team.name = name;
- Client.findTeamByName(name,
- (findTeam) => {
- if (findTeam) {
- this.setState({nameError: formatMessage(holders.unavailable)});
- $('#finish-button').button('reset');
- } else {
- Client.createTeam(teamSignup.team,
- (team) => {
- Client.track('signup', 'signup_team_08_complete');
- $('#sign-up-button').button('reset');
- AsyncClient.getDirectProfiles();
- TeamStore.saveTeam(team);
- TeamStore.appendTeamMember({team_id: team.id, user_id: UserStore.getCurrentId(), roles: 'admin'});
- TeamStore.emitChange();
- browserHistory.push('/' + team.name + '/channels/town-square');
- },
- (err) => {
- this.setState({nameError: err.message});
- $('#finish-button').button('reset');
- }
- );
-
+ checkIfTeamExists(name,
+ (foundTeam) => {
+ if (foundTeam) {
+ this.setState({nameError: Utils.localizeMessage('create_team.team_url.unavailable', 'This URL is unavailable. Please try another.')});
$('#finish-button').button('reset');
+ return;
}
+
+ createTeam(teamSignup.team,
+ () => {
+ track('signup', 'signup_team_08_complete');
+ $('#sign-up-button').button('reset');
+ },
+ (err) => {
+ this.setState({nameError: err.message});
+ $('#finish-button').button('reset');
+ }
+ );
},
(err) => {
this.setState({nameError: err.message});
@@ -121,15 +89,17 @@ class TeamUrl extends React.Component {
}
);
}
+
handleFocus(e) {
e.preventDefault();
e.currentTarget.select();
}
+
render() {
$('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'});
- Client.track('signup', 'signup_team_03_url');
+ track('signup', 'signup_team_03_url');
let nameError = null;
let nameDivClass = 'form-group';
@@ -223,9 +193,6 @@ class TeamUrl extends React.Component {
}
TeamUrl.propTypes = {
- intl: intlShape.isRequired,
state: React.PropTypes.object,
updateParent: React.PropTypes.func
};
-
-export default injectIntl(TeamUrl);
diff --git a/webapp/components/create_team/create_team.jsx b/webapp/components/create_team/create_team_controller.jsx
index 8a119a122..ad2a008bd 100644
--- a/webapp/components/create_team/create_team.jsx
+++ b/webapp/components/create_team/create_team_controller.jsx
@@ -8,7 +8,7 @@ import {browserHistory, Link} from 'react-router';
import React from 'react';
-export default class CreateTeam extends React.Component {
+export default class CreateTeamController extends React.Component {
constructor(props) {
super(props);
@@ -67,6 +67,6 @@ export default class CreateTeam extends React.Component {
}
}
-CreateTeam.propTypes = {
+CreateTeamController.propTypes = {
children: React.PropTypes.node
};
diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx
index bc67a34f9..92b16f925 100644
--- a/webapp/components/edit_post_modal.jsx
+++ b/webapp/components/edit_post_modal.jsx
@@ -5,7 +5,7 @@ import $ from 'jquery';
import ReactDOM from 'react-dom';
import Client from 'utils/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Textbox from './textbox.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import PostStore from 'stores/post_store.jsx';
diff --git a/webapp/components/error_bar.jsx b/webapp/components/error_bar.jsx
index d28be671d..c13d0dd6a 100644
--- a/webapp/components/error_bar.jsx
+++ b/webapp/components/error_bar.jsx
@@ -58,7 +58,12 @@ export default class ErrorBar extends React.Component {
e.preventDefault();
}
- ErrorStore.clearLastError();
+ if (ErrorStore.getLastError() && ErrorStore.getLastError().email_preview) {
+ ErrorStore.clearPreviewError();
+ } else {
+ ErrorStore.clearLastError();
+ }
+
this.setState({message: null});
}
@@ -81,7 +86,7 @@ export default class ErrorBar extends React.Component {
className='error-bar__close'
onClick={this.handleClose}
>
- &times;
+ {'×'}
</a>
</div>
);
diff --git a/webapp/components/file_attachment.jsx b/webapp/components/file_attachment.jsx
index 4a040a35b..d5bbae4c9 100644
--- a/webapp/components/file_attachment.jsx
+++ b/webapp/components/file_attachment.jsx
@@ -8,6 +8,7 @@ import Client from 'utils/web_client.jsx';
import Constants from 'utils/constants.jsx';
import {intlShape, injectIntl, defineMessages} from 'react-intl';
+import {Tooltip, OverlayTrigger} from 'react-bootstrap';
const holders = defineMessages({
download: {
@@ -24,6 +25,7 @@ class FileAttachment extends React.Component {
this.loadFiles = this.loadFiles.bind(this);
this.addBackgroundImage = this.addBackgroundImage.bind(this);
+ this.onAttachmentClick = this.onAttachmentClick.bind(this);
this.canSetState = false;
this.state = {fileSize: -1};
@@ -126,6 +128,10 @@ class FileAttachment extends React.Component {
$(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial');
}
}
+ onAttachmentClick(e) {
+ e.preventDefault();
+ this.props.handleImageClick(this.props.index);
+ }
render() {
var filename = this.props.filename;
@@ -149,12 +155,12 @@ class FileAttachment extends React.Component {
if (this.state.fileSize < 0) {
Client.getFileInfo(
filename,
- function success(data) {
+ (data) => {
if (this.canSetState) {
this.setState({fileSize: parseInt(data.size, 10)});
}
- }.bind(this),
- function error() {
+ },
+ () => {
// Do nothing
}
);
@@ -169,35 +175,64 @@ class FileAttachment extends React.Component {
} else {
trimmedFilename = filenameString;
}
+ var filenameOverlay = (
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={<Tooltip id='file-name__tooltip'>{this.props.intl.formatMessage(holders.download) + ' "' + filenameString + '"'}</Tooltip>}
+ >
+ <a
+ href={fileUrl}
+ download={filenameString}
+ className='post-image__name'
+ target='_blank'
+ rel='noopener noreferrer'
+ >
+ {trimmedFilename}
+ </a>
+ </OverlayTrigger>
+ );
+
+ if (this.props.compactDisplay) {
+ filenameOverlay = (
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={<Tooltip id='file-name__tooltip'>{filenameString}</Tooltip>}
+ >
+ <a
+ href='#'
+ onClick={this.onAttachmentClick}
+ className='post-image__name'
+ rel='noopener noreferrer'
+ >
+ <i className='glyphicon glyphicon-paperclip'/>{trimmedFilename}
+ </a>
+ </OverlayTrigger>
+ );
+ }
return (
<div
className='post-image__column'
key={filename}
>
- <a className='post-image__thumbnail'
+ <a
+ className='post-image__thumbnail'
href='#'
- onClick={() => this.props.handleImageClick(this.props.index)}
+ onClick={this.onAttachmentClick}
>
{thumbnail}
</a>
<div className='post-image__details'>
- <a
- href={fileUrl}
- download={filenameString}
- data-toggle='tooltip'
- title={this.props.intl.formatMessage(holders.download) + ' \"' + filenameString + '\"'}
- className='post-image__name'
- target='_blank'
- >
- {trimmedFilename}
- </a>
+ {filenameOverlay}
<div>
<a
href={fileUrl}
download={filenameString}
className='post-image__download'
target='_blank'
+ rel='noopener noreferrer'
>
<span
className='fa fa-download'
@@ -222,7 +257,9 @@ FileAttachment.propTypes = {
index: React.PropTypes.number.isRequired,
// handler for when the thumbnail is clicked passed the index above
- handleImageClick: React.PropTypes.func
+ handleImageClick: React.PropTypes.func,
+
+ compactDisplay: React.PropTypes.bool
};
export default injectIntl(FileAttachment);
diff --git a/webapp/components/file_attachment_list.jsx b/webapp/components/file_attachment_list.jsx
index 59fd56bc3..e4b841769 100644
--- a/webapp/components/file_attachment_list.jsx
+++ b/webapp/components/file_attachment_list.jsx
@@ -29,6 +29,7 @@ export default class FileAttachmentList extends React.Component {
filename={filenames[i]}
index={i}
handleImageClick={this.handleImageClick}
+ compactDisplay={this.props.compactDisplay}
/>
);
}
@@ -60,5 +61,7 @@ FileAttachmentList.propTypes = {
channelId: React.PropTypes.string,
// the user that owns the post that this is attached to
- userId: React.PropTypes.string
+ userId: React.PropTypes.string,
+
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/file_info_preview.jsx b/webapp/components/file_info_preview.jsx
index d5dcd75eb..fe4e76f91 100644
--- a/webapp/components/file_info_preview.jsx
+++ b/webapp/components/file_info_preview.jsx
@@ -38,6 +38,7 @@ export default function FileInfoPreview({filename, fileUrl, fileInfo, formatMess
className={'file-details__preview'}
to={fileUrl}
target='_blank'
+ rel='noopener noreferrer'
>
<span className='file-details__preview-helper'/>
<img src={Utils.getPreviewImagePath(filename)}/>
diff --git a/webapp/components/file_upload.jsx b/webapp/components/file_upload.jsx
index 05f1701a8..829e580b9 100644
--- a/webapp/components/file_upload.jsx
+++ b/webapp/components/file_upload.jsx
@@ -284,7 +284,10 @@ class FileUpload extends React.Component {
keyUpload(e) {
if ((e.ctrlKey || e.metaKey) && e.keyCode === Constants.KeyCodes.U) {
- $(this.refs.input).focus().trigger('click');
+ e.preventDefault();
+ if (this.props.postType === 'post' && document.activeElement.id === 'post_textbox' || this.props.postType === 'comment' && document.activeElement.id === 'reply_textbox') {
+ $(this.refs.fileInput).focus().trigger('click');
+ }
}
}
diff --git a/webapp/components/header_footer_template.jsx b/webapp/components/header_footer_template.jsx
index 71716c4fb..8267c73c5 100644
--- a/webapp/components/header_footer_template.jsx
+++ b/webapp/components/header_footer_template.jsx
@@ -28,13 +28,13 @@ export default class NotLoggedIn extends React.Component {
<span className='pull-right footer-site-name'>{global.window.mm_config.SiteName}</span>
</div>
<div className='col-xs-12'>
- <span className='pull-right footer-link copyright'>{'© 2015 Mattermost, Inc.'}</span>
+ <span className='pull-right footer-link copyright'>{'© 2015-2016 Mattermost, Inc.'}</span>
<a
id='help_link'
className='pull-right footer-link'
target='_blank'
+ rel='noopener noreferrer'
href={global.window.mm_config.HelpLink}
- rel='noreferrer'
>
<FormattedMessage id='web.footer.help'/>
</a>
@@ -42,8 +42,8 @@ export default class NotLoggedIn extends React.Component {
id='terms_link'
className='pull-right footer-link'
target='_blank'
+ rel='noopener noreferrer'
href={global.window.mm_config.TermsOfServiceLink}
- rel='noreferrer'
>
<FormattedMessage id='web.footer.terms'/>
</a>
@@ -51,8 +51,8 @@ export default class NotLoggedIn extends React.Component {
id='privacy_link'
className='pull-right footer-link'
target='_blank'
+ rel='noopener noreferrer'
href={global.window.mm_config.PrivacyPolicyLink}
- rel='noreferrer'
>
<FormattedMessage id='web.footer.privacy'/>
</a>
@@ -60,8 +60,8 @@ export default class NotLoggedIn extends React.Component {
id='about_link'
className='pull-right footer-link'
target='_blank'
+ rel='noopener noreferrer'
href={global.window.mm_config.AboutLink}
- rel='noreferrer'
>
<FormattedMessage id='web.footer.about'/>
</a>
diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx
index 4ac620f08..96a9eb75d 100644
--- a/webapp/components/invite_member_modal.jsx
+++ b/webapp/components/invite_member_modal.jsx
@@ -6,7 +6,7 @@ import * as utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import Client from 'utils/web_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import ModalStore from 'stores/modal_store.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
@@ -258,15 +258,17 @@ class InviteMemberModal extends React.Component {
var removeButton = null;
if (index) {
- removeButton = (<div>
- <button
- type='button'
- className='btn btn-link remove__member'
- onClick={this.removeInviteFields.bind(this, index)}
- >
- <span className='fa fa-trash'></span>
- </button>
- </div>);
+ removeButton = (
+ <div>
+ <button
+ type='button'
+ className='btn btn-link remove__member'
+ onClick={this.removeInviteFields.bind(this, index)}
+ >
+ <span className='fa fa-trash'></span>
+ </button>
+ </div>
+ );
}
var emailClass = 'form-group invite';
if (emailError) {
@@ -283,54 +285,56 @@ class InviteMemberModal extends React.Component {
if (lastNameError) {
lastNameClass += ' has-error';
}
- nameFields = (<div className='row--invite'>
- <div className='col-sm-6'>
- <div className={firstNameClass}>
- <input
- type='text'
- className='form-control'
- ref={'first_name' + index}
- placeholder={formatMessage(holders.firstname)}
- maxLength='64'
- disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
- spellCheck='false'
- />
- {firstNameError}
- </div>
- </div>
- <div className='col-sm-6'>
- <div className={lastNameClass}>
- <input
- type='text'
- className='form-control'
- ref={'last_name' + index}
- placeholder={formatMessage(holders.lastname)}
- maxLength='64'
- disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
- spellCheck='false'
- />
- {lastNameError}
- </div>
- </div>
- </div>);
+ nameFields = (
+ <div className='row--invite'>
+ <div className='col-sm-6'>
+ <div className={firstNameClass}>
+ <input
+ type='text'
+ className='form-control'
+ ref={'first_name' + index}
+ placeholder={formatMessage(holders.firstname)}
+ maxLength='64'
+ disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
+ spellCheck='false'
+ />
+ {firstNameError}
+ </div>
+ </div>
+ <div className='col-sm-6'>
+ <div className={lastNameClass}>
+ <input
+ type='text'
+ className='form-control'
+ ref={'last_name' + index}
+ placeholder={formatMessage(holders.lastname)}
+ maxLength='64'
+ disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
+ spellCheck='false'
+ />
+ {lastNameError}
+ </div>
+ </div>
+ </div>
+ );
inviteSections[index] = (
<div key={'key' + index}>
- {removeButton}
- <div className={emailClass}>
- <input
- onKeyUp={this.displayNameKeyUp}
- type='text'
- ref={'email' + index}
- className='form-control'
- placeholder='email@domain.com'
- maxLength='64'
- disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
- spellCheck='false'
- />
- {emailError}
- </div>
- {nameFields}
+ {removeButton}
+ <div className={emailClass}>
+ <input
+ onKeyUp={this.displayNameKeyUp}
+ type='text'
+ ref={'email' + index}
+ className='form-control'
+ placeholder='email@domain.com'
+ maxLength='64'
+ disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
+ spellCheck='false'
+ />
+ {emailError}
+ </div>
+ {nameFields}
</div>
);
}
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 9f1fac6bc..0c37d62cb 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -8,7 +8,7 @@ import UserStore from 'stores/user_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
-import * as Websockets from 'action_creators/websocket_actions.jsx';
+import * as Websockets from 'actions/websocket_actions.jsx';
import Constants from 'utils/constants.jsx';
import {browserHistory} from 'react-router';
diff --git a/webapp/components/login/login.jsx b/webapp/components/login/login_controller.jsx
index e0969001a..1b1f65436 100644
--- a/webapp/components/login/login.jsx
+++ b/webapp/components/login/login_controller.jsx
@@ -5,10 +5,11 @@ import LoginMfa from './components/login_mfa.jsx';
import ErrorBar from 'components/error_bar.jsx';
import FormError from 'components/form_error.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import UserStore from 'stores/user_store.jsx';
import Client from 'utils/web_client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -20,7 +21,7 @@ import {browserHistory, Link} from 'react-router';
import React from 'react';
import logoImage from 'images/logo.png';
-export default class Login extends React.Component {
+export default class LoginController extends React.Component {
constructor(props) {
super(props);
@@ -44,13 +45,15 @@ export default class Login extends React.Component {
if (UserStore.getCurrentUser()) {
browserHistory.push('/select_team');
}
+
+ AsyncClient.checkVersion();
}
preSubmit(e) {
e.preventDefault();
const loginId = this.state.loginId.trim();
- const password = this.state.password.trim();
+ const password = this.state.password;
if (global.window.mm_config.EnableMultifactorAuthentication === 'true') {
Client.checkMfa(
@@ -257,6 +260,7 @@ export default class Login extends React.Component {
onChange={this.handleLoginIdChange}
placeholder={this.createLoginPlaceholder(emailSigninEnabled, usernameSigninEnabled, ldapEnabled)}
spellCheck='false'
+ autoCapitalize='off'
/>
</div>
<div className={'form-group' + errorClass}>
@@ -289,7 +293,10 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableOpenServer === 'true') {
loginControls.push(
- <div key='signup'>
+ <div
+ className='form-group'
+ key='signup'
+ >
<span>
<FormattedMessage
id='login.noAccount'
@@ -440,8 +447,8 @@ export default class Login extends React.Component {
}
}
-Login.defaultProps = {
+LoginController.defaultProps = {
};
-Login.propTypes = {
+LoginController.propTypes = {
params: React.PropTypes.object.isRequired
};
diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx
index 087db68e6..34cad32d2 100644
--- a/webapp/components/more_channels.jsx
+++ b/webapp/components/more_channels.jsx
@@ -9,7 +9,7 @@ import ChannelStore from 'stores/channel_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router';
diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx
index 2895047b5..de61bcf98 100644
--- a/webapp/components/more_direct_channels.jsx
+++ b/webapp/components/more_direct_channels.jsx
@@ -6,7 +6,7 @@ import FilteredUserList from './filtered_user_list.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import * as Utils from 'utils/utils.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router';
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index 21ca53649..d4968986e 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -17,6 +17,7 @@ import ToggleModalButton from './toggle_modal_button.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
import Client from 'utils/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
@@ -34,6 +35,8 @@ import {Link, browserHistory} from 'react-router';
import React from 'react';
+import * as GlobalActions from 'actions/global_actions.jsx';
+
export default class Navbar extends React.Component {
constructor(props) {
super(props);
@@ -50,6 +53,12 @@ export default class Navbar extends React.Component {
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
+ this.navigateChannelShortcut = this.navigateChannelShortcut.bind(this);
+ this.navigateUnreadChannelShortcut = this.navigateUnreadChannelShortcut.bind(this);
+ this.getDisplayedChannels = this.getDisplayedChannels.bind(this);
+ this.compareByName = this.compareByName.bind(this);
+ this.compareByDisplayName = this.compareByDisplayName.bind(this);
+
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
state.showEditChannelHeaderModal = false;
@@ -72,10 +81,14 @@ export default class Navbar extends React.Component {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addExtraInfoChangeListener(this.onChange);
$('.inner-wrap').click(this.hideSidebars);
+ document.addEventListener('keydown', this.navigateChannelShortcut);
+ document.addEventListener('keydown', this.navigateUnreadChannelShortcut);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
ChannelStore.removeExtraInfoChangeListener(this.onChange);
+ document.removeEventListener('keydown', this.navigateChannelShortcut);
+ document.removeEventListener('keydown', this.navigateUnreadChannelShortcut);
}
handleSubmit(e) {
e.preventDefault();
@@ -150,6 +163,96 @@ export default class Navbar extends React.Component {
showRenameChannelModal: false
});
}
+ navigateChannelShortcut(e) {
+ if (e.altKey && !e.shiftKey && (e.keyCode === Constants.KeyCodes.UP || e.keyCode === Constants.KeyCodes.DOWN)) {
+ e.preventDefault();
+ const allChannels = this.getDisplayedChannels();
+ const curChannel = this.state.channel;
+ let curIndex = -1;
+ for (let i = 0; i < allChannels.length; i++) {
+ if (allChannels[i].id === curChannel.id) {
+ curIndex = i;
+ }
+ }
+ let nextChannel = curChannel;
+ let nextIndex = curIndex;
+ if (e.keyCode === Constants.KeyCodes.DOWN) {
+ nextIndex = curIndex + 1;
+ } else if (e.keyCode === Constants.KeyCodes.UP) {
+ nextIndex = curIndex - 1;
+ }
+ nextChannel = allChannels[Utils.mod(nextIndex, allChannels.length)];
+ GlobalActions.emitChannelClickEvent(nextChannel);
+ }
+ }
+ navigateUnreadChannelShortcut(e) {
+ if (e.altKey && e.shiftKey && (e.keyCode === Constants.KeyCodes.UP || e.keyCode === Constants.KeyCodes.DOWN)) {
+ e.preventDefault();
+ const allChannels = this.getDisplayedChannels();
+ const curChannel = this.state.channel;
+ let curIndex = -1;
+ for (let i = 0; i < allChannels.length; i++) {
+ if (allChannels[i].id === curChannel.id) {
+ curIndex = i;
+ }
+ }
+ let nextChannel = curChannel;
+ let nextIndex = curIndex;
+ let count = 0;
+ let increment = 0;
+ if (e.keyCode === Constants.KeyCodes.UP) {
+ increment = -1;
+ } else if (e.keyCode === Constants.KeyCodes.DOWN) {
+ increment = 1;
+ }
+ let unreadCounts = ChannelStore.getUnreadCount(allChannels[nextIndex].id);
+ while (count < allChannels.length && unreadCounts.msgs === 0 && unreadCounts.mentions === 0) {
+ nextIndex += increment;
+ count++;
+ nextIndex = Utils.mod(nextIndex, allChannels.length);
+ unreadCounts = ChannelStore.getUnreadCount(allChannels[nextIndex].id);
+ }
+ if (unreadCounts.msgs !== 0 || unreadCounts.mentions !== 0) {
+ nextChannel = allChannels[nextIndex];
+ GlobalActions.emitChannelClickEvent(nextChannel);
+ }
+ }
+ }
+ getDisplayedChannels() {
+ const allChannels = ChannelStore.getChannels().sort(this.compareByName);
+ const publicChannels = allChannels.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
+ const privateChannels = allChannels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
+
+ const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
+
+ const directChannels = [];
+ const directNonTeamChannels = [];
+ for (const [name, value] of preferences) {
+ if (value !== 'true') {
+ continue;
+ }
+
+ const directChannel = allChannels.find(Utils.isDirectChannelForUser.bind(null, name));
+ directChannel.display_name = Utils.displayUsername(name);
+
+ if (UserStore.hasTeamProfile(name)) {
+ directChannels.push(directChannel);
+ } else {
+ directNonTeamChannels.push(directChannel);
+ }
+ }
+
+ directChannels.sort(this.compareByDisplayName);
+ directNonTeamChannels.sort(this.compareByDisplayName);
+
+ return publicChannels.concat(privateChannels).concat(directChannels).concat(directNonTeamChannels);
+ }
+ compareByName(a, b) {
+ return a.name.localeCompare(b.name);
+ }
+ compareByDisplayName(a, b) {
+ return a.display_name.localeCompare(b.display_name);
+ }
createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) {
if (channel) {
var viewInfoOption = (
@@ -218,20 +321,23 @@ export default class Navbar extends React.Component {
</li>
);
- leaveChannelOption = (
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={this.handleLeave}
- >
- <FormattedMessage
- id='navbar.leave'
- defaultMessage='Leave Channel'
- />
- </a>
- </li>
- );
+ const canLeave = channel.type === Constants.PRIVATE_CHANNEL ? this.state.userCount > 1 : true;
+ if (canLeave) {
+ leaveChannelOption = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleLeave}
+ >
+ <FormattedMessage
+ id='navbar.leave'
+ defaultMessage='Leave Channel'
+ />
+ </a>
+ </li>
+ );
+ }
}
var manageMembersOption;
diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx
index 7f1cfce7c..87df3848d 100644
--- a/webapp/components/navbar_dropdown.jsx
+++ b/webapp/components/navbar_dropdown.jsx
@@ -4,7 +4,7 @@
import $ from 'jquery';
import ReactDOM from 'react-dom';
import * as Utils from 'utils/utils.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -28,6 +28,7 @@ export default class NavbarDropdown extends React.Component {
this.handleAboutModal = this.handleAboutModal.bind(this);
this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
+ this.openAccountSettings = this.openAccountSettings.bind(this);
this.state = {
showUserSettingsModal: false,
@@ -53,6 +54,7 @@ export default class NavbarDropdown extends React.Component {
});
TeamStore.addChangeListener(this.onTeamChange);
+ document.addEventListener('keydown', this.openAccountSettings);
}
onTeamChange() {
@@ -65,14 +67,19 @@ export default class NavbarDropdown extends React.Component {
componentWillUnmount() {
$(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
TeamStore.removeChangeListener(this.onTeamChange);
+ document.removeEventListener('keydown', this.openAccountSettings);
+ }
+ openAccountSettings(e) {
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.keyCode === Constants.KeyCodes.A) {
+ e.preventDefault();
+ this.setState({showUserSettingsModal: true});
+ }
}
-
render() {
var teamLink = '';
var inviteLink = '';
var manageLink = '';
var sysAdminLink = '';
- var adminDivider = '';
var currentUser = this.props.currentUser;
var isAdmin = false;
var isSystemAdmin = false;
@@ -126,8 +133,6 @@ export default class NavbarDropdown extends React.Component {
</li>
);
- adminDivider = (<li className='divider'></li>);
-
teamSettings = (
<li>
<a
@@ -213,6 +218,10 @@ export default class NavbarDropdown extends React.Component {
<Link
to={'/' + team.name + '/channels/town-square'}
>
+ <FormattedMessage
+ id='navbar_dropdown.switchTo'
+ defaultMessage='Switch to '
+ />
{team.display_name}
</Link>
</li>
@@ -228,8 +237,8 @@ export default class NavbarDropdown extends React.Component {
<li>
<Link
target='_blank'
+ rel='noopener noreferrer'
to={global.window.mm_config.HelpLink}
- rel='noreferrer'
>
<FormattedMessage
id='navbar_dropdown.help'
@@ -246,8 +255,8 @@ export default class NavbarDropdown extends React.Component {
<li>
<Link
target='_blank'
+ rel='noopener noreferrer'
to={global.window.mm_config.ReportAProblemLink}
- rel='noreferrer'
>
<FormattedMessage
id='navbar_dropdown.report'
@@ -304,7 +313,7 @@ export default class NavbarDropdown extends React.Component {
/>
</a>
</li>
- {adminDivider}
+ <li className='divider'></li>
{teamSettings}
{integrationsLink}
{manageLink}
diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx
index c2f450f98..955758237 100644
--- a/webapp/components/needs_team.jsx
+++ b/webapp/components/needs_team.jsx
@@ -12,7 +12,7 @@ import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
const TutorialSteps = Constants.TutorialSteps;
const Preferences = Constants.Preferences;
diff --git a/webapp/components/password_reset_form.jsx b/webapp/components/password_reset_form.jsx
index 23b8952cc..887bc0c8e 100644
--- a/webapp/components/password_reset_form.jsx
+++ b/webapp/components/password_reset_form.jsx
@@ -22,7 +22,7 @@ class PasswordResetForm extends React.Component {
handlePasswordReset(e) {
e.preventDefault();
- const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.password).value;
if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) {
this.setState({
error: (
diff --git a/webapp/components/pending_post_actions.jsx b/webapp/components/pending_post_actions.jsx
new file mode 100644
index 000000000..7528ef207
--- /dev/null
+++ b/webapp/components/pending_post_actions.jsx
@@ -0,0 +1,92 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import PostStore from 'stores/post_store.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+
+import Client from 'utils/web_client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import Constants from 'utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+export default class PendingPostActions extends React.Component {
+ constructor(props) {
+ super(props);
+ this.retryPost = this.retryPost.bind(this);
+ this.cancelPost = this.cancelPost.bind(this);
+ this.state = {};
+ }
+ retryPost(e) {
+ e.preventDefault();
+
+ var post = this.props.post;
+ Client.createPost(post,
+ (data) => {
+ AsyncClient.getPosts(post.channel_id);
+
+ var channel = ChannelStore.get(post.channel_id);
+ var member = ChannelStore.getMember(post.channel_id);
+ member.msg_count = channel.total_msg_count;
+ member.last_viewed_at = (new Date()).getTime();
+ ChannelStore.setChannelMember(member);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_POST,
+ post: data
+ });
+ },
+ () => {
+ post.state = Constants.POST_FAILED;
+ PostStore.updatePendingPost(post);
+ this.forceUpdate();
+ }
+ );
+
+ post.state = Constants.POST_LOADING;
+ PostStore.updatePendingPost(post);
+ this.forceUpdate();
+ }
+ cancelPost(e) {
+ e.preventDefault();
+
+ var post = this.props.post;
+ PostStore.removePendingPost(post.channel_id, post.pending_post_id);
+ this.forceUpdate();
+ }
+ render() {
+ return (<span className='pending-post-actions'>
+ <a
+ className='post-retry'
+ href='#'
+ onClick={this.retryPost}
+ >
+ <FormattedMessage
+ id='pending_post_actions.retry'
+ defaultMessage='Retry'
+ />
+ </a>
+ {' - '}
+ <a
+ className='post-cancel'
+ href='#'
+ onClick={this.cancelPost}
+ >
+ <FormattedMessage
+ id='pending_post_actions.cancel'
+ defaultMessage='Cancel'
+ />
+ </a>
+ </span>);
+ }
+}
+
+PendingPostActions.propTypes = {
+ post: React.PropTypes.object
+};
diff --git a/webapp/components/post.jsx b/webapp/components/post.jsx
index ae3fa9c98..2b28d442c 100644
--- a/webapp/components/post.jsx
+++ b/webapp/components/post.jsx
@@ -4,14 +4,9 @@
import PostHeader from './post_header.jsx';
import PostBody from './post_body.jsx';
-import PostStore from 'stores/post_store.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
-
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
@@ -23,7 +18,6 @@ export default class Post extends React.Component {
this.handleCommentClick = this.handleCommentClick.bind(this);
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
- this.retryPost = this.retryPost.bind(this);
this.state = {};
}
@@ -44,36 +38,6 @@ export default class Post extends React.Component {
this.refs.info.forceUpdate();
this.refs.header.forceUpdate();
}
- retryPost(e) {
- e.preventDefault();
-
- var post = this.props.post;
- Client.createPost(post,
- (data) => {
- AsyncClient.getPosts();
-
- var channel = ChannelStore.get(post.channel_id);
- var member = ChannelStore.getMember(post.channel_id);
- member.msg_count = channel.total_msg_count;
- member.last_viewed_at = Utils.getTimestamp();
- ChannelStore.setChannelMember(member);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_POST,
- post: data
- });
- },
- () => {
- post.state = Constants.POST_FAILED;
- PostStore.updatePendingPost(post);
- this.forceUpdate();
- }
- );
-
- post.state = Constants.POST_LOADING;
- PostStore.updatePendingPost(post);
- this.forceUpdate();
- }
shouldComponentUpdate(nextProps) {
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
@@ -103,6 +67,10 @@ export default class Post extends React.Component {
return true;
}
+ if (nextProps.compactDisplay !== this.props.compactDisplay) {
+ return true;
+ }
+
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
return true;
}
@@ -187,24 +155,21 @@ export default class Post extends React.Component {
systemMessageClass = 'post--system';
}
- let profilePic = null;
- if (!this.props.hideProfilePic) {
+ let profilePic = (
+ <img
+ src={Utils.getProfilePicSrcForPost(post, timestamp)}
+ height='36'
+ width='36'
+ />
+ );
+
+ if (Utils.isSystemMessage(post)) {
profilePic = (
- <img
- src={Utils.getProfilePicSrcForPost(post, timestamp)}
- height='36'
- width='36'
+ <span
+ className='icon'
+ dangerouslySetInnerHTML={{__html: mattermostLogo}}
/>
);
-
- if (Utils.isSystemMessage(post)) {
- profilePic = (
- <span
- className='icon'
- dangerouslySetInnerHTML={{__html: mattermostLogo}}
- />
- );
- }
}
let centerClass = '';
@@ -212,11 +177,16 @@ export default class Post extends React.Component {
centerClass = 'center';
}
+ let compactClass = '';
+ if (this.props.compactDisplay) {
+ compactClass = 'post--compact';
+ }
+
return (
<div>
<div
id={'post_' + post.id}
- className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass}
+ className={'post ' + sameUserClass + ' ' + compactClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass}
>
<div className={'post__content ' + centerClass}>
<div className='post__img'>{profilePic}</div>
@@ -231,6 +201,7 @@ export default class Post extends React.Component {
sameUser={this.props.sameUser}
user={this.props.user}
currentUser={this.props.currentUser}
+ compactDisplay={this.props.compactDisplay}
/>
<PostBody
post={post}
@@ -238,7 +209,7 @@ export default class Post extends React.Component {
parentPost={parentPost}
posts={posts}
handleCommentClick={this.handleCommentClick}
- retryPost={this.retryPost}
+ compactDisplay={this.props.compactDisplay}
/>
</div>
</div>
@@ -261,5 +232,6 @@ Post.propTypes = {
displayNameType: React.PropTypes.string,
hasProfiles: React.PropTypes.bool,
currentUser: React.PropTypes.object.isRequired,
- center: React.PropTypes.bool
+ center: React.PropTypes.bool,
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/post_attachment.jsx b/webapp/components/post_attachment.jsx
index 1c3df6ea2..8b5ff91f2 100644
--- a/webapp/components/post_attachment.jsx
+++ b/webapp/components/post_attachment.jsx
@@ -59,7 +59,7 @@ class PostAttachment extends React.Component {
toggleCollapseState(e) {
e.preventDefault();
- let state = this.state;
+ const state = this.state;
state.text = state.collapsed ? state.uncollapsedText : state.collapsedText;
state.collapsed = !state.collapsed;
this.setState(state);
@@ -142,22 +142,22 @@ class PostAttachment extends React.Component {
});
if (headerCols.length > 0) { // Flush last fields
fieldTables.push(
- <table
- className='attachment___fields'
- key={'attachment__table__' + nrTables}
- >
- <thead>
- <tr>
+ <table
+ className='attachment___fields'
+ key={'attachment__table__' + nrTables}
+ >
+ <thead>
+ <tr>
{headerCols}
- </tr>
- </thead>
- <tbody>
- <tr>
- {bodyCols}
- </tr>
- </tbody>
- </table>
- );
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ {bodyCols}
+ </tr>
+ </tbody>
+ </table>
+ );
}
return (
<div>
@@ -209,6 +209,7 @@ class PostAttachment extends React.Component {
<a
href={data.author_link}
target='_blank'
+ rel='noopener noreferrer'
>
{author}
</a>
@@ -226,6 +227,7 @@ class PostAttachment extends React.Component {
className='attachment__title-link'
href={data.title_link}
target='_blank'
+ rel='noopener noreferrer'
>
{data.title}
</a>
diff --git a/webapp/components/post_attachment_oembed.jsx b/webapp/components/post_attachment_oembed.jsx
index a4e4ce001..359c7cc35 100644
--- a/webapp/components/post_attachment_oembed.jsx
+++ b/webapp/components/post_attachment_oembed.jsx
@@ -72,30 +72,31 @@ export default class PostAttachmentOEmbed extends React.Component {
className='attachment attachment--oembed'
ref='attachment'
>
- <div className='attachment__content'>
- <div
- className={'clearfix attachment__container'}
+ <div className='attachment__content'>
+ <div
+ className={'clearfix attachment__container'}
+ >
+ <h1
+ className='attachment__title'
>
- <h1
- className='attachment__title'
+ <a
+ className='attachment__title-link'
+ href={data.url}
+ target='_blank'
+ rel='noopener noreferrer'
>
- <a
- className='attachment__title-link'
- href={data.url}
- target='_blank'
- >
- {data.title}
- </a>
- </h1>
- <div >
- <div
- className={'attachment__body attachment__body--no_thumb'}
- >
- {content}
- </div>
+ {data.title}
+ </a>
+ </h1>
+ <div >
+ <div
+ className={'attachment__body attachment__body--no_thumb'}
+ >
+ {content}
</div>
</div>
</div>
+ </div>
</div>
);
}
diff --git a/webapp/components/post_body.jsx b/webapp/components/post_body.jsx
index 6c4e97d8e..415052d96 100644
--- a/webapp/components/post_body.jsx
+++ b/webapp/components/post_body.jsx
@@ -7,6 +7,7 @@ import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
+import PendingPostActions from './pending_post_actions.jsx';
import {FormattedMessage} from 'react-intl';
@@ -24,7 +25,7 @@ export default class PostBody extends React.Component {
return true;
}
- if (nextProps.retryPost.toString() !== this.props.retryPost.toString()) {
+ if (!Utils.areObjectsEqual(nextProps.compactDisplay, this.props.compactDisplay)) {
return true;
}
@@ -110,18 +111,7 @@ export default class PostBody extends React.Component {
let loading;
if (post.state === Constants.POST_FAILED) {
postClass += ' post--fail';
- loading = (
- <a
- className='theme post-retry pull-right'
- href='#'
- onClick={this.props.retryPost}
- >
- <FormattedMessage
- id='post_body.retry'
- defaultMessage='Retry'
- />
- </a>
- );
+ loading = <PendingPostActions post={this.props.post}/>;
} else if (post.state === Constants.POST_LOADING) {
postClass += ' post-waiting';
loading = (
@@ -136,9 +126,11 @@ export default class PostBody extends React.Component {
if (filenames && filenames.length > 0) {
fileAttachmentHolder = (
<FileAttachmentList
+
filenames={filenames}
channelId={post.channel_id}
userId={post.user_id}
+ compactDisplay={this.props.compactDisplay}
/>
);
}
@@ -189,5 +181,6 @@ PostBody.propTypes = {
post: React.PropTypes.object.isRequired,
parentPost: React.PropTypes.object,
retryPost: React.PropTypes.func.isRequired,
- handleCommentClick: React.PropTypes.func.isRequired
+ handleCommentClick: React.PropTypes.func.isRequired,
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/post_body_additional_content.jsx b/webapp/components/post_body_additional_content.jsx
index 452597dde..cdb735b47 100644
--- a/webapp/components/post_body_additional_content.jsx
+++ b/webapp/components/post_body_additional_content.jsx
@@ -120,7 +120,8 @@ export default class PostBodyAdditionalContent extends React.Component {
let toggle;
if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_TOGGLE)) {
toggle = (
- <a className='post__embed-visibility'
+ <a
+ className='post__embed-visibility'
data-expanded={this.state.embedVisible}
aria-label='Toggle Embed Visibility'
onClick={this.toggleEmbedVisibility}
@@ -131,7 +132,8 @@ export default class PostBodyAdditionalContent extends React.Component {
return (
<div>
{toggle}
- <div className='post__embed-container'
+ <div
+ className='post__embed-container'
hidden={!this.state.embedVisible}
>
{generateEmbed}
diff --git a/webapp/components/post_focus_view.jsx b/webapp/components/post_focus_view.jsx
index 0655a9916..30a2f9d72 100644
--- a/webapp/components/post_focus_view.jsx
+++ b/webapp/components/post_focus_view.jsx
@@ -5,7 +5,7 @@ import PostsView from './posts_view.jsx';
import PostStore from 'stores/post_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import React from 'react';
diff --git a/webapp/components/post_header.jsx b/webapp/components/post_header.jsx
index 9161d37f9..6fae092e5 100644
--- a/webapp/components/post_header.jsx
+++ b/webapp/components/post_header.jsx
@@ -14,6 +14,7 @@ export default class PostHeader extends React.Component {
super(props);
this.state = {};
}
+
render() {
const post = this.props.post;
@@ -31,7 +32,7 @@ export default class PostHeader extends React.Component {
);
}
- botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>;
+ botIndicator = <li className='col col__name bot-indicator'>{Constants.BOT_NAME}</li>;
} else if (Utils.isSystemMessage(post)) {
userProfile = (
<UserProfile
@@ -56,6 +57,7 @@ export default class PostHeader extends React.Component {
isLastComment={this.props.isLastComment}
sameUser={this.props.sameUser}
currentUser={this.props.currentUser}
+ compactDisplay={this.props.compactDisplay}
/>
</li>
</ul>
@@ -76,5 +78,6 @@ PostHeader.propTypes = {
commentCount: React.PropTypes.number.isRequired,
isLastComment: React.PropTypes.bool.isRequired,
handleCommentClick: React.PropTypes.func.isRequired,
- sameUser: React.PropTypes.bool.isRequired
+ sameUser: React.PropTypes.bool.isRequired,
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/post_info.jsx b/webapp/components/post_info.jsx
index 50b03c0be..e3b80e45c 100644
--- a/webapp/components/post_info.jsx
+++ b/webapp/components/post_info.jsx
@@ -4,7 +4,7 @@
import $ from 'jquery';
import * as Utils from 'utils/utils.jsx';
import TimeSince from './time_since.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -33,6 +33,7 @@ export default class PostInfo extends React.Component {
var post = this.props.post;
var isOwner = this.props.currentUser.id === post.user_id;
var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+ const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) {
return '';
@@ -108,7 +109,7 @@ export default class PostInfo extends React.Component {
);
}
- if (isOwner) {
+ if (isOwner && !isSystemMessage) {
dropdownContents.push(
<li
key='editPost'
@@ -219,6 +220,7 @@ export default class PostInfo extends React.Component {
<TimeSince
eventTime={post.create_at}
sameUser={this.props.sameUser}
+ compactDisplay={this.props.compactDisplay}
/>
</li>
<li className='col col__reply'>
@@ -250,5 +252,6 @@ PostInfo.propTypes = {
allowReply: React.PropTypes.string.isRequired,
handleCommentClick: React.PropTypes.func.isRequired,
sameUser: React.PropTypes.bool.isRequired,
- currentUser: React.PropTypes.object.isRequired
+ currentUser: React.PropTypes.object.isRequired,
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/posts_view.jsx b/webapp/components/posts_view.jsx
index cc9e738bc..64da4e67c 100644
--- a/webapp/components/posts_view.jsx
+++ b/webapp/components/posts_view.jsx
@@ -6,7 +6,7 @@ import $ from 'jquery';
import Post from './post.jsx';
import FloatingTimestamp from './floating_timestamp.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -55,6 +55,7 @@ export default class PostsView extends React.Component {
this.state = {
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
centerPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
+ compactPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
isScrolling: false,
topPostId: null,
currentUser: UserStore.getCurrentUser(),
@@ -79,7 +80,8 @@ export default class PostsView extends React.Component {
updateState() {
this.setState({
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
- centerPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED
+ centerPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
+ compactPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT
});
}
onUserChange() {
@@ -274,6 +276,7 @@ export default class PostsView extends React.Component {
user={profile}
currentUser={this.state.currentUser}
center={this.state.centerPosts}
+ compactDisplay={this.state.compactPosts}
/>
);
@@ -479,6 +482,9 @@ export default class PostsView extends React.Component {
if (this.state.centerPosts !== nextState.centerPosts) {
return true;
}
+ if (this.state.compactPosts !== nextState.compactPosts) {
+ return true;
+ }
if (!Utils.areObjectsEqual(this.state.profiles, nextState.profiles)) {
return true;
}
@@ -592,7 +598,8 @@ PostsView.propTypes = {
showMoreMessagesBottom: React.PropTypes.bool,
channel: React.PropTypes.object,
messageSeparatorTime: React.PropTypes.number,
- postsToHighlight: React.PropTypes.object
+ postsToHighlight: React.PropTypes.object,
+ compactDisplay: React.PropTypes.bool
};
function ScrollToBottomArrows({isScrolling, atBottom, onClick}) {
diff --git a/webapp/components/posts_view_container.jsx b/webapp/components/posts_view_container.jsx
index d1d8a2093..3f8a44cc3 100644
--- a/webapp/components/posts_view_container.jsx
+++ b/webapp/components/posts_view_container.jsx
@@ -9,7 +9,7 @@ import LoadingScreen from './loading_screen.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
diff --git a/webapp/components/removed_from_channel_modal.jsx b/webapp/components/removed_from_channel_modal.jsx
index d037c089d..2199dbbec 100644
--- a/webapp/components/removed_from_channel_modal.jsx
+++ b/webapp/components/removed_from_channel_modal.jsx
@@ -98,7 +98,11 @@ export default class RemovedFromChannelModal extends React.Component {
className='close'
data-dismiss='modal'
aria-label='Close'
- ><span aria-hidden='true'>&times;</span></button>
+ >
+ <span aria-hidden='true'>
+ {'×'}
+ </span>
+ </button>
<h4 className='modal-title'>
<FormattedMessage
id='removed_channel.from'
@@ -107,16 +111,16 @@ export default class RemovedFromChannelModal extends React.Component {
<span className='name'>{channelName}</span></h4>
</div>
<div className='modal-body'>
- <p>
- <FormattedMessage
- id='removed_channel.remover'
- defaultMessage='{remover} removed you from {channel}'
- values={{
- remover: (remover),
- channel: (channelName)
- }}
- />
- </p>
+ <p>
+ <FormattedMessage
+ id='removed_channel.remover'
+ defaultMessage='{remover} removed you from {channel}'
+ values={{
+ remover: (remover),
+ channel: (channelName)
+ }}
+ />
+ </p>
</div>
<div className='modal-footer'>
<button
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index 5097e0573..a771803b8 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -3,22 +3,18 @@
import UserProfile from './user_profile.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
+import PendingPostActions from './pending_post_actions.jsx';
-import PostStore from 'stores/post_store.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import Constants from 'utils/constants.jsx';
-const ActionTypes = Constants.ActionTypes;
import {FormattedMessage, FormattedDate} from 'react-intl';
@@ -30,41 +26,10 @@ export default class RhsComment extends React.Component {
constructor(props) {
super(props);
- this.retryComment = this.retryComment.bind(this);
this.handlePermalink = this.handlePermalink.bind(this);
this.state = {};
}
- retryComment(e) {
- e.preventDefault();
-
- var post = this.props.post;
- Client.createPost(post,
- (data) => {
- AsyncClient.getPosts(post.channel_id);
-
- var channel = ChannelStore.get(post.channel_id);
- var member = ChannelStore.getMember(post.channel_id);
- member.msg_count = channel.total_msg_count;
- member.last_viewed_at = (new Date()).getTime();
- ChannelStore.setChannelMember(member);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_POST,
- post: data
- });
- },
- () => {
- post.state = Constants.POST_FAILED;
- PostStore.updatePendingPost(post);
- this.forceUpdate();
- }
- );
-
- post.state = Constants.POST_LOADING;
- PostStore.updatePendingPost(post);
- this.forceUpdate();
- }
handlePermalink(e) {
e.preventDefault();
GlobalActions.showGetPostLinkModal(this.props.post);
@@ -85,6 +50,7 @@ export default class RhsComment extends React.Component {
const isOwner = this.props.currentUser.id === post.user_id;
var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+ const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
var dropdownContents = [];
@@ -107,7 +73,7 @@ export default class RhsComment extends React.Component {
);
}
- if (isOwner) {
+ if (isOwner && !isSystemMessage) {
dropdownContents.push(
<li
role='presentation'
@@ -185,6 +151,11 @@ export default class RhsComment extends React.Component {
var timestamp = this.props.currentUser.update_at;
+ let botIndicator;
+
+ if (post.props && post.props.from_webhook) {
+ botIndicator = <li className='col col__name bot-indicator'>{Constants.BOT_NAME}</li>;
+ }
let loading;
let postClass = '';
let message = (
@@ -197,18 +168,7 @@ export default class RhsComment extends React.Component {
if (post.state === Constants.POST_FAILED) {
postClass += ' post-fail';
- loading = (
- <a
- className='theme post-retry pull-right'
- href='#'
- onClick={this.retryComment}
- >
- <FormattedMessage
- id='rhs_comment.retry'
- defaultMessage='Retry'
- />
- </a>
- );
+ loading = <PendingPostActions post={this.props.post}/>;
} else if (post.state === Constants.POST_LOADING) {
postClass += ' post-waiting';
loading = (
@@ -251,9 +211,10 @@ export default class RhsComment extends React.Component {
</div>
<div>
<ul className='post__header'>
- <li className='col__name'>
+ <li className='col col__name'>
<strong><UserProfile user={this.props.user}/></strong>
</li>
+ {botIndicator}
<li className='col'>
<time className='post__time'>
<FormattedDate
diff --git a/webapp/components/rhs_header_post.jsx b/webapp/components/rhs_header_post.jsx
index 493040800..6e0d9276e 100644
--- a/webapp/components/rhs_header_post.jsx
+++ b/webapp/components/rhs_header_post.jsx
@@ -3,7 +3,7 @@
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from 'utils/constants.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index 849971864..02fc4fc59 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -9,7 +9,7 @@ import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -41,6 +41,7 @@ export default class RhsRootPost extends React.Component {
const user = this.props.user;
var isOwner = this.props.currentUser.id === post.user_id;
var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+ const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
var timestamp = UserStore.getProfile(post.user_id).update_at;
var channel = ChannelStore.get(post.channel_id);
@@ -94,7 +95,7 @@ export default class RhsRootPost extends React.Component {
);
}
- if (isOwner) {
+ if (isOwner && !isSystemMessage) {
dropdownContents.push(
<li
key='rhs-root-edit'
diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx
index 2398e5e69..ca5ed2dc8 100644
--- a/webapp/components/rhs_thread.jsx
+++ b/webapp/components/rhs_thread.jsx
@@ -17,6 +17,14 @@ import Scrollbars from 'react-custom-scrollbars';
import React from 'react';
+export function renderView(props) {
+ return (
+ <div
+ {...props}
+ className='scrollbar--view'
+ />);
+}
+
export function renderThumbHorizontal(props) {
return (
<div
@@ -211,6 +219,7 @@ export default class RhsThread extends React.Component {
autoHideDuration={500}
renderThumbHorizontal={renderThumbHorizontal}
renderThumbVertical={renderThumbVertical}
+ renderView={renderView}
>
<div className='post-right__scroll'>
<RootPost
diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx
index 6a5af75c6..c96499392 100644
--- a/webapp/components/root.jsx
+++ b/webapp/components/root.jsx
@@ -4,7 +4,7 @@
//import $ from 'jquery';
//import Client from 'utils/web_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import LocalizationStore from 'stores/localization_store.jsx';
import {IntlProvider} from 'react-intl';
diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx
index c5baf50ef..4daa94d8a 100644
--- a/webapp/components/search_results.jsx
+++ b/webapp/components/search_results.jsx
@@ -30,7 +30,8 @@ function getStateFromStores() {
return {
results,
- channels
+ channels,
+ searchTerm: SearchStore.getSearchTerm()
};
}
@@ -119,7 +120,7 @@ export default class SearchResults extends React.Component {
searchForm = <SearchBox/>;
}
var noResults = (!results || !results.order || !results.order.length);
- var searchTerm = SearchStore.getSearchTerm();
+ const searchTerm = this.state.searchTerm;
const profiles = this.state.profiles || {};
var ctls = null;
diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx
index 0c25e23bc..708b148d8 100644
--- a/webapp/components/search_results_item.jsx
+++ b/webapp/components/search_results_item.jsx
@@ -6,7 +6,7 @@ import UserProfile from './user_profile.jsx';
import UserStore from 'stores/user_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -67,6 +67,12 @@ export default class SearchResultsItem extends React.Component {
disableProfilePopover = true;
}
+ let botIndicator;
+
+ if (post.props && post.props.from_webhook) {
+ botIndicator = <li className='col col__name bot-indicator'>{Constants.BOT_NAME}</li>;
+ }
+
return (
<div className='search-item__container'>
<div className='date-separator'>
@@ -94,13 +100,14 @@ export default class SearchResultsItem extends React.Component {
</div>
<div>
<ul className='post__header'>
- <li className='col__name'><strong>
+ <li className='col col__name'><strong>
<UserProfile
user={user}
overwriteName={overrideUsername}
disablePopover={disableProfilePopover}
/>
</strong></li>
+ {botIndicator}
<li className='col'>
<time className='search-item-time'>
<FormattedDate
diff --git a/webapp/components/select_team/select_team.jsx b/webapp/components/select_team/select_team.jsx
index 45a708d8c..a04961d5b 100644
--- a/webapp/components/select_team/select_team.jsx
+++ b/webapp/components/select_team/select_team.jsx
@@ -7,7 +7,7 @@ import * as Utils from 'utils/utils.jsx';
import ErrorBar from 'components/error_bar.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {Link} from 'react-router';
diff --git a/webapp/components/settings_sidebar.jsx b/webapp/components/settings_sidebar.jsx
index d55eb5366..dc59409a0 100644
--- a/webapp/components/settings_sidebar.jsx
+++ b/webapp/components/settings_sidebar.jsx
@@ -23,7 +23,7 @@ export default class SettingsSidebar extends React.Component {
}
}
render() {
- let tabList = this.props.tabs.map(function makeTab(tab) {
+ let tabList = this.props.tabs.map((tab) => {
let key = `${tab.name}_li`;
let className = '';
if (this.props.activeTab === tab.name) {
@@ -44,7 +44,7 @@ export default class SettingsSidebar extends React.Component {
</a>
</li>
);
- }.bind(this));
+ });
return (
<div>
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index a4d85f4ff..be0fb205b 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -169,6 +169,9 @@ export default class Sidebar extends React.Component {
$('.sidebar--left .nav-pills__container').perfectScrollbar();
}
+ this.refs.container.scrollTop = 0;
+ $('.nav-pills__container').perfectScrollbar('update');
+
// close the LHS on mobile when you change channels
if (this.state.activeId !== prevState.activeId) {
$('.app__body .inner-wrap').removeClass('move--right');
@@ -640,13 +643,13 @@ export default class Sidebar extends React.Component {
placement='top'
overlay={createChannelTootlip}
>
- <a
- className='add-channel-btn'
- href='#'
- onClick={this.showNewChannelModal.bind(this, 'O')}
- >
- {'+'}
- </a>
+ <a
+ className='add-channel-btn'
+ href='#'
+ onClick={this.showNewChannelModal.bind(this, 'O')}
+ >
+ {'+'}
+ </a>
</OverlayTrigger>
</h4>
</li>
@@ -677,13 +680,13 @@ export default class Sidebar extends React.Component {
placement='top'
overlay={createGroupTootlip}
>
- <a
- className='add-channel-btn'
- href='#'
- onClick={this.showNewChannelModal.bind(this, 'P')}
- >
- {'+'}
- </a>
+ <a
+ className='add-channel-btn'
+ href='#'
+ onClick={this.showNewChannelModal.bind(this, 'P')}
+ >
+ {'+'}
+ </a>
</OverlayTrigger>
</h4>
</li>
diff --git a/webapp/components/sidebar_header.jsx b/webapp/components/sidebar_header.jsx
index 143a3458a..76d9cf214 100644
--- a/webapp/components/sidebar_header.jsx
+++ b/webapp/components/sidebar_header.jsx
@@ -89,7 +89,7 @@ export default class SidebarHeader extends React.Component {
overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDisplayName}</Tooltip>}
ref='descriptionOverlay'
>
- <div className='team__name'>{this.props.teamDisplayName}</div>
+ <div className='team__name'>{this.props.teamDisplayName}</div>
</OverlayTrigger>
</div>
</a>
diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx
index b36d84b79..b24b8e4fb 100644
--- a/webapp/components/sidebar_right_menu.jsx
+++ b/webapp/components/sidebar_right_menu.jsx
@@ -10,7 +10,7 @@ import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -174,8 +174,8 @@ export default class SidebarRightMenu extends React.Component {
<li>
<Link
target='_blank'
+ rel='noopener noreferrer'
to={global.window.mm_config.HelpLink}
- rel='noreferrer'
>
<i className='fa fa-question'></i>
<FormattedMessage
@@ -193,8 +193,8 @@ export default class SidebarRightMenu extends React.Component {
<li>
<Link
target='_blank'
+ rel='noopener noreferrer'
to={global.window.mm_config.ReportAProblemLink}
- rel='noreferrer'
>
<i className='fa fa-phone'></i>
<FormattedMessage
@@ -250,7 +250,7 @@ export default class SidebarRightMenu extends React.Component {
<i className='fa fa-exchange'></i>
<FormattedMessage
id='sidebar_right_menu.switch_team'
- defaultMessage='Switch Team'
+ defaultMessage='Team Selection'
/>
</Link>
</li>
diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx
index 5c06cefed..ad8b94722 100644
--- a/webapp/components/signup_user_complete.jsx
+++ b/webapp/components/signup_user_complete.jsx
@@ -3,7 +3,9 @@
import FormError from 'components/form_error.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+
+import * as GlobalActions from 'actions/global_actions.jsx';
+import {track} from 'actions/analytics_actions.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -12,11 +14,10 @@ import * as Utils from 'utils/utils.jsx';
import Client from 'utils/web_client.jsx';
import Constants from 'utils/constants.jsx';
-import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-import {browserHistory, Link} from 'react-router';
-
import React from 'react';
import ReactDOM from 'react-dom';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {browserHistory, Link} from 'react-router';
import logoImage from 'images/logo.png';
@@ -198,6 +199,33 @@ export default class SignupUserComplete extends React.Component {
);
}
+ handleUserCreated(user, data) {
+ track('signup', 'signup_user_02_complete');
+ Client.loginById(
+ data.id,
+ user.password,
+ '',
+ () => {
+ if (this.state.hash > 0) {
+ BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true}));
+ }
+
+ GlobalActions.emitInitialLoad(
+ () => {
+ browserHistory.push('/select_team');
+ }
+ );
+ },
+ (err) => {
+ if (err.id === 'api.user.login.not_verified.app_error') {
+ browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName));
+ } else {
+ this.setState({serverError: err.message});
+ }
+ }
+ );
+ }
+
handleSubmit(e) {
e.preventDefault();
@@ -260,7 +288,7 @@ export default class SignupUserComplete extends React.Component {
return;
}
- const providedPassword = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ const providedPassword = ReactDOM.findDOMNode(this.refs.password).value;
if (!providedPassword || providedPassword.length < Constants.MIN_PASSWORD_LENGTH) {
this.setState({
nameError: '',
@@ -296,32 +324,7 @@ export default class SignupUserComplete extends React.Component {
this.state.data,
this.state.hash,
this.state.inviteId,
- (data) => {
- Client.track('signup', 'signup_user_02_complete');
- Client.loginById(
- data.id,
- user.password,
- '',
- () => {
- if (this.state.hash > 0) {
- BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true}));
- }
-
- GlobalActions.emitInitialLoad(
- () => {
- browserHistory.push('/select_team');
- }
- );
- },
- (err) => {
- if (err.id === 'api.user.login.not_verified.app_error') {
- browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName));
- } else {
- this.setState({serverError: err.message});
- }
- }
- );
- },
+ this.handleUserCreated.bind(this, user),
(err) => {
this.setState({serverError: err.message});
}
@@ -371,6 +374,7 @@ export default class SignupUserComplete extends React.Component {
onChange={this.handleLdapIdChange}
placeholder={ldapIdPlaceholder}
spellCheck='false'
+ autoCapitalize='off'
/>
</div>
<div className={'form-group' + errorClass}>
@@ -402,7 +406,7 @@ export default class SignupUserComplete extends React.Component {
}
render() {
- Client.track('signup', 'signup_user_01_welcome');
+ track('signup', 'signup_user_01_welcome');
// If we have been used then just display a message
if (this.state.usedBefore) {
@@ -511,6 +515,7 @@ export default class SignupUserComplete extends React.Component {
maxLength='128'
autoFocus={true}
spellCheck='false'
+ autoCapitalize='off'
/>
{emailError}
{emailHelpText}
@@ -594,6 +599,7 @@ export default class SignupUserComplete extends React.Component {
placeholder=''
maxLength={Constants.MAX_USERNAME_LENGTH}
spellCheck='false'
+ autoCapitalize='off'
/>
{nameError}
{nameHelpText}
diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx
index 998c17340..86d349a1a 100644
--- a/webapp/components/suggestion/suggestion_box.jsx
+++ b/webapp/components/suggestion/suggestion_box.jsx
@@ -5,7 +5,7 @@ import $ from 'jquery';
import ReactDOM from 'react-dom';
import Constants from 'utils/constants.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import SuggestionStore from 'stores/suggestion_store.jsx';
import * as Utils from 'utils/utils.jsx';
diff --git a/webapp/components/suggestion/suggestion_list.jsx b/webapp/components/suggestion/suggestion_list.jsx
index ce950e4f4..91f7443cb 100644
--- a/webapp/components/suggestion/suggestion_list.jsx
+++ b/webapp/components/suggestion/suggestion_list.jsx
@@ -3,7 +3,7 @@
import $ from 'jquery';
import ReactDOM from 'react-dom';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import SuggestionStore from 'stores/suggestion_store.jsx';
import React from 'react';
@@ -62,6 +62,10 @@ export default class SuggestionList extends React.Component {
scrollToItem(term) {
const content = this.getContent();
+ if (!content) {
+ return;
+ }
+
const visibleContentHeight = content[0].clientHeight;
const actualContentHeight = content[0].scrollHeight;
diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx
index 1f783fe9f..70d52740e 100644
--- a/webapp/components/team_general_tab.jsx
+++ b/webapp/components/team_general_tab.jsx
@@ -9,7 +9,7 @@ import Client from 'utils/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import TeamStore from 'stores/team_store.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
const holders = defineMessages({
dirDisabled: {
@@ -378,9 +378,9 @@ class GeneralTab extends React.Component {
</div>
</div>
<div className='setting-list__hint'>
- <FormattedMessage
+ <FormattedHTMLMessage
id='general_tab.codeLongDesc'
- defaultMessage='The Invite Code is used as part of the URL in the team invitation link created by **Get Team Invite Link** in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.'
+ defaultMessage='The Invite Code is used as part of the URL in the team invitation link created by <strong>Get Team Invite Link</strong> in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.'
/>
</div>
</div>
@@ -473,7 +473,9 @@ class GeneralTab extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>
+ {'×'}
+ </span>
</button>
<h4
className='modal-title'
@@ -518,4 +520,4 @@ GeneralTab.propTypes = {
activeSection: React.PropTypes.string.isRequired
};
-export default injectIntl(GeneralTab); \ No newline at end of file
+export default injectIntl(GeneralTab);
diff --git a/webapp/components/team_import_tab.jsx b/webapp/components/team_import_tab.jsx
index 782273c5a..f724a789a 100644
--- a/webapp/components/team_import_tab.jsx
+++ b/webapp/components/team_import_tab.jsx
@@ -123,7 +123,8 @@ class TeamImportTab extends React.Component {
return (
<div>
<div className='modal-header'>
- <button type='button'
+ <button
+ type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
diff --git a/webapp/components/team_settings_modal.jsx b/webapp/components/team_settings_modal.jsx
index 657643367..fedf34ab5 100644
--- a/webapp/components/team_settings_modal.jsx
+++ b/webapp/components/team_settings_modal.jsx
@@ -92,7 +92,9 @@ class TeamSettingsModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>
+ {'×'}
+ </span>
</button>
<h4
className='modal-title'
@@ -133,4 +135,4 @@ TeamSettingsModal.propTypes = {
intl: intlShape.isRequired
};
-export default injectIntl(TeamSettingsModal); \ No newline at end of file
+export default injectIntl(TeamSettingsModal);
diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx
index 4aa88d267..7f5ecea09 100644
--- a/webapp/components/textbox.jsx
+++ b/webapp/components/textbox.jsx
@@ -211,6 +211,7 @@ export default class Textbox extends React.Component {
{previewLink}
<a
target='_blank'
+ rel='noopener noreferrer'
href='http://docs.mattermost.com/help/getting-started/messaging-basics.html'
className='textbox-help-link'
>
diff --git a/webapp/components/time_since.jsx b/webapp/components/time_since.jsx
index f715193e2..50a0f7d04 100644
--- a/webapp/components/time_since.jsx
+++ b/webapp/components/time_since.jsx
@@ -26,7 +26,7 @@ export default class TimeSince extends React.Component {
clearInterval(this.intervalId);
}
render() {
- if (this.props.sameUser) {
+ if (this.props.sameUser || this.props.compactDisplay) {
return (
<time className='post__time'>
{Utils.displayTimeFormatted(this.props.eventTime)}
@@ -69,5 +69,6 @@ TimeSince.defaultProps = {
TimeSince.propTypes = {
eventTime: React.PropTypes.number.isRequired,
- sameUser: React.PropTypes.bool
+ sameUser: React.PropTypes.bool,
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/toggle_modal_button.jsx b/webapp/components/toggle_modal_button.jsx
index 69bdbbda0..6082901de 100644
--- a/webapp/components/toggle_modal_button.jsx
+++ b/webapp/components/toggle_modal_button.jsx
@@ -25,7 +25,7 @@ export default class ModalToggleButton extends React.Component {
}
render() {
- const {children, dialogType, dialogProps, onClick, ...props} = this.props; // eslint-disable-line no-use-before-define
+ const {children, dialogType, dialogProps, onClick, ...props} = this.props;
// allow callers to provide an onClick which will be called before the modal is shown
let clickHandler = this.show;
@@ -38,7 +38,7 @@ export default class ModalToggleButton extends React.Component {
}
// this assumes that all modals will have a show property and an onHide event
- const dialog = React.createElement(this.props.dialogType, Object.assign({}, dialogProps, {
+ const dialog = React.createElement(dialogType, Object.assign({}, dialogProps, {
show: this.state.show,
onHide: () => {
this.hide();
diff --git a/webapp/components/tutorial/tutorial_intro_screens.jsx b/webapp/components/tutorial/tutorial_intro_screens.jsx
index 95c26edca..277ff967f 100644
--- a/webapp/components/tutorial/tutorial_intro_screens.jsx
+++ b/webapp/components/tutorial/tutorial_intro_screens.jsx
@@ -6,7 +6,7 @@ import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -149,6 +149,7 @@ export default class TutorialIntroScreens extends React.Component {
<a
href={'mailto:' + global.window.mm_config.SupportEmail}
target='_blank'
+ rel='noopener noreferrer'
>
{global.window.mm_config.SupportEmail}
</a>
diff --git a/webapp/components/user_settings/custom_theme_chooser.jsx b/webapp/components/user_settings/custom_theme_chooser.jsx
index 9fbdd1251..e77ea1d30 100644
--- a/webapp/components/user_settings/custom_theme_chooser.jsx
+++ b/webapp/components/user_settings/custom_theme_chooser.jsx
@@ -230,11 +230,11 @@ class CustomThemeChooser extends React.Component {
overlay={popoverContent}
ref='headerOverlay'
>
- <span className='input-group-addon'>
- <img
- src={codeThemeURL}
- />
- </span>
+ <span className='input-group-addon'>
+ <img
+ src={codeThemeURL}
+ />
+ </span>
</OverlayTrigger>
</div>
</div>
diff --git a/webapp/components/user_settings/import_theme_modal.jsx b/webapp/components/user_settings/import_theme_modal.jsx
index 32da296bf..f743feee6 100644
--- a/webapp/components/user_settings/import_theme_modal.jsx
+++ b/webapp/components/user_settings/import_theme_modal.jsx
@@ -81,7 +81,7 @@ class ImportThemeModal extends React.Component {
theme.mentionHighlightLink = '#2f81b7';
theme.codeTheme = 'github';
- let user = UserStore.getCurrentUser();
+ const user = UserStore.getCurrentUser();
user.theme_props = theme;
Client.updateUser(user,
diff --git a/webapp/components/user_settings/manage_languages.jsx b/webapp/components/user_settings/manage_languages.jsx
index bbf3a2e40..269181922 100644
--- a/webapp/components/user_settings/manage_languages.jsx
+++ b/webapp/components/user_settings/manage_languages.jsx
@@ -5,7 +5,7 @@ import SettingItemMax from '../setting_item_max.jsx';
import Client from 'utils/web_client.jsx';
import * as I18n from 'i18n/i18n.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
diff --git a/webapp/components/user_settings/premade_theme_chooser.jsx b/webapp/components/user_settings/premade_theme_chooser.jsx
index 4b0faf865..9552c686d 100644
--- a/webapp/components/user_settings/premade_theme_chooser.jsx
+++ b/webapp/components/user_settings/premade_theme_chooser.jsx
@@ -59,6 +59,7 @@ export default class PremadeThemeChooser extends React.Component {
<a
href='http://docs.mattermost.com/help/settings/theme-colors.html#custom-theme-examples'
target='_blank'
+ rel='noopener noreferrer'
>
<FormattedMessage
id='user.settings.display.theme.otherThemes'
diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx
index 61e0e1dad..dc5bd1c0e 100644
--- a/webapp/components/user_settings/user_settings_advanced.jsx
+++ b/webapp/components/user_settings/user_settings_advanced.jsx
@@ -173,6 +173,7 @@ class AdvancedSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='sendOnCtrlEnter'
checked={ctrlSendActive[0]}
onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'true')}
/>
@@ -187,6 +188,7 @@ class AdvancedSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='sendOnCtrlEnter'
checked={ctrlSendActive[1]}
onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'false')}
/>
diff --git a/webapp/components/user_settings/user_settings_developer.jsx b/webapp/components/user_settings/user_settings_developer.jsx
index cabb021cb..ae6d60362 100644
--- a/webapp/components/user_settings/user_settings_developer.jsx
+++ b/webapp/components/user_settings/user_settings_developer.jsx
@@ -3,7 +3,7 @@
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
@@ -135,4 +135,4 @@ DeveloperTab.propTypes = {
collapseModal: React.PropTypes.func.isRequired
};
-export default injectIntl(DeveloperTab); \ No newline at end of file
+export default injectIntl(DeveloperTab);
diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx
index c4af57d4c..16175d4de 100644
--- a/webapp/components/user_settings/user_settings_display.jsx
+++ b/webapp/components/user_settings/user_settings_display.jsx
@@ -23,7 +23,8 @@ function getDisplayStateFromStores() {
militaryTime: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', 'false'),
nameFormat: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'username'),
selectedFont: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT),
- channelDisplayMode: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT)
+ channelDisplayMode: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT),
+ messageDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT)
};
}
@@ -70,8 +71,14 @@ export default class UserSettingsDisplay extends React.Component {
name: Preferences.CHANNEL_DISPLAY_MODE,
value: this.state.channelDisplayMode
};
+ const messageDisplayPreference = {
+ user_id: userId,
+ category: Preferences.CATEGORY_DISPLAY_SETTINGS,
+ name: Preferences.MESSAGE_DISPLAY,
+ value: this.state.messageDisplay
+ };
- AsyncClient.savePreferences([timePreference, namePreference, fontPreference, channelDisplayModePreference],
+ AsyncClient.savePreferences([timePreference, namePreference, fontPreference, channelDisplayModePreference, messageDisplayPreference],
() => {
this.updateSection('');
},
@@ -89,6 +96,9 @@ export default class UserSettingsDisplay extends React.Component {
handleChannelDisplayModeRadio(channelDisplayMode) {
this.setState({channelDisplayMode});
}
+ handlemessageDisplayRadio(messageDisplay) {
+ this.setState({messageDisplay});
+ }
handleFont(selectedFont) {
Utils.applyFont(selectedFont);
this.setState({selectedFont});
@@ -115,6 +125,7 @@ export default class UserSettingsDisplay extends React.Component {
let channelDisplayModeSection;
let fontSection;
let languagesSection;
+ let messageDisplaySection;
if (this.props.activeSection === 'clock') {
const clockFormat = [false, false];
@@ -135,6 +146,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='clockFormat'
checked={clockFormat[0]}
onChange={this.handleClockRadio.bind(this, 'false')}
/>
@@ -149,6 +161,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='clockFormat'
checked={clockFormat[1]}
onChange={this.handleClockRadio.bind(this, 'true')}
/>
@@ -253,6 +266,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='nameFormat'
checked={nameFormat[1]}
onChange={this.handleNameRadio.bind(this, 'username')}
/>
@@ -264,6 +278,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='nameFormat'
checked={nameFormat[0]}
onChange={this.handleNameRadio.bind(this, 'nickname_full_name')}
/>
@@ -275,6 +290,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='nameFormat'
checked={nameFormat[2]}
onChange={this.handleNameRadio.bind(this, 'full_name')}
/>
@@ -350,6 +366,107 @@ export default class UserSettingsDisplay extends React.Component {
);
}
+ if (this.props.activeSection === Preferences.MESSAGE_DISPLAY) {
+ const messageDisplay = [false, false];
+ if (this.state.messageDisplay === Preferences.MESSAGE_DISPLAY_CLEAN) {
+ messageDisplay[0] = true;
+ } else {
+ messageDisplay[1] = true;
+ }
+
+ const inputs = [
+ <div key='userDisplayNameOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ name='messageDisplay'
+ checked={messageDisplay[0]}
+ onChange={this.handlemessageDisplayRadio.bind(this, Preferences.MESSAGE_DISPLAY_CLEAN)}
+ />
+ <FormattedMessage
+ id='user.settings.display.messageDisplayClean'
+ defaultMessage='Clean'
+ />
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ name='messageDisplay'
+ checked={messageDisplay[1]}
+ onChange={this.handlemessageDisplayRadio.bind(this, Preferences.MESSAGE_DISPLAY_COMPACT)}
+ />
+ <FormattedMessage
+ id='user.settings.display.messageDisplayCompact'
+ defaultMessage='Compact'
+ />
+ </label>
+ <br/>
+ </div>
+ <div>
+ <br/>
+ <FormattedMessage
+ id='user.settings.display.messageDisplayDescription'
+ defaultMessage='Select how messages in a channel should be displayed.'
+ />
+ </div>
+ </div>
+ ];
+
+ messageDisplaySection = (
+ <SettingItemMax
+ title={
+ <FormattedMessage
+ id='user.settings.display.messageDisplayTitle'
+ defaultMessage='Message Display'
+ />
+ }
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ let describe;
+ if (this.state.messageDisplay === Preferences.MESSAGE_DISPLAY_CLEAN) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.display.messageDisplayClean'
+ defaultMessage='Clean'
+ />
+ );
+ } else {
+ describe = (
+ <FormattedMessage
+ id='user.settings.display.messageDisplayCompact'
+ defaultMessage='Compact'
+ />
+ );
+ }
+
+ messageDisplaySection = (
+ <SettingItemMin
+ title={
+ <FormattedMessage
+ id='user.settings.display.messageDisplayTitle'
+ defaultMessage='Message Display'
+ />
+ }
+ describe={describe}
+ updateSection={() => {
+ this.props.updateSection(Preferences.MESSAGE_DISPLAY);
+ }}
+ />
+ );
+ }
+
if (this.props.activeSection === Preferences.CHANNEL_DISPLAY_MODE) {
const channelDisplayMode = [false, false];
if (this.state.channelDisplayMode === Preferences.CHANNEL_DISPLAY_MODE_CENTERED) {
@@ -364,6 +481,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='channelDisplayMode'
checked={channelDisplayMode[0]}
onChange={this.handleChannelDisplayModeRadio.bind(this, Preferences.CHANNEL_DISPLAY_MODE_CENTERED)}
/>
@@ -378,6 +496,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='channelDisplayMode'
checked={channelDisplayMode[1]}
onChange={this.handleChannelDisplayModeRadio.bind(this, Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN)}
/>
@@ -392,7 +511,7 @@ export default class UserSettingsDisplay extends React.Component {
<br/>
<FormattedMessage
id='user.settings.display.channeldisplaymode'
- defaultMessage='Select how text in a channel is displayed.'
+ defaultMessage='Select the width of the center channel.'
/>
</div>
</div>
@@ -601,6 +720,8 @@ export default class UserSettingsDisplay extends React.Component {
<div className='divider-dark'/>
{nameFormatSection}
<div className='divider-dark'/>
+ {messageDisplaySection}
+ <div className='divider-dark'/>
{channelDisplayModeSection}
<div className='divider-dark'/>
{languagesSection}
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index be1d1e6c5..6149b1630 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -160,6 +160,11 @@ class UserSettingsGeneralTab extends React.Component {
const email = this.state.email.trim().toLowerCase();
const confirmEmail = this.state.confirmEmail.trim().toLowerCase();
+ if (user.email === email) {
+ this.updateSection('');
+ return;
+ }
+
const {formatMessage} = this.props.intl;
if (email === '' || !Utils.isEmail(email)) {
this.setState({emailError: formatMessage(holders.validEmail), clientError: '', serverError: ''});
@@ -171,11 +176,6 @@ class UserSettingsGeneralTab extends React.Component {
return;
}
- if (user.email === email) {
- this.updateSection('');
- return;
- }
-
user.email = email;
this.submitUser(user, true);
}
@@ -342,7 +342,7 @@ class UserSettingsGeneralTab extends React.Component {
<div className='col-sm-7'>
<input
className='form-control'
- type='text'
+ type='email'
onChange={this.updateEmail}
value={this.state.email}
/>
@@ -363,7 +363,7 @@ class UserSettingsGeneralTab extends React.Component {
<div className='col-sm-7'>
<input
className='form-control'
- type='text'
+ type='email'
onChange={this.updateConfirmEmail}
value={this.state.confirmEmail}
/>
@@ -681,6 +681,7 @@ class UserSettingsGeneralTab extends React.Component {
type='text'
onChange={this.updateNickname}
value={this.state.nickname}
+ autoCapitalize='off'
/>
</div>
</div>
@@ -764,6 +765,7 @@ class UserSettingsGeneralTab extends React.Component {
type='text'
onChange={this.updateUsername}
value={this.state.username}
+ autoCapitalize='off'
/>
</div>
</div>
diff --git a/webapp/components/user_settings/user_settings_notifications.jsx b/webapp/components/user_settings/user_settings_notifications.jsx
index fa84ce2d6..410ce1a4e 100644
--- a/webapp/components/user_settings/user_settings_notifications.jsx
+++ b/webapp/components/user_settings/user_settings_notifications.jsx
@@ -30,6 +30,10 @@ function getNotificationsStateFromStores() {
if (user.notify_props && user.notify_props.email) {
email = user.notify_props.email;
}
+ var push = 'mention';
+ if (user.notify_props && user.notify_props.push) {
+ push = user.notify_props.push;
+ }
var usernameKey = false;
var mentionKey = false;
@@ -72,9 +76,20 @@ function getNotificationsStateFromStores() {
}
}
- return {notifyLevel: desktop, enableEmail: email, soundNeeded: soundNeeded, enableSound: sound,
- usernameKey: usernameKey, mentionKey: mentionKey, customKeys: customKeys, customKeysChecked: customKeys.length > 0,
- firstNameKey: firstNameKey, allKey: allKey, channelKey: channelKey};
+ return {
+ notifyLevel: desktop,
+ notifyPushLevel: push,
+ enableEmail: email,
+ soundNeeded,
+ enableSound: sound,
+ usernameKey,
+ mentionKey,
+ customKeys,
+ customKeysChecked: customKeys.length > 0,
+ firstNameKey,
+ allKey,
+ channelKey
+ };
}
const holders = defineMessages({
@@ -121,6 +136,7 @@ class NotificationsTab extends React.Component {
this.updateChannelKey = this.updateChannelKey.bind(this);
this.updateCustomMentionKeys = this.updateCustomMentionKeys.bind(this);
this.onCustomChange = this.onCustomChange.bind(this);
+ this.createPushNotificationSection = this.createPushNotificationSection.bind(this);
this.state = getNotificationsStateFromStores();
}
@@ -130,6 +146,7 @@ class NotificationsTab extends React.Component {
data.email = this.state.enableEmail;
data.desktop_sound = this.state.enableSound;
data.desktop = this.state.notifyLevel;
+ data.push = this.state.notifyPushLevel;
var mentionKeys = [];
if (this.state.usernameKey) {
@@ -150,13 +167,13 @@ class NotificationsTab extends React.Component {
data.channel = this.state.channelKey.toString();
Client.updateUserNotifyProps(data,
- function success() {
+ () => {
this.props.updateSection('');
AsyncClient.getMe();
- }.bind(this),
- function failure(err) {
+ },
+ (err) => {
this.setState({serverError: err.message});
- }.bind(this)
+ }
);
}
handleCancel(e) {
@@ -185,15 +202,21 @@ class NotificationsTab extends React.Component {
this.updateState();
}
handleNotifyRadio(notifyLevel) {
- this.setState({notifyLevel: notifyLevel});
+ this.setState({notifyLevel});
+ ReactDOM.findDOMNode(this.refs.wrapper).focus();
+ }
+
+ handlePushRadio(notifyPushLevel) {
+ this.setState({notifyPushLevel});
ReactDOM.findDOMNode(this.refs.wrapper).focus();
}
+
handleEmailRadio(enableEmail) {
- this.setState({enableEmail: enableEmail});
+ this.setState({enableEmail});
ReactDOM.findDOMNode(this.refs.wrapper).focus();
}
handleSoundRadio(enableSound) {
- this.setState({enableSound: enableSound});
+ this.setState({enableSound});
ReactDOM.findDOMNode(this.refs.wrapper).focus();
}
updateUsernameKey(val) {
@@ -227,12 +250,129 @@ class NotificationsTab extends React.Component {
ReactDOM.findDOMNode(this.refs.customcheck).checked = true;
this.updateCustomMentionKeys();
}
+ createPushNotificationSection() {
+ var handleUpdateDesktopSection;
+ if (this.props.activeSection === 'push') {
+ var notifyActive = [false, false, false];
+ if (this.state.notifyPushLevel === 'all') {
+ notifyActive[0] = true;
+ } else if (this.state.notifyPushLevel === 'none') {
+ notifyActive[2] = true;
+ } else {
+ notifyActive[1] = true;
+ }
+
+ let inputs = [];
+
+ inputs.push(
+ <div key='userNotificationLevelOption'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ name='pushNotificationLevel'
+ checked={notifyActive[0]}
+ onChange={this.handlePushRadio.bind(this, 'all')}
+ />
+ <FormattedMessage
+ id='user.settings.push_notification.allActivity'
+ defaultMessage='For all activity'
+ />
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ name='pushNotificationLevel'
+ checked={notifyActive[1]}
+ onChange={this.handlePushRadio.bind(this, 'mention')}
+ />
+ <FormattedMessage
+ id='user.settings.push_notifications.onlyMentions'
+ defaultMessage='For mentions and direct messages'
+ />
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ name='pushNotificationLevel'
+ checked={notifyActive[2]}
+ onChange={this.handlePushRadio.bind(this, 'none')}
+ />
+ <FormattedMessage
+ id='user.settings.push_notifications.off'
+ defaultMessage='Off'
+ />
+ </label>
+ </div>
+ </div>
+ );
+
+ const extraInfo = (
+ <span>
+ <FormattedMessage
+ id='user.settings.push_notifications.info'
+ defaultMessage='Notification alerts are pushed to your mobile device when there is activity in Mattermost.'
+ />
+ </span>
+ );
+
+ return (
+ <SettingItemMax
+ title={Utils.localizeMessage('user.settings.notifications.push', 'Mobile push notifications')}
+ extraInfo={extraInfo}
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={this.state.serverError}
+ updateSection={this.handleCancel}
+ />
+ );
+ }
+
+ let describe = '';
+ if (this.state.notifyPushLevel === 'all') {
+ describe = (
+ <FormattedMessage
+ id='user.settings.push_notification.allActivity'
+ defaultMessage='For all activity'
+ />
+ );
+ } else if (this.state.notifyPushLevel === 'none') {
+ describe = (
+ <FormattedMessage
+ id='user.settings.push_notifications.off'
+ defaultMessage='Off'
+ />
+ );
+ } else {
+ describe = (
+ <FormattedMessage
+ id='user.settings.push_notifications.onlyMentions'
+ defaultMessage='For mentions and direct messages'
+ />
+ );
+ }
+
+ handleUpdateDesktopSection = function updateDesktopSection() {
+ this.props.updateSection('push');
+ }.bind(this);
+
+ return (
+ <SettingItemMin
+ title={Utils.localizeMessage('user.settings.notifications.push', 'Mobile push notifications')}
+ describe={describe}
+ updateSection={handleUpdateDesktopSection}
+ />
+ );
+ }
render() {
const {formatMessage} = this.props.intl;
- var serverError = null;
- if (this.state.serverError) {
- serverError = this.state.serverError;
- }
+ const serverError = this.state.serverError;
var user = this.props.user;
@@ -254,7 +394,9 @@ class NotificationsTab extends React.Component {
<div key='userNotificationLevelOption'>
<div className='radio'>
<label>
- <input type='radio'
+ <input
+ type='radio'
+ name='desktopNotificationLevel'
checked={notifyActive[0]}
onChange={this.handleNotifyRadio.bind(this, 'all')}
/>
@@ -269,6 +411,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='desktopNotificationLevel'
checked={notifyActive[1]}
onChange={this.handleNotifyRadio.bind(this, 'mention')}
/>
@@ -283,6 +426,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='desktopNotificationLevel'
checked={notifyActive[2]}
onChange={this.handleNotifyRadio.bind(this, 'none')}
/>
@@ -370,6 +514,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='notificationSounds'
checked={soundActive[0]}
onChange={this.handleSoundRadio.bind(this, 'true')}
/>
@@ -384,6 +529,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='notificationSounds'
checked={soundActive[1]}
onChange={this.handleSoundRadio.bind(this, 'false')}
/>
@@ -393,8 +539,8 @@ class NotificationsTab extends React.Component {
/>
</label>
<br/>
- </div>
- </div>
+ </div>
+ </div>
);
const extraInfo = (
@@ -473,6 +619,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='emailNotifications'
checked={emailActive[0]}
onChange={this.handleEmailRadio.bind(this, 'true')}
/>
@@ -487,6 +634,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='emailNotifications'
checked={emailActive[1]}
onChange={this.handleEmailRadio.bind(this, 'false')}
/>
@@ -763,6 +911,11 @@ class NotificationsTab extends React.Component {
);
}
+ let pushNotificationSection;
+ if (global.window.mm_config.SendPushNotifications === 'true') {
+ pushNotificationSection = this.createPushNotificationSection();
+ }
+
return (
<div>
<div className='modal-header'>
@@ -808,6 +961,8 @@ class NotificationsTab extends React.Component {
<div className='divider-light'/>
{emailSection}
<div className='divider-light'/>
+ {pushNotificationSection}
+ <div className='divider-light'/>
{keysSection}
<div className='divider-dark'/>
</div>
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index 700aa295a..47a762442 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -61,6 +61,7 @@ class SecurityTab extends React.Component {
this.state = this.getDefaultState();
}
+
getDefaultState() {
return {
currentPassword: '',
@@ -71,6 +72,7 @@ class SecurityTab extends React.Component {
mfaToken: ''
};
}
+
submitPassword(e) {
e.preventDefault();
@@ -117,6 +119,7 @@ class SecurityTab extends React.Component {
}
);
}
+
activateMfa() {
Client.updateMfa(
this.state.mfaToken,
@@ -138,6 +141,7 @@ class SecurityTab extends React.Component {
}
);
}
+
deactivateMfa() {
Client.updateMfa(
'',
@@ -159,22 +163,28 @@ class SecurityTab extends React.Component {
}
);
}
+
updateCurrentPassword(e) {
this.setState({currentPassword: e.target.value});
}
+
updateNewPassword(e) {
this.setState({newPassword: e.target.value});
}
+
updateConfirmPassword(e) {
this.setState({confirmPassword: e.target.value});
}
+
updateMfaToken(e) {
this.setState({mfaToken: e.target.value});
}
+
showQrCode(e) {
e.preventDefault();
this.setState({mfaShowQr: true});
}
+
createMfaSection() {
let updateSectionStatus;
let submit;
@@ -329,6 +339,7 @@ class SecurityTab extends React.Component {
/>
);
}
+
createPasswordSection() {
let updateSectionStatus;
@@ -519,6 +530,7 @@ class SecurityTab extends React.Component {
/>
);
}
+
createSignInSection() {
let updateSectionStatus;
const user = this.props.user;
@@ -608,11 +620,11 @@ class SecurityTab extends React.Component {
const inputs = [];
inputs.push(
<div key='userSignInOption'>
- {emailOption}
- {gitlabOption}
- <br/>
- {ldapOption}
- {googleOption}
+ {emailOption}
+ {gitlabOption}
+ <br/>
+ {ldapOption}
+ {googleOption}
</div>
);
@@ -676,7 +688,10 @@ class SecurityTab extends React.Component {
/>
);
}
+
render() {
+ const user = this.props.user;
+
const passwordSection = this.createPasswordSection();
let numMethods = 0;
@@ -690,7 +705,9 @@ class SecurityTab extends React.Component {
}
let mfaSection;
- if (global.window.mm_config.EnableMultifactorAuthentication === 'true' && global.window.mm_license.IsLicensed === 'true') {
+ if (global.window.mm_config.EnableMultifactorAuthentication === 'true' &&
+ global.window.mm_license.IsLicensed === 'true' &&
+ (user.auth_service === '' || user.auth_service === Constants.LDAP_SERVICE)) {
mfaSection = this.createMfaSection();
}
diff --git a/webapp/components/user_settings/user_settings_theme.jsx b/webapp/components/user_settings/user_settings_theme.jsx
index f19538f71..811f4d8e4 100644
--- a/webapp/components/user_settings/user_settings_theme.jsx
+++ b/webapp/components/user_settings/user_settings_theme.jsx
@@ -212,7 +212,9 @@ export default class ThemeSetting extends React.Component {
key='premadeThemeColorLabel'
>
<label>
- <input type='radio'
+ <input
+ type='radio'
+ name='theme'
checked={!displayCustom}
onChange={this.updateType.bind(this, 'premade')}
/>
@@ -233,7 +235,9 @@ export default class ThemeSetting extends React.Component {
key='customThemeColorLabel'
>
<label>
- <input type='radio'
+ <input
+ type='radio'
+ name='theme'
checked={displayCustom}
onChange={this.updateType.bind(this, 'custom')}
/>
diff --git a/webapp/components/view_image.jsx b/webapp/components/view_image.jsx
index c4d7cb4aa..7b827ac0e 100644
--- a/webapp/components/view_image.jsx
+++ b/webapp/components/view_image.jsx
@@ -3,7 +3,7 @@
import $ from 'jquery';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import AudioVideoPreview from './audio_video_preview.jsx';
import Constants from 'utils/constants.jsx';
@@ -404,6 +404,7 @@ function ImagePreview({filename, fileUrl, fileInfo, maxHeight}) {
<a
href={fileUrl}
target='_blank'
+ rel='noopener noreferrer'
download={true}
>
<img
diff --git a/webapp/components/view_image_popover_bar.jsx b/webapp/components/view_image_popover_bar.jsx
index 5b9b2362f..3554ae3f8 100644
--- a/webapp/components/view_image_popover_bar.jsx
+++ b/webapp/components/view_image_popover_bar.jsx
@@ -54,6 +54,7 @@ export default class ViewImagePopoverBar extends React.Component {
download={this.props.filename}
className='text'
target='_blank'
+ rel='noopener noreferrer'
>
<FormattedMessage
id='view_image_popover.download'
diff --git a/webapp/components/youtube_video.jsx b/webapp/components/youtube_video.jsx
index 6083fd8a1..dc2d368d7 100644
--- a/webapp/components/youtube_video.jsx
+++ b/webapp/components/youtube_video.jsx
@@ -1,13 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
import ChannelStore from 'stores/channel_store.jsx';
+import WebClient from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
-const ytRegex = /(?:http|https):\/\/(?:www\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(\/u\/\w\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^\/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#\&\?]*)/;
+const ytRegex = /(?:http|https):\/\/(?:www\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(\/u\/\w\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^\/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&\?]*)/;
import React from 'react';
-import {Link} from 'react-router';
export default class YoutubeVideo extends React.Component {
constructor(props) {
@@ -15,12 +15,16 @@ export default class YoutubeVideo extends React.Component {
this.updateStateFromProps = this.updateStateFromProps.bind(this);
this.handleReceivedMetadata = this.handleReceivedMetadata.bind(this);
+ this.handleMetadataError = this.handleMetadataError.bind(this);
+ this.loadWithoutKey = this.loadWithoutKey.bind(this);
this.play = this.play.bind(this);
this.stop = this.stop.bind(this);
this.stopOnChannelChange = this.stopOnChannelChange.bind(this);
this.state = {
+ loaded: false,
+ failed: false,
playing: false,
title: ''
};
@@ -78,23 +82,39 @@ export default class YoutubeVideo extends React.Component {
}
componentDidMount() {
- if (global.window.mm_config.GoogleDeveloperKey) {
- $.ajax({
- async: true,
- url: 'https://www.googleapis.com/youtube/v3/videos',
- type: 'GET',
- data: {part: 'snippet', id: this.state.videoId, key: global.window.mm_config.GoogleDeveloperKey},
- success: this.handleReceivedMetadata
- });
+ const key = global.window.mm_config.GoogleDeveloperKey;
+ if (key) {
+ WebClient.getYoutubeVideoInfo(key, this.state.videoId,
+ this.handleReceivedMetadata, this.handleMetadataError);
+ } else {
+ this.loadWithoutKey();
}
}
+ loadWithoutKey() {
+ this.setState({loaded: true});
+ }
+
+ handleMetadataError() {
+ this.setState({
+ failed: true,
+ loaded: true,
+ title: Utils.localizeMessage('youtube_video.notFound', 'Video not found')
+ });
+ }
+
handleReceivedMetadata(data) {
- if (!data.items.length || !data.items[0].snippet) {
+ if (!data || !data.items || !data.items.length || !data.items[0].snippet) {
+ this.setState({
+ failed: true,
+ loaded: true,
+ title: Utils.localizeMessage('youtube_video.notFound', 'Video not found')
+ });
return null;
}
var metadata = data.items[0].snippet;
this.setState({
+ loaded: true,
receivedYoutubeData: true,
title: metadata.title
});
@@ -120,13 +140,28 @@ export default class YoutubeVideo extends React.Component {
}
render() {
+ if (!this.state.loaded) {
+ return <div className='video-loading'/>;
+ }
+
let header = 'Youtube';
if (this.state.title) {
header = header + ' - ';
}
let content;
- if (this.state.playing) {
+ if (this.state.failed) {
+ content = (
+ <div>
+ <div className='video-thumbnail__container'>
+ <div className='video-thumbnail__error'>
+ <div><i className='fa fa-warning fa-2x'/></div>
+ <div>{Utils.localizeMessage('youtube_video.notFound', 'Video not found')}</div>
+ </div>
+ </div>
+ </div>
+ );
+ } else if (this.state.playing) {
content = (
<iframe
src={'https://www.youtube.com/embed/' + this.state.videoId + '?autoplay=1&autohide=1&border=0&wmode=opaque&fs=1&enablejsapi=1' + this.state.time}
@@ -157,7 +192,15 @@ export default class YoutubeVideo extends React.Component {
<div>
<h4>
<span className='video-type'>{header}</span>
- <span className='video-title'><Link to={this.props.link}>{this.state.title}</Link></span>
+ <span className='video-title'>
+ <a
+ href={this.props.link}
+ target='blank'
+ rel='noopener noreferrer'
+ >
+ {this.state.title}
+ </a>
+ </span>
</h4>
<div
className='video-div embed-responsive-item'
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 7e46903db..609de5a75 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -6,6 +6,7 @@
"about.enterpriseEditionSt": "Modern enterprise communication from behind your firewall.",
"about.enterpriseEditione1": "Enterprise Edition",
"about.hash": "Build Hash:",
+ "about.hashee": "EE Build Hash:",
"about.licensed": "Licensed by:",
"about.number": "Build Number:",
"about.teamEditionLearn": "Join the Mattermost community at ",
@@ -46,9 +47,12 @@
"add_command.method.help": "The type of command request issued to the Request URL.",
"add_command.method.post": "POST",
"add_command.trigger": "Command Trigger Word",
- "add_command.trigger.help1": "Examples: /patient, /client, /employee",
- "add_command.trigger.help2": "Reserved: /echo, /join, /logout, /me, /shrug",
- "add_command.trigger.placeholder": "Command trigger e.g. \"hello\" not including the slash",
+ "add_command.trigger.help1": "Examples: patient, client, employee",
+ "add_command.trigger.help2": "Reserved: echo, join, logout, me, shrug",
+ "add_command.trigger.placeholder": "Command trigger e.g. \"hello\"",
+ "add_command.triggerInvalidLength": "A trigger word must contain between {min} and {max} characters",
+ "add_command.triggerInvalidSlash": "A trigger word cannot begin with a /",
+ "add_command.triggerInvalidSpace": "A trigger word must not contain spaces",
"add_command.triggerRequired": "A trigger word is required",
"add_command.url": "Request URL",
"add_command.url.help": "The callback URL to receive the HTTP POST or GET event request when the slash command is run.",
@@ -85,7 +89,7 @@
"admin.compliance.enableDesc": "When true, Mattermost allows compliance reporting",
"admin.compliance.enableTitle": "Enable Compliance:",
"admin.compliance.false": "false",
- "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Note:</h4><p>Compliance is an enterprise feature. Your current license does not support Compliance. Click <a href=\"http://mattermost.com\"target=\"_blank\">here</a> for information and pricing on enterprise licenses.</p>",
+ "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Note:</h4><p>Compliance is an enterprise feature. Your current license does not support Compliance. Click <a href=\"http://mattermost.com\" target=\"_blank\">here</a> for information and pricing on enterprise licenses.</p>",
"admin.compliance.save": "Save",
"admin.compliance.saving": "Saving Config...",
"admin.compliance.title": "Compliance Settings",
@@ -128,9 +132,7 @@
"admin.email.allowUsernameSignInTitle": "Allow Sign In With Username: ",
"admin.email.easHelp": "Learn more about compiling and deploying your own mobile apps from an <a href=\"http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas\" target=\"_blank\">Enterprise App Store</a>.",
"admin.email.emailFail": "Connection unsuccessful: {error}",
- "admin.email.emailSettings": "Email Settings",
"admin.email.emailSuccess": "No errors were reported while sending an email. Please check your inbox to make sure.",
- "admin.email.false": "false",
"admin.email.fullPushNotification": "Send full message snippet",
"admin.email.genericPushNotification": "Send generic description with user and channel names",
"admin.email.inviteSaltDescription": "32-character salt added to signing of email invites. Randomly generated on install. Click \"Re-Generate\" to create new salt.",
@@ -160,11 +162,8 @@
"admin.email.pushServerEx": "E.g.: \"http://push-test.mattermost.com\"",
"admin.email.pushServerTitle": "Push Notification Server:",
"admin.email.pushTitle": "Send Push Notifications: ",
- "admin.email.regenerate": "Re-Generate",
"admin.email.requireVerificationDescription": "Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.",
"admin.email.requireVerificationTitle": "Require Email Verification: ",
- "admin.email.save": "Save",
- "admin.email.saving": "Saving Config...",
"admin.email.selfPush": "Manually enter Push Notification Service location",
"admin.email.smtpPasswordDescription": " Obtain this credential from administrator setting up your email server.",
"admin.email.smtpPasswordExample": "Ex: \"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
@@ -179,7 +178,7 @@
"admin.email.smtpUsernameExample": "Ex: \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
"admin.email.smtpUsernameTitle": "SMTP Username:",
"admin.email.testing": "Testing...",
- "admin.email.true": "true",
+ "admin.false": "false",
"admin.gitab.clientSecretDescription": "Obtain this value via the instructions above for logging into GitLab.",
"admin.gitlab.EnableHtmlDesc": "<ol><li>Log in to your GitLab account and go to Profile Settings -> Applications.</li><li>Enter Redirect URIs \"<your-mattermost-url>/login/gitlab/complete\" (example: http://localhost:8065/login/gitlab/complete) and \"<your-mattermost-url>/signup/gitlab/complete\". </li><li>Then use \"Secret\" and \"Id\" fields from GitLab to complete the options below.</li><li>Complete the Endpoint URLs below. </li></ol>",
"admin.gitlab.authDescription": "Enter https://<your-gitlab-url>/oauth/authorize (example https://example.com:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URL depending on your server configuration.",
@@ -192,14 +191,10 @@
"admin.gitlab.clientSecretTitle": "Secret:",
"admin.gitlab.enableDescription": "When true, Mattermost allows team creation and account signup using GitLab OAuth.",
"admin.gitlab.enableTitle": "Enable Sign Up With GitLab: ",
- "admin.gitlab.false": "false",
- "admin.gitlab.save": "Save",
- "admin.gitlab.saving": "Saving Config...",
"admin.gitlab.settingsTitle": "GitLab Settings",
"admin.gitlab.tokenDescription": "Enter https://<your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.",
"admin.gitlab.tokenExample": "Ex \"\"",
"admin.gitlab.tokenTitle": "Token Endpoint:",
- "admin.gitlab.true": "true",
"admin.gitlab.userDescription": "Enter https://<your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.",
"admin.gitlab.userExample": "Ex \"\"",
"admin.gitlab.userTitle": "User API Endpoint:",
@@ -215,8 +210,6 @@
"admin.image.amazonS3SecretDescription": "Obtain this credential from your Amazon EC2 administrator.",
"admin.image.amazonS3SecretExample": "Ex \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.image.amazonS3SecretTitle": "Amazon S3 Secret Access Key:",
- "admin.image.false": "false",
- "admin.image.fileSettings": "File Settings",
"admin.image.localDescription": "Directory to which image files are written. If blank, will be set to ./data/.",
"admin.image.localExample": "Ex \"./data/\"",
"admin.image.localTitle": "Local Directory Location:",
@@ -235,9 +228,6 @@
"admin.image.publicLinkDescription": "32-character salt added to signing of public image links. Randomly generated on install. Click \"Re-Generate\" to create new salt.",
"admin.image.publicLinkExample": "Ex \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
"admin.image.publicLinkTitle": "Public Link Salt:",
- "admin.image.regenerate": "Re-Generate",
- "admin.image.save": "Save",
- "admin.image.saving": "Saving Config...",
"admin.image.shareDescription": "Allow users to share public links to files and images.",
"admin.image.shareTitle": "Share Public File Link: ",
"admin.image.storeAmazonS3": "Amazon S3",
@@ -249,7 +239,6 @@
"admin.image.thumbWidthDescription": "Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.",
"admin.image.thumbWidthExample": "Ex \"120\"",
"admin.image.thumbWidthTitle": "Thumbnail Width:",
- "admin.image.true": "true",
"admin.ldap.bannerDesc": "If a user attribute changes on the LDAP server it will be updated the next time the user enters their credentials to log in to Mattermost. This includes if a user is made inactive or removed from an LDAP server. Synchronization with LDAP servers is planned in a future release.",
"admin.ldap.bannerHeading": "Note:",
"admin.ldap.baseDesc": "The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.",
@@ -264,7 +253,6 @@
"admin.ldap.emailAttrTitle": "Email Attribute:",
"admin.ldap.enableDesc": "When true, Mattermost allows login using LDAP",
"admin.ldap.enableTitle": "Enable Login With LDAP:",
- "admin.ldap.false": "false",
"admin.ldap.firstnameAttrDesc": "The attribute in the LDAP server that will be used to populate the first name of users in Mattermost.",
"admin.ldap.firstnameAttrEx": "Ex \"givenName\"",
"admin.ldap.firstnameAttrTitle": "First Name Attrubute",
@@ -280,22 +268,18 @@
"admin.ldap.nicknameAttrDesc": "(Optional) The attribute in the LDAP server that will be used to populate the nickname of users in Mattermost.",
"admin.ldap.nicknameAttrEx": "Ex \"nickname\"",
"admin.ldap.nicknameAttrTitle": "Nickname Attribute:",
- "admin.ldap.noLicense": "<h4 class=\"banner__heading\">Note:</h4><p>LDAP is an enterprise feature. Your current license does not support LDAP. Click <a href=\"http://mattermost.com\"target=\"_blank\">here</a> for information and pricing on enterprise licenses.</p>",
+ "admin.ldap.noLicense": "<h4 class=\"banner__heading\">Note:</h4><p>LDAP is an enterprise feature. Your current license does not support LDAP. Click <a href=\"http://mattermost.com\" target=\"_blank\">here</a> for information and pricing on enterprise licenses.</p>",
"admin.ldap.portDesc": "The port Mattermost will use to connect to the LDAP server. Default is 389.",
"admin.ldap.portEx": "Ex \"389\"",
"admin.ldap.portTitle": "LDAP Port:",
"admin.ldap.queryDesc": "The timeout value for queries to the LDAP server. Increase if you are getting timeout errors caused by a slow LDAP server.",
"admin.ldap.queryEx": "Ex \"60\"",
"admin.ldap.queryTitle": "Query Timeout (seconds):",
- "admin.ldap.save": "Save",
- "admin.ldap.saving": "Saving Config...",
"admin.ldap.serverDesc": "The domain or IP address of LDAP server.",
"admin.ldap.serverEx": "Ex \"10.0.0.23\"",
"admin.ldap.serverTitle": "LDAP Server:",
"admin.ldap.skipCertificateVerification": "Skip Certificate Verification",
"admin.ldap.skipCertificateVerificationDesc": "Skips the certificate verification step for TLS or STARTTLS connections. Not recommended for production environments where TLS is required. For testing only.",
- "admin.ldap.title": "LDAP Settings",
- "admin.ldap.true": "true",
"admin.ldap.uernameAttrDesc": "The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.",
"admin.ldap.userFilterDisc": "Optionally enter an LDAP Filter to use when searching for user objects. Only the users selected by the query will be able to access Mattermost. For Active Directory, the query to filter out disabled users is (&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).",
"admin.ldap.userFilterEx": "Ex. \"(objectClass=user)\"",
@@ -316,7 +300,6 @@
"admin.license.uploading": "Uploading License...",
"admin.log.consoleDescription": "Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true, server writes messages to the standard output stream (stdout).",
"admin.log.consoleTitle": "Log To The Console: ",
- "admin.log.false": "false",
"admin.log.fileDescription": "Typically set to true in production. When true, log files are written to the log file specified in file location field below.",
"admin.log.fileLevelDescription": "This setting determines the level of detail at which log events are written to the log file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.",
"admin.log.fileLevelTitle": "File Log Level:",
@@ -336,27 +319,18 @@
"admin.log.locationPlaceholder": "Enter your file location",
"admin.log.locationTitle": "File Location:",
"admin.log.logSettings": "Log Settings",
- "admin.log.save": "Save",
- "admin.log.saving": "Saving Config...",
- "admin.log.true": "true",
"admin.logs.reload": "Reload",
"admin.logs.title": "Server Logs",
"admin.nav.help": "Help",
"admin.nav.logout": "Logout",
"admin.nav.report": "Report a Problem",
- "admin.nav.switch": "Switch to {display_name}",
- "admin.privacy.false": "false",
- "admin.privacy.save": "Save",
- "admin.privacy.saving": "Saving Config...",
+ "admin.nav.switch": "Team Selection",
"admin.privacy.showEmailDescription": "When false, hides email address of users from other users in the user interface, including team owners and team administrators. Used when system is set up for managing teams where some users choose to keep their contact information private.",
"admin.privacy.showEmailTitle": "Show Email Address: ",
"admin.privacy.showFullNameDescription": "When false, hides full name of users from other users, including team owners and team administrators. Username is shown in place of full name.",
"admin.privacy.showFullNameTitle": "Show Full Name: ",
- "admin.privacy.title": "Privacy Settings",
- "admin.privacy.true": "true",
"admin.rate.enableLimiterDescription": "When true, APIs are throttled at rates specified below.",
"admin.rate.enableLimiterTitle": "Enable Rate Limiter: ",
- "admin.rate.false": "false",
"admin.rate.httpHeaderDescription": "When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring NGINX set to \"X-Real-IP\", when configuring AmazonELB set to \"X-Forwarded-For\").",
"admin.rate.httpHeaderExample": "Ex \"X-Real-IP\", \"X-Forwarded-For\"",
"admin.rate.httpHeaderTitle": "Vary By HTTP Header:",
@@ -370,10 +344,13 @@
"admin.rate.queriesTitle": "Number Of Queries Per Second:",
"admin.rate.remoteDescription": "When true, rate limit API access by IP address.",
"admin.rate.remoteTitle": "Vary By Remote Address: ",
- "admin.rate.save": "Save",
- "admin.rate.saving": "Saving Config...",
- "admin.rate.title": "Rate Limit Settings",
- "admin.rate.true": "true",
+ "admin.recycle.button": "Recycle Database Connections",
+ "admin.recycle.loading": " Recycling...",
+ "admin.recycle.reloadFail": "Recycling unsuccessful: {error}",
+ "admin.regenerate": "Re-Generate",
+ "admin.reload.button": "Reload Configuration From Disk",
+ "admin.reload.loading": " Loading...",
+ "admin.reload.reloadFail": "Reloading unsuccessful: {error}",
"admin.reset_password.close": "Close",
"admin.reset_password.newPassword": "New Password",
"admin.reset_password.select": "Select",
@@ -393,7 +370,6 @@
"admin.service.corsTitle": "Allow Cross-origin Requests from:",
"admin.service.developerDesc": "(Developer Option) When true, extra information around errors will be displayed in the UI.",
"admin.service.developerTitle": "Enable Developer Mode: ",
- "admin.service.false": "false",
"admin.service.googleDescription": "Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Leaving the field blank disables the automatic generation of YouTube video previews from links.",
"admin.service.googleExample": "Ex \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"",
"admin.service.googleTitle": "Google Developer Key:",
@@ -414,8 +390,6 @@
"admin.service.outWebhooksTitle": "Enable Outgoing Webhooks: ",
"admin.service.overrideDescription": "When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.",
"admin.service.overrideTitle": "Enable Overriding Usernames from Webhooks and Slash Commands: ",
- "admin.service.save": "Save",
- "admin.service.saving": "Saving Config...",
"admin.service.securityDesc": "When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.",
"admin.service.securityTitle": "Enable Security Alerts: ",
"admin.service.segmentDescription": "For users running a SaaS services, sign up for a key at Segment.com to track metrics.",
@@ -428,41 +402,55 @@
"admin.service.ssoSessionDaysDesc": "The SSO session will expire after the number of days specified and will require a user to login again.",
"admin.service.testingDescription": "(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.",
"admin.service.testingTitle": "Enable Testing: ",
- "admin.service.title": "Service Settings",
- "admin.service.true": "true",
"admin.service.webSessionDays": "Session Length for Web in Days:",
"admin.service.webSessionDaysDesc": "The web session will expire after the number of days specified and will require a user to login again.",
"admin.service.webhooksDescription": "When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.",
"admin.service.webhooksTitle": "Enable Incoming Webhooks: ",
"admin.sidebar.addTeamSidebar": "Add team from sidebar menu",
"admin.sidebar.audits": "Compliance and Auditing",
- "admin.sidebar.compliance": "Compliance Settings",
- "admin.sidebar.email": "Email Settings",
- "admin.sidebar.file": "File Settings",
- "admin.sidebar.gitlab": "GitLab Settings",
- "admin.sidebar.ldap": "LDAP Settings",
+ "admin.sidebar.authentication": "Authentication",
+ "admin.sidebar.compliance": "Compliance",
+ "admin.sidebar.configuration": "Configuration",
+ "admin.sidebar.connections": "Connections",
+ "admin.sidebar.customBrand": "Custom Branding",
+ "admin.sidebar.customization": "Customization",
+ "admin.sidebar.database": "Database",
+ "admin.sidebar.developer": "Developer",
+ "admin.sidebar.email": "Email",
+ "admin.sidebar.external": "External Services",
+ "admin.sidebar.files": "Files",
+ "admin.sidebar.general": "General",
+ "admin.sidebar.gitlab": "GitLab",
+ "admin.sidebar.images": "Images",
+ "admin.sidebar.integrations": "Integrations",
+ "admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Edition and License",
- "admin.sidebar.loading": "Loading",
- "admin.sidebar.log": "Log Settings",
+ "admin.sidebar.logging": "Logging",
+ "admin.sidebar.login": "Login",
"admin.sidebar.logs": "Logs",
+ "admin.sidebar.notifications": "Notifications",
"admin.sidebar.other": "OTHER",
- "admin.sidebar.privacy": "Privacy Settings",
- "admin.sidebar.rate_limit": "Rate Limit Settings",
+ "admin.sidebar.privacy": "Privacy",
+ "admin.sidebar.publicLinks": "Public Links",
+ "admin.sidebar.push": "Mobile Push",
+ "admin.sidebar.rateLimiting": "Rate Limiting",
"admin.sidebar.reports": "SITE REPORTS",
"admin.sidebar.rmTeamSidebar": "Remove team from sidebar menu",
- "admin.sidebar.service": "Service Settings",
+ "admin.sidebar.security": "Security",
+ "admin.sidebar.sessions": "Sessions",
"admin.sidebar.settings": "SETTINGS",
- "admin.sidebar.sql": "SQL Settings",
- "admin.sidebar.statistics": "- Statistics",
- "admin.sidebar.support": "Legal and Support Settings",
- "admin.sidebar.team": "Team Settings",
- "admin.sidebar.teams": "TEAMS ({count})",
- "admin.sidebar.users": "- Users",
+ "admin.sidebar.sign_up": "Sign Up",
+ "admin.sidebar.statistics": "Statistics",
+ "admin.sidebar.storage": "Storage",
+ "admin.sidebar.support": "Legal and Support",
+ "admin.sidebar.teams": "TEAMS ({count, number})",
+ "admin.sidebar.users": "Users",
+ "admin.sidebar.usersAndTeams": "Users and Teams",
"admin.sidebar.view_statistics": "View Statistics",
+ "admin.sidebar.webhooks": "Webhooks and Commands",
"admin.sidebarHeader.systemConsole": "System Console",
"admin.sql.dataSource": "Data Source:",
"admin.sql.driverName": "Driver Name:",
- "admin.sql.false": "false",
"admin.sql.keyDescription": "32-character salt available to encrypt and decrypt sensitive fields in database.",
"admin.sql.keyExample": "Ex \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
"admin.sql.keyTitle": "At Rest Encrypt Key:",
@@ -474,14 +462,9 @@
"admin.sql.maxOpenTitle": "Maximum Open Connections:",
"admin.sql.noteDescription": "Changing properties in this section will require a server restart before taking effect.",
"admin.sql.noteTitle": "Note:",
- "admin.sql.regenerate": "Re-Generate",
"admin.sql.replicas": "Data Source Replicas:",
- "admin.sql.save": "Save",
- "admin.sql.saving": "Saving Config...",
- "admin.sql.title": "SQL Settings",
"admin.sql.traceDescription": "(Development Mode) When true, executing SQL statements are written to the log.",
"admin.sql.traceTitle": "Trace: ",
- "admin.sql.true": "true",
"admin.sql.warning": "Warning: re-generating this salt may cause some columns in the database to return empty results.",
"admin.support.aboutDesc": "Link to About page for more information on your Mattermost deployment, for example its purpose and audience within your organization. Defaults to Mattermost information page.",
"admin.support.aboutTitle": "About link:",
@@ -495,11 +478,8 @@
"admin.support.privacyTitle": "Privacy Policy link:",
"admin.support.problemDesc": "Link to help documentation from team site main menu. By default this points to the peer-to-peer troubleshooting forum where users can search for, find and request help with technical issues.",
"admin.support.problemTitle": "Report a Problem link:",
- "admin.support.save": "Save",
- "admin.support.saving": "Saving Config...",
"admin.support.termsDesc": "Link to Terms of Service available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.",
"admin.support.termsTitle": "Terms of Service link:",
- "admin.support.title": "Legal and Support Settings",
"admin.system_analytics.activeUsers": "Active Users With Posts",
"admin.system_analytics.title": "the System",
"admin.system_analytics.totalPosts": "Total Posts",
@@ -511,7 +491,6 @@
"admin.team.chooseImage": "Choose New Image",
"admin.team.dirDesc": "When true, teams that are configured to show in team directory will show on main page inplace of creating a new team.",
"admin.team.dirTitle": "Enable Team Directory: ",
- "admin.team.false": "false",
"admin.team.maxUsersDescription": "Maximum total number of users per team, including both active and inactive users.",
"admin.team.maxUsersExample": "Ex \"25\"",
"admin.team.maxUsersTitle": "Max Users Per Team:",
@@ -527,15 +506,11 @@
"admin.team.restrictTitle": "Restrict Creation To Domains:",
"admin.team.restrict_direct_message_any": "Any user on the Mattermost server",
"admin.team.restrict_direct_message_team": "Any member of the team",
- "admin.team.save": "Save",
- "admin.team.saving": "Saving Config...",
"admin.team.siteNameDescription": "Name of service shown in login screens and UI.",
"admin.team.siteNameExample": "Ex \"Mattermost\"",
"admin.team.siteNameTitle": "Site Name:",
"admin.team.teamCreationDescription": "When false, the ability to create teams is disabled. The create team button displays error when pressed.",
"admin.team.teamCreationTitle": "Enable Team Creation: ",
- "admin.team.title": "Team Settings",
- "admin.team.true": "true",
"admin.team.upload": "Upload",
"admin.team.uploadDesc": "Customize your user experience by adding a custom image to your login screen. See examples at <a href='http://docs.mattermost.com/administration/config-settings.html#custom-branding' target='_blank'>docs.mattermost.com/administration/config-settings.html#custom-branding</a>.",
"admin.team.uploaded": "Uploaded!",
@@ -544,6 +519,7 @@
"admin.team.userCreationTitle": "Enable User Creation: ",
"admin.team_analytics.activeUsers": "Active Users With Posts",
"admin.team_analytics.totalPosts": "Total Posts",
+ "admin.true": "true",
"admin.userList.title": "Users for {team}",
"admin.userList.title2": "Users for {team} ({count})",
"admin.user_item.authServiceEmail": ", <strong>Sign-in Method:</strong> Email",
@@ -876,7 +852,7 @@
"find_team.submitError": "Please enter a valid email address",
"general_tab.chooseName": "Please choose a new name for your team",
"general_tab.codeDesc": "Click 'Edit' to regenerate Invite Code.",
- "general_tab.codeLongDesc": "The Invite Code is used as part of the URL in the team invitation link created by **Get Team Invite Link** in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.",
+ "general_tab.codeLongDesc": "The Invite Code is used as part of the URL in the team invitation link created by <strong>Get Team Invite Link</strong> in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.",
"general_tab.codeTitle": "Invite Code",
"general_tab.dirContact": "Contact your system administrator to turn on the team directory on the system home page.",
"general_tab.dirDisabled": "Team Directory has been disabled. Please ask a System Admin to enable the Team Directory in the System Console team settings.",
@@ -1025,6 +1001,7 @@
"navbar_dropdown.manageMembers": "Manage Members",
"navbar_dropdown.report": "Report a Problem",
"navbar_dropdown.switchTeam": "Switch to {team}",
+ "navbar_dropdown.switchTo": "Switch to ",
"navbar_dropdown.teamLink": "Get Team Invite Link",
"navbar_dropdown.teamSettings": "Team Settings",
"password_form.change": "Change my password",
@@ -1041,6 +1018,8 @@
"password_send.link": "<p>A password reset link has been sent to <b>{email}</b></p>",
"password_send.reset": "Reset my password",
"password_send.title": "Password Reset",
+ "pending_post_actions.cancel": "Cancel",
+ "pending_post_actions.retry": "Retry",
"permalink.error.access": "Permalink belongs to a channel you do not have access to",
"post_attachment.collapse": "▲ collapse text",
"post_attachment.more": "▼ read more",
@@ -1048,7 +1027,6 @@
"post_body.deleted": "(message deleted)",
"post_body.plusMore": " plus {count} other files",
"post_body.plusOne": " plus 1 other file",
- "post_body.retry": "Retry",
"post_delete.notPosted": "Comment could not be posted",
"post_delete.okay": "Okay",
"post_delete.someone": "Someone deleted the message on which you tried to post a comment.",
@@ -1098,7 +1076,6 @@
"rhs_comment.del": "Delete",
"rhs_comment.edit": "Edit",
"rhs_comment.permalink": "Permalink",
- "rhs_comment.retry": "Retry",
"rhs_header.details": "Message Details",
"rhs_root.del": "Delete",
"rhs_root.direct": "Direct Message",
@@ -1146,7 +1123,7 @@
"sidebar_right_menu.logout": "Logout",
"sidebar_right_menu.manageMembers": "Manage Members",
"sidebar_right_menu.report": "Report a Problem",
- "sidebar_right_menu.switch_team": "Switch Team",
+ "sidebar_right_menu.switch_team": "Team Selection",
"sidebar_right_menu.teamLink": "Get Team Invite Link",
"sidebar_right_menu.teamSettings": "Team Settings",
"signup_team.choose": "Teams you are a member of: ",
@@ -1288,13 +1265,17 @@
"user.settings.developer.thirdParty": "Open to register a new third-party application",
"user.settings.developer.title": "Developer Settings",
"user.settings.display.channelDisplayTitle": "Channel Display Mode",
- "user.settings.display.channeldisplaymode": "Select how text in a channel is displayed.",
+ "user.settings.display.channeldisplaymode": "Select the width of the center channel.",
"user.settings.display.clockDisplay": "Clock Display",
"user.settings.display.fixedWidthCentered": "Fixed width, centered",
"user.settings.display.fontDesc": "Select the font displayed in the Mattermost user interface.",
"user.settings.display.fontTitle": "Display Font",
"user.settings.display.fullScreen": "Full width",
"user.settings.display.language": "Language",
+ "user.settings.display.messageDisplayClean": "Clean",
+ "user.settings.display.messageDisplayCompact": "Compact",
+ "user.settings.display.messageDisplayDescription": "Select how messages in a channel should be displayed.",
+ "user.settings.display.messageDisplayTitle": "Message Display",
"user.settings.display.militaryClock": "24-hour clock (example: 16:00)",
"user.settings.display.nameOptsDesc": "Set how to display other user's names in posts and the Direct Messages list.",
"user.settings.display.normalClock": "12-hour clock (example: 4:00 PM)",
@@ -1370,6 +1351,7 @@
"user.settings.modal.security": "Security",
"user.settings.modal.title": "Account Settings",
"user.settings.notification.allActivity": "For all activity",
+ "user.settings.notification.push": "Mobile push notifications",
"user.settings.notification.soundConfig": "Please configure notification sounds in your browser settings",
"user.settings.notifications.channelWide": "Channel-wide mentions \"@channel\"",
"user.settings.notifications.close": "Close",
@@ -1392,6 +1374,10 @@
"user.settings.notifications.title": "Notification Settings",
"user.settings.notifications.usernameMention": "Your username mentioned \"@{username}\"",
"user.settings.notifications.wordsTrigger": "Words that trigger mentions",
+ "user.settings.push_notification.allActivity": "For all activity",
+ "user.settings.push_notification.info": "Notification alerts are pushed to your mobile device when there is activity in Mattermost.",
+ "user.settings.push_notification.off": "Off",
+ "user.settings.push_notification.onlyMentions": "For mentions and direct messages",
"user.settings.security.close": "Close",
"user.settings.security.currentPassword": "Current Password",
"user.settings.security.currentPasswordError": "Please enter your current password",
@@ -1428,5 +1414,6 @@
"web.footer.privacy": "Privacy",
"web.footer.terms": "Terms",
"web.header.back": "Back",
- "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere"
+ "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere",
+ "youtube_video.notFound": "Video not found"
}
diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json
index 7aa3071f6..7c6eb98b1 100644
--- a/webapp/i18n/es.json
+++ b/webapp/i18n/es.json
@@ -6,6 +6,7 @@
"about.enterpriseEditionSt": "Comunicaciones empresariales modernas protegidas por tu cortafuegos.",
"about.enterpriseEditione1": "Edición Enterprise E1",
"about.hash": "Hash de compilación:",
+ "about.hashee": "Hash de compilación de EE:",
"about.licensed": "Licenciado por:",
"about.number": "Número de compilación:",
"about.teamEditionLearn": "Únete a la comunidad Mattermost en ",
@@ -28,12 +29,12 @@
"activity_log_modal.androidNativeApp": "Android App Nativa",
"activity_log_modal.iphoneNativeApp": "iPhone App Nativa",
"add_command.autocomplete": "Autocompletar",
- "add_command.autocomplete.help": "Mostrar este comando en la lista de auto completado.",
+ "add_command.autocomplete.help": " Mostrar este comando en la lista de auto completado.",
"add_command.autocompleteDescription": "Descripción del Autocompletado",
"add_command.autocompleteDescription.help": "Descripción corta opcional para la lista de autocompletado del comando de barra.",
"add_command.autocompleteDescription.placeholder": "Ejemplo: \"Retorna resultados de una búsqueda con los registros de un paciente\"",
"add_command.autocompleteHint": "Pista del Autocompletado",
- "add_command.autocompleteHint.help": "Pista opcional que aparece como paramentros necesarios en la lista de autocompletado para el comando.",
+ "add_command.autocompleteHint.help": "Pista opcional que aparece como parámetros necesarios en la lista de auto completado para el comando.",
"add_command.autocompleteHint.placeholder": "Ejemplo: [Nombre del Paciente]",
"add_command.description": "Descripción",
"add_command.displayName": "Nombre a mostrar",
@@ -46,9 +47,12 @@
"add_command.method.help": "El tipo de comando que se utiliza al hacer una solicitud al URL.",
"add_command.method.post": "POST",
"add_command.trigger": "Palabra Gatilladora del Comando",
- "add_command.trigger.help1": "Ejemplos: /paciente, /cliente, /empleado",
- "add_command.trigger.help2": "Reservadas: /echo, /join, /logout, /me, /shrug",
- "add_command.trigger.placeholder": "Gatillador del Comando ej. \"hola\" no se debe incluir la barra",
+ "add_command.trigger.help1": "Ejemplos: paciente, cliente, empleado",
+ "add_command.trigger.help2": "Reservadas: echo, join, logout, me, shrug",
+ "add_command.trigger.placeholder": "Gatillador del comando ej. \"hola\"",
+ "add_command.triggerInvalidLength": "La palabra gatilladora debe tener entre {min} y {max} caracteres",
+ "add_command.triggerInvalidSlash": "La palabra gatilladora no puede comenzar con /",
+ "add_command.triggerInvalidSpace": "La palabra gatilladora no debe contener espacios",
"add_command.triggerRequired": "Se requiere una palabra gatilladora",
"add_command.url": "URL de Solicitud",
"add_command.url.help": "El URL para recibir el evento de la solicitud HTTP POST o GET cuando se ejecuta el comando de barra.",
@@ -128,9 +132,7 @@
"admin.email.allowUsernameSignInTitle": "Permitir inicio de sesión con Nombre de usuario: ",
"admin.email.easHelp": "Conoce más acerca de como compilar y desplegar tus propias aplicaciones moviles desde un <a href=\"http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas\" target=\"_blank\">App Store de Empresa</a>.",
"admin.email.emailFail": "Conexión fallida: {error}",
- "admin.email.emailSettings": "Configuraciones de correo",
"admin.email.emailSuccess": "No fueron reportados errores mientras se enviada el correo. Favor validar en tu bandeja de entrada.",
- "admin.email.false": "falso",
"admin.email.fullPushNotification": "Enviar el mensaje completo",
"admin.email.genericPushNotification": "Enviar descripción generica con nombres de usuario y canal",
"admin.email.inviteSaltDescription": "32-caracter salt añadido a la firma de invitación de correos. Aleatoriamente generado en la instalación. Click \"Re-Generar\" para crear nuevo salt.",
@@ -160,11 +162,8 @@
"admin.email.pushServerEx": "Ej.: \"https://push-test.mattermost.com\"",
"admin.email.pushServerTitle": "Servidor de Notificaciones:",
"admin.email.pushTitle": "Envío de Notificaciones: ",
- "admin.email.regenerate": "Regenerar",
"admin.email.requireVerificationDescription": "Normalmente asignado como verdadero en producción. Cuando es verdadero, Mattermost requiere una verificación del correo electrónico después de crear la cuenta y antes de iniciar sesión por primera vez. Los desarrolladores pude que quieran dejar esta opción en falso para evitar la necesidad de verificar correos y así desarrollar más rápido.",
"admin.email.requireVerificationTitle": "Require verificación de correo electrónico: ",
- "admin.email.save": "Guardar",
- "admin.email.saving": "Guardando...",
"admin.email.selfPush": "Ingresar manualmente la ubicación del Servicio de Notificaciones Push",
"admin.email.smtpPasswordDescription": " Obten esta credencial del administrador del servidor de correos.",
"admin.email.smtpPasswordExample": "Ej: \"tucontraseña\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
@@ -179,9 +178,9 @@
"admin.email.smtpUsernameExample": "Ej: \"admin@tuempresa.com\", \"AKIADTOVBGERKLCBV\"",
"admin.email.smtpUsernameTitle": "Usuario SMTP:",
"admin.email.testing": "Probando...",
- "admin.email.true": "verdadero",
+ "admin.false": "false",
"admin.gitab.clientSecretDescription": "Utilizar este valor vía instrucciones suministradas anteriormente para iniciar sesión en GitLab.",
- "admin.gitlab.EnableHtmlDesc": "<ol><li>Inicia sesión con tu cuenta en GitLab y dirigete a Profile Settings -> Applications.</li><li>Ingresa los URIs \"<tu-mattermost-url>/login/gitlab/complete\" (ejemplo: http://localhost:8065/login/gitlab/complete) y \"<tu-mattermost-url>/signup/gitlab/complete\". </li><li>Luego utiliza los valores de los campos \"Secret\" e \"Id\" de GitLab y completa las opciones que abajo se presentan.</li><li>Completa las dirección URLs abajo. </li></ol>",
+ "admin.gitlab.EnableHtmlDesc": "<ol><li>Inicia sesión con tu cuenta en GitLab y dirigete a Profile Settings -> Applications.</li><li>Ingresa los URIs \"<your-mattermost-url>/login/gitlab/complete\" (ejemplo: http://localhost:8065/login/gitlab/complete) y \"<your-mattermost-url>/signup/gitlab/complete\". </li><li>Luego utiliza los valores de los campos \"Secret\" e \"Id\" de GitLab y completa las opciones que abajo se presentan.</li><li>Completa las dirección URLs abajo. </li></ol>",
"admin.gitlab.authDescription": "Ingresar <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize). Asegurate que si utilizas HTTPS o HTTPS tus URLS sean correctas",
"admin.gitlab.authExample": "Ej \"\"",
"admin.gitlab.authTitle": "URL para autentificación:",
@@ -192,15 +191,11 @@
"admin.gitlab.clientSecretTitle": "Secreto:",
"admin.gitlab.enableDescription": "Cuando está asignado como verdadero, Mattermost permite la creación de equipos y cuentas utilizando el servicio de OAuth de GitLab.",
"admin.gitlab.enableTitle": "Enable Sign Up With GitLab: ",
- "admin.gitlab.false": "falso",
- "admin.gitlab.save": "Guardar",
- "admin.gitlab.saving": "Guardando...",
"admin.gitlab.settingsTitle": "Configuración de GitLab",
"admin.gitlab.tokenDescription": "Ingresar <your-gitlab-url>/oauth/token. Asegurate que si utilizas HTTPS o HTTPS tus URLS sean correctas",
"admin.gitlab.tokenExample": "Ej \"\"",
"admin.gitlab.tokenTitle": "Url para obteción de Token:",
- "admin.gitlab.true": "verdadero",
- "admin.gitlab.userDescription": "Ingresar <tu-gitlab-url>/api/v3/user. Asegurate que si utilizas HTTPS o HTTPS tus URLS sean correctas",
+ "admin.gitlab.userDescription": "Ingresar <your-gitlab-url>/api/v3/user. Asegurate que si utilizas HTTPS o HTTPS tus URLS sean correctas",
"admin.gitlab.userExample": "Ej \"\"",
"admin.gitlab.userTitle": "URL para obtener datos de usuario:",
"admin.image.amazonS3BucketDescription": "Nombre que ha seleccionado para el bucket S3 en AWS.",
@@ -215,8 +210,6 @@
"admin.image.amazonS3SecretDescription": "Obetener esta credencial del administrador de tu Amazon EC2.",
"admin.image.amazonS3SecretExample": "Ej \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.image.amazonS3SecretTitle": "Llave secreta de Amazon S3:",
- "admin.image.false": "falso",
- "admin.image.fileSettings": "Configuración de archivos",
"admin.image.localDescription": "Directorio al que se esriben los archivos de imágen. Si es vacio, se establecerá en ./data/.",
"admin.image.localExample": "Ej \"./dato/\"",
"admin.image.localTitle": "Directorio local de ubicación:",
@@ -235,9 +228,6 @@
"admin.image.publicLinkDescription": "Salt de 32-characteres agregado para firmar los enlaces para las imagenes públicas. Aleatoriamente generados en la instalación. Pincha \"Regenerar\" para crear un nuevo salt.",
"admin.image.publicLinkExample": "Ej \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
"admin.image.publicLinkTitle": "Título del enlace público:",
- "admin.image.regenerate": "Regenerar",
- "admin.image.save": "Guardar",
- "admin.image.saving": "Guardando...",
"admin.image.shareDescription": "Permitir a los usuarios compartir enlaces públicos para archivos e imágenes.",
"admin.image.shareTitle": "Compartir publicamente enlaces de archivos: ",
"admin.image.storeAmazonS3": "Amazon S3",
@@ -249,7 +239,6 @@
"admin.image.thumbWidthDescription": "Ancho de imágen miniatura subida. Actualizando este valor la imagen miniatura cambia en el futuro, pero no cambia para imagenes creadas en el pasado.",
"admin.image.thumbWidthExample": "Ej \"120\"",
"admin.image.thumbWidthTitle": "Ancho de imágen miniatura:",
- "admin.image.true": "verdadero",
"admin.ldap.bannerDesc": "Si el atributo de un usuario cambia en el servidor LDAP será actualizado la próxima vez que el usuario ingrese sus credenciales para iniciar sesión en Mattermost. Esto incluye si un usuario se inactiva o se remueve en el servidor LDAP. Sincronización con servidores LDAP está planificado para futuras versiones.",
"admin.ldap.bannerHeading": "Nota:",
"admin.ldap.baseDesc": "El DN Base es el Nombre Distinguido de la ubicación donde Mattermost debe comenzar a buscar a los usuarios en el árbol del LDAP.",
@@ -264,7 +253,6 @@
"admin.ldap.emailAttrTitle": "Atributo de Correo Electrónico:",
"admin.ldap.enableDesc": "Cuando es verdadero, Mattermost permite realizar inicio de sesión utilizando LDAP",
"admin.ldap.enableTitle": "Habilitar inicio de sesión con LDAP:",
- "admin.ldap.false": "falso",
"admin.ldap.firstnameAttrDesc": "El atributo en el servidor LDAP que será utilizado para poblar el nombre de los usuarios en Mattermost.",
"admin.ldap.firstnameAttrEx": "Ej \"givenName\"",
"admin.ldap.firstnameAttrTitle": "Atributo del Nombre",
@@ -287,15 +275,11 @@
"admin.ldap.queryDesc": "El tiempo de espera para las consultas en el servidor LDAP. Aumenta este valor si estás obteniendo errores por falta de tiempo debido a un servidor de LDAP lento.",
"admin.ldap.queryEx": "Ej \"60\"",
"admin.ldap.queryTitle": "Tiempo de espera para las Consultas (segundos):",
- "admin.ldap.save": "Guardar",
- "admin.ldap.saving": "Guardando...",
"admin.ldap.serverDesc": "El dominio o dirección IP del servidor LDAP.",
"admin.ldap.serverEx": "Ej \"10.0.0.23\"",
"admin.ldap.serverTitle": "Servidor LDAP:",
"admin.ldap.skipCertificateVerification": "Omitir la Verificación del Certificado",
"admin.ldap.skipCertificateVerificationDesc": "Omite la verificación del certificado para las conexiones TLS o STARTTLS. No recomendado para ambientes de producción donde TLS es requerido. Utilizalo sólamente para pruebas.",
- "admin.ldap.title": "Configuración de LDAP",
- "admin.ldap.true": "verdadero",
"admin.ldap.uernameAttrDesc": "El atributo en el servidor LDAP que se utilizará para poblar el nombre de usuario en Mattermost. Este puede ser igual al Attributo Id.",
"admin.ldap.userFilterDisc": "Filtro de LDAP para buscar los objetos de los usuarios.",
"admin.ldap.userFilterEx": "Ej. \"(objectClass=user)\"",
@@ -316,7 +300,6 @@
"admin.license.uploading": "Subiendo Licencia...",
"admin.log.consoleDescription": "Normalmente asignado en falso en producción. Los desarolladores pueden configurar este campo en verdadero para ver de mensajes de consola basado en las opciones de nivel configuradas. Si es verdadera, el servidor escribirá los mensajes en una salida estandar (stdout).",
"admin.log.consoleTitle": "Mostrar registros en la consola: ",
- "admin.log.false": "falso",
"admin.log.fileDescription": "Normalmente asignado en verdadero en producción. Cueando es verdadero, los archivos de registro son escritos en la ubicación especificada a continuación.",
"admin.log.fileLevelDescription": "Esta configuración determina el nivel de detalle con el cual los eventos serán escritos en el archivo de registro. ERROR: Sólo salida de mensajes de error. INFO: Salida de mensaje de error y información acerca de la partida e inicialización. DEBUG: Muestra un alto detalle para que los desarolladores que trabajan con eventos de depuración.",
"admin.log.fileLevelTitle": "Nivel registro:",
@@ -336,27 +319,18 @@
"admin.log.locationPlaceholder": "Ingresar locación de archivo",
"admin.log.locationTitle": "Ubicación de archivo:",
"admin.log.logSettings": "Configuración de registro",
- "admin.log.save": "Guardar",
- "admin.log.saving": "Guardando...",
- "admin.log.true": "verdadero",
"admin.logs.reload": "Recargar",
"admin.logs.title": "Servidor de registros",
"admin.nav.help": "Ayuda",
"admin.nav.logout": "Cerrar sesión",
"admin.nav.report": "Reportar problema",
- "admin.nav.switch": "Cambiar a {display_name}",
- "admin.privacy.false": "falso",
- "admin.privacy.save": "Guardar",
- "admin.privacy.saving": "Guardando...",
+ "admin.nav.switch": "Seleccionar Equipo",
"admin.privacy.showEmailDescription": "Cuando es falso, oculta la dirección de correo para otros usuarios en la interfaz de usuario, incluyendo a los dueños y administradores del grupo. Usado cuando el sistema es configurado para administrar grupos y donde algunos usuarios escogen mantener su información de contacto como privada.",
"admin.privacy.showEmailTitle": "Mostrar dirección de correo electrónico: ",
"admin.privacy.showFullNameDescription": "Cuando está asignado en falso, esconde el nombre completo de los usuarios para otros usuarios, incluyendo dueños de equipos y administradores de equipos. El nombre de usuario es mostrado en vez del nombre completo.",
"admin.privacy.showFullNameTitle": "Mostrar nombre completo: ",
- "admin.privacy.title": "Configuraciones de privacidad",
- "admin.privacy.true": "verdadero",
"admin.rate.enableLimiterDescription": "Cuando es verdadero, La APIs son reguladas a tasas especificadas a continuación.",
"admin.rate.enableLimiterTitle": "Habilitar el limitador de velocidad: ",
- "admin.rate.false": "falso",
"admin.rate.httpHeaderDescription": "Al llenar este campo, se limita la velocidad según el encabezado HTTP especificado (e.j. cuando se configura con NGINX asigna \"X-Real-IP\", cuando se configura con AmazonELB asigna \"X-Forwarded-For\").",
"admin.rate.httpHeaderExample": "Ej \"X-Real-IP\", \"X-Forwarded-For\"",
"admin.rate.httpHeaderTitle": "Variar para encabezado HTTP:",
@@ -370,14 +344,17 @@
"admin.rate.queriesTitle": "Número de consultas por minuto:",
"admin.rate.remoteDescription": "Cuando es verdadero, límite de velocidad para el accedo a la API desde dirección IP.",
"admin.rate.remoteTitle": "Variar por direcciones remotas: ",
- "admin.rate.save": "Guardar",
- "admin.rate.saving": "Guardando...",
- "admin.rate.title": "Configuración de velocidad",
- "admin.rate.true": "verdadero",
+ "admin.recycle.button": "Recycle Database Connections",
+ "admin.recycle.loading": " Recycling...",
+ "admin.recycle.reloadFail": "Recycling unsuccessful: {error}",
+ "admin.regenerate": "Re-Generate",
+ "admin.reload.button": "Reload Configuration From Disk",
+ "admin.reload.loading": " Loading...",
+ "admin.reload.reloadFail": "Reloading unsuccessful: {error}",
"admin.reset_password.close": "Cerrar",
"admin.reset_password.newPassword": "Nueva contraseña",
"admin.reset_password.select": "Seleccionar",
- "admin.reset_password.submit": "Por favor, introducir como mínimo 5 caracteres.",
+ "admin.reset_password.submit": "Por favor, introducir como mínimo {chars} caracteres.",
"admin.reset_password.titleReset": "Restablecer la contraseña",
"admin.reset_password.titleSwitch": "Cambiar cuenta a Correo Electrónico/Contraseña",
"admin.select_team.close": "Cerrar",
@@ -393,7 +370,6 @@
"admin.service.corsTitle": "Permitir Solicitudes de Origen Cruzado desde:",
"admin.service.developerDesc": "(Opción de Desarrollador) Cuando está asignado en verdadero, información extra sobre errores se muestra en el UI.",
"admin.service.developerTitle": "Habilitar modo de Desarrollador: ",
- "admin.service.false": "falso",
"admin.service.googleDescription": "Asigna una llave a este campo para habilitar la previsualización de videos de YouTube tomados de los enlaces que aparecen en los mensajes o comentarios. Las instrucciones de como obtener una llave está disponible en <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Al dejar este campo en blanco deshabilita la generación de previsualizaciones de videos de YouTube desde los enlaces.",
"admin.service.googleExample": "Ej \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"",
"admin.service.googleTitle": "Llave de desarrolador Google:",
@@ -414,8 +390,6 @@
"admin.service.outWebhooksTitle": "Habilitar Webhooks de Salida: ",
"admin.service.overrideDescription": "Cuando es verdadero, se le permitirá cambiar el nombre de usuario desde webhooks. Nota, en conjunto con cambio de icono, podría exponer a los usuarios a sufrir ataques de phishing.",
"admin.service.overrideTitle": "Habilitar el cambio de nombres de usuario desde los Webhooks: ",
- "admin.service.save": "Guardar",
- "admin.service.saving": "Guardando....",
"admin.service.securityDesc": "Cuando es verdadero, Los Administradores del Sistema serán notificados por correo electrónico se han anunciado alertas de seguridad relevantes en las últimas 12 horas. Requiere que los correos estén habilitados.",
"admin.service.securityTitle": "Habilitar Alertas de Seguridad: ",
"admin.service.segmentDescription": "Para usuarios que corren en servicios SaaS, registarse en Segment.com para obtener su llave.",
@@ -428,41 +402,55 @@
"admin.service.ssoSessionDaysDesc": "Las sesión expirará lugo de transcurrido el numero de días especificado y se solicitará al usuario que inicie sesión nuevamente.",
"admin.service.testingDescription": "(Opción de desarollo) Cuando es verdadero, /pruebadecarga el comando slash es habilitado para cargar el nombre de la cuenta y probar la data. Cambiando esto será necesario reiniciar el servidor para que haga efecto.",
"admin.service.testingTitle": "Habilitar Pruebas: ",
- "admin.service.title": "Configuracion de servicios",
- "admin.service.true": "verdadero",
"admin.service.webSessionDays": "Duración de la Sesión en Días para Web:",
"admin.service.webSessionDaysDesc": "La sesión web expirará luego de transcurrido el número de días especificado y se solicitará al usuaio que inicie sesión nuevamente.",
"admin.service.webhooksDescription": "Cuando es verdadero, la entradas de webhooks será permitida. Para ayudar a combatir ataques phishing, todos los comentarios de webhooks serán marcados con una etiqueta BOT.",
"admin.service.webhooksTitle": "Habilitar Webhooks de Entrada: ",
"admin.sidebar.addTeamSidebar": "Agregar un equipo el menú lateral",
"admin.sidebar.audits": "Auditorías",
- "admin.sidebar.compliance": "Configuración de Cumplimiento",
- "admin.sidebar.email": "Configuración de correo",
- "admin.sidebar.file": "Configuracion de archivos",
- "admin.sidebar.gitlab": "Configuración de GitLab",
- "admin.sidebar.ldap": "Configuración LDAP",
+ "admin.sidebar.authentication": "Autenticación",
+ "admin.sidebar.compliance": "Cumplimiento",
+ "admin.sidebar.configuration": "Configuración",
+ "admin.sidebar.connections": "Conexiones",
+ "admin.sidebar.customBrand": "Marca Personalizada",
+ "admin.sidebar.customization": "Personalización",
+ "admin.sidebar.database": "Base de Datos",
+ "admin.sidebar.developer": "Desarrollo",
+ "admin.sidebar.email": "Correo electrónico",
+ "admin.sidebar.external": "Servicios Externos",
+ "admin.sidebar.files": "Archivos",
+ "admin.sidebar.general": "General",
+ "admin.sidebar.gitlab": "GitLab",
+ "admin.sidebar.images": "Imagenes",
+ "admin.sidebar.integrations": "Integraciones",
+ "admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Edición y Licencia",
- "admin.sidebar.loading": "Cargando",
- "admin.sidebar.log": "Configuracion de log",
+ "admin.sidebar.logging": "Registros",
+ "admin.sidebar.login": "Inicio de Sesión",
"admin.sidebar.logs": "Registros",
+ "admin.sidebar.notifications": "Notificaciones",
"admin.sidebar.other": "OTROS",
- "admin.sidebar.privacy": "Configuración de privacidad",
- "admin.sidebar.rate_limit": "Configuración de velocidad",
+ "admin.sidebar.privacy": "Privacidad",
+ "admin.sidebar.publicLinks": "Enlaces Públicos",
+ "admin.sidebar.push": "Push a Móvil",
+ "admin.sidebar.rateLimiting": "Límite de Velocidad",
"admin.sidebar.reports": "REPORTES DEL SITIO",
"admin.sidebar.rmTeamSidebar": "Remover un equipo del menú lateral",
- "admin.sidebar.service": "Configuración de servicio",
+ "admin.sidebar.security": "Seguridad",
+ "admin.sidebar.sessions": "Sesiones",
"admin.sidebar.settings": "CONFIGURACIONES",
- "admin.sidebar.sql": "Configuración de SQL",
- "admin.sidebar.statistics": "- Estadísticas",
- "admin.sidebar.support": "Configuración de Soporte",
- "admin.sidebar.team": "Configuración de equipo",
- "admin.sidebar.teams": "EQUIPOS ({count})",
- "admin.sidebar.users": "- Usuarios",
+ "admin.sidebar.sign_up": "Registro",
+ "admin.sidebar.statistics": "Estadísticas",
+ "admin.sidebar.storage": "Almacenamiento",
+ "admin.sidebar.support": "Legal y Soporte",
+ "admin.sidebar.teams": "EQUIPOS ({count, number})",
+ "admin.sidebar.users": "Usuarios",
+ "admin.sidebar.usersAndTeams": "Usuarios y Equipos",
"admin.sidebar.view_statistics": "Ver Estadísticas",
+ "admin.sidebar.webhooks": "Webhooks y Comandos",
"admin.sidebarHeader.systemConsole": "Consola de sistema",
"admin.sql.dataSource": "Origen de datos:",
"admin.sql.driverName": "Nombre de controlador:",
- "admin.sql.false": "falso",
"admin.sql.keyDescription": "32-caracter disponible para encriptar y desincriptar campos sencible de la base de datos.",
"admin.sql.keyExample": "Ej \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
"admin.sql.keyTitle": "At Rest Encrypt Key:",
@@ -474,14 +462,9 @@
"admin.sql.maxOpenTitle": "Máximo de conexiones abiertas:",
"admin.sql.noteDescription": "Cambiando las propiedades de esta sección se requerirá reiniciar el servidor para que los cambios tomen efecto",
"admin.sql.noteTitle": "Nota:",
- "admin.sql.regenerate": "Regenerar",
"admin.sql.replicas": "Origen de datos de réplica:",
- "admin.sql.save": "Guardar",
- "admin.sql.saving": "Guardando...",
- "admin.sql.title": "Configuración de SQL",
"admin.sql.traceDescription": "(Modo desarrolador) Cuando es verdadero, la ejecución de sentencias SQL se escriben en el registro.",
"admin.sql.traceTitle": "Traza: ",
- "admin.sql.true": "verdadero",
"admin.sql.warning": "Precaución: re-generando esto puede causar que algunas columnas de la base de datos retornen resultados vacíos.",
"admin.support.aboutDesc": "Enlace para la página de Acerca que contiene más información sobre Mattermost, por ejemplo el propósito y audiencia dentro de la organización. De forma predeterminada apunta a la página de información de Mattermost.",
"admin.support.aboutTitle": "Enlace de Acerca:",
@@ -495,11 +478,8 @@
"admin.support.privacyTitle": "Enlace de políticas de Privacidad:",
"admin.support.problemDesc": "Enlace con la documentación de ayuda para el equipo desde el menú principal. Como predeterminado esto apunta a un foro de ayuda donde los usuarios pueden buscar, encontrar y solicitar ayuda sobre temas técnicos.",
"admin.support.problemTitle": "Enlace de Reportar un Problema:",
- "admin.support.save": "Guradar",
- "admin.support.saving": "Guardando...",
"admin.support.termsDesc": "Enlace para los Terminos y Condiciones disponible para los usuarios en versión de escritorio y movil. Al dejarlo en blanco esconderá la opción que muestra el aviso.",
"admin.support.termsTitle": "Enlace de Terminos y Condiciones:",
- "admin.support.title": "Configuración de Soporte",
"admin.system_analytics.activeUsers": "Usuarios Activos con Mensajes",
"admin.system_analytics.title": "el Sistema",
"admin.system_analytics.totalPosts": "Total de Mensajes",
@@ -511,7 +491,6 @@
"admin.team.chooseImage": "Selecciona una Imagen Nueva",
"admin.team.dirDesc": "Cuando es verdadero, Los equipos que esten configurados para mostrarse en el directorio de equipos se mostrarán en vez de crear un nuevo equipo.",
"admin.team.dirTitle": "Habilitar Directorio de Equipos: ",
- "admin.team.false": "falso",
"admin.team.maxUsersDescription": "Número máximo de usuarios por equipo, incluyendo usuarios activos e inactivos.",
"admin.team.maxUsersExample": "Ej \"25\"",
"admin.team.maxUsersTitle": "Máximo de usuarios por equipo:",
@@ -527,15 +506,11 @@
"admin.team.restrictTitle": "Restringir la creación de dominios:",
"admin.team.restrict_direct_message_any": "Cualquier usuario en el servidor de Mattermost",
"admin.team.restrict_direct_message_team": "Cualquier miembro del equipo",
- "admin.team.save": "Guardar",
- "admin.team.saving": "Guardando...",
"admin.team.siteNameDescription": "Nombre de servicios mostrados en pantalla login y UI.",
"admin.team.siteNameExample": "Ex \"Mattermost\"",
"admin.team.siteNameTitle": "Nombre de sitio:",
"admin.team.teamCreationDescription": "Cuando es falso, la posibilidad de crear equipos es deshabilitada. El botón crear equipo arrojará error cuando sea presionado.",
"admin.team.teamCreationTitle": "Habilitar Creación de Equipos: ",
- "admin.team.title": "Configuración de equipo",
- "admin.team.true": "Verdadero",
"admin.team.upload": "Subir",
"admin.team.uploadDesc": "Personaliza la experiencia de usuario al agregar una imagen personalizada a la pantalla de inicio de sesión. Puedes ver ejemplos en <a href='http://docs.mattermost.com/administration/config-settings.html#custom-branding' target='_blank'>docs.mattermost.com/administration/config-settings.html#custom-branding</a>.",
"admin.team.uploaded": "Subida!",
@@ -544,6 +519,7 @@
"admin.team.userCreationTitle": "Habilitar Creación de Usuarios: ",
"admin.team_analytics.activeUsers": "Active Users With Posts",
"admin.team_analytics.totalPosts": "Total de Mensajes",
+ "admin.true": "true",
"admin.userList.title": "Usuarios para {team}",
"admin.userList.title2": "Usuarios para {team} ({count})",
"admin.user_item.authServiceEmail": ", <strong>Método de inicio de sesión:</strong> Correo electrónico",
@@ -861,11 +837,11 @@
"file_upload.limited": "Se pueden subir un máximo de {count} archivos. Por favor envía otros mensajes para adjuntar más archivos.",
"file_upload.pasted": "Imagen Pegada el ",
"filtered_user_list.any_team": "Todos los Usuarios",
- "filtered_user_list.count": "{count} {count, plural, one {miembro} other {miembros}}",
- "filtered_user_list.countTotal": "{count} {count, plural, one {miembro} other {miembros}} de {total} Total",
+ "filtered_user_list.count": "{count} {count, plural, =0 {0 members} one {member} other {members}}",
+ "filtered_user_list.countTotal": "{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} Total",
"filtered_user_list.member": "Miembro",
"filtered_user_list.search": "Buscar miembros",
- "filtered_user_list.show": "Mostrar",
+ "filtered_user_list.show": "Filtro:",
"filtered_user_list.team_only": "Miembros de este Equipo",
"find_team.email": "Correo electrónico",
"find_team.findDescription": "Enviamos un correo electrónico con los equipos a los que perteneces.",
@@ -876,7 +852,7 @@
"find_team.submitError": "Por favor ingresa una dirección válida",
"general_tab.chooseName": "Por favor escoge otro nombre para tu equipo",
"general_tab.codeDesc": "Pincha 'Editar' para regenerar el código de Invitación.",
- "general_tab.codeLongDesc": "El Código de Invitación es utilizado como parte del URL del enlace creado por la opción **Obtener Enlace de invitación** en el menú principal. Regenerar este código crea un nuevo enlace e invalida los enlaces anteriores.",
+ "general_tab.codeLongDesc": "El Código de Invitación es utilizado como parte del URL del enlace creado por la opción <strong>Obtener Enlace de invitación</strong> en el menú principal. Regenerar este código crea un nuevo enlace e invalida los enlaces anteriores.",
"general_tab.codeTitle": "Código de Invitación",
"general_tab.dirContact": "Contácta a un administrador del sistema para habilitar el directorio de equipos en la página principal.",
"general_tab.dirDisabled": "El directorio de Equipos ha sido deshabilitado. Por favor solicita a un Administrador de Sistema que habilite la opción de Directorio de Equipos en la Consola del Sistema.",
@@ -1025,6 +1001,7 @@
"navbar_dropdown.manageMembers": "Administrar Miembros",
"navbar_dropdown.report": "Reportar un Problema",
"navbar_dropdown.switchTeam": "Cambiar a {team}",
+ "navbar_dropdown.switchTo": "Cambiar a ",
"navbar_dropdown.teamLink": "Enlace invitación al equipo",
"navbar_dropdown.teamSettings": "Configurar Equipo",
"password_form.change": "Cambiar mi contraseña",
@@ -1041,6 +1018,8 @@
"password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseña a <b>{email}</b></p>",
"password_send.reset": "Restablecer mi contraseña",
"password_send.title": "Restablecer Contraseña",
+ "pending_post_actions.cancel": "Cancelar",
+ "pending_post_actions.retry": "Reintentar",
"permalink.error.access": "El enlace permanente pertenece a un canal al cual no tienes acceso",
"post_attachment.collapse": "▲ colapsar texto",
"post_attachment.more": "▼ leer más",
@@ -1048,7 +1027,6 @@
"post_body.deleted": "(mensaje eliminado)",
"post_body.plusMore": " más {count} otros archivos",
"post_body.plusOne": " más 1 archivo",
- "post_body.retry": "Reintentar",
"post_delete.notPosted": "No se pudo enviar el comentario",
"post_delete.okay": "Ok",
"post_delete.someone": "Alguien borró el mensaje que querías comentar.",
@@ -1098,7 +1076,6 @@
"rhs_comment.del": "Borrar",
"rhs_comment.edit": "Editar",
"rhs_comment.permalink": "Enlace permanente",
- "rhs_comment.retry": "Reintentar",
"rhs_header.details": "Detalles del Mensaje",
"rhs_root.del": "Borrar",
"rhs_root.direct": "Mensaje Directo",
@@ -1146,7 +1123,7 @@
"sidebar_right_menu.logout": "Cerrar sesión",
"sidebar_right_menu.manageMembers": "Adminisrar Miembros",
"sidebar_right_menu.report": "Reporta un Problema",
- "sidebar_right_menu.switch_team": "Cambiar Equipo",
+ "sidebar_right_menu.switch_team": "Seleccionar Equipo",
"sidebar_right_menu.teamLink": "Enlace Invitación al Equipo",
"sidebar_right_menu.teamSettings": "Configurar Equipo",
"signup_team.choose": "Equipos a los que perteneces: ",
@@ -1288,13 +1265,17 @@
"user.settings.developer.thirdParty": "Abrir para registrar una nueva aplicación externa",
"user.settings.developer.title": "Configuraciones de Desarrollo",
"user.settings.display.channelDisplayTitle": "Modo en que se muestra el Canal",
- "user.settings.display.channeldisplaymode": "Selecciona como se muestra el texto del canal.",
+ "user.settings.display.channeldisplaymode": "Selecciona el ancho de la vista central.",
"user.settings.display.clockDisplay": "Visualización del Reloj",
"user.settings.display.fixedWidthCentered": "De ancho fijo, centrado",
"user.settings.display.fontDesc": "Selecciona la fuente con la que quieres ver la interfaz de Mattermost.",
"user.settings.display.fontTitle": "Fuente de Visualización",
"user.settings.display.fullScreen": "De ancho total",
"user.settings.display.language": "Idioma",
+ "user.settings.display.messageDisplayClean": "Limpio",
+ "user.settings.display.messageDisplayCompact": "Compacto",
+ "user.settings.display.messageDisplayDescription": "Selecciona la forma en como se deben mostrar los mensajes en el canal.",
+ "user.settings.display.messageDisplayTitle": "Mostrar Mensaje",
"user.settings.display.militaryClock": "Reloj de 24 horas (ejemplo: 16:00)",
"user.settings.display.nameOptsDesc": "Asigna como mostrar los nombres de los otros usuarios en los mensajes y en la lista de Mensajes Directos.",
"user.settings.display.normalClock": "Reloj de 12 horas (ejemplo: 4:00 pm)",
@@ -1353,7 +1334,7 @@
"user.settings.import_theme.submitError": "Formato inválido, por favor intenta copiando y pegando nuevamente.",
"user.settings.languages.change": "Cambia el idioma con el que se muestra la intefaz de usuario",
"user.settings.mfa.add": "Agrega AMF a tu cuenta",
- "user.settings.mfa.addHelp": "Para agregar autenticación de múltiples factores a tu cuenta debes tener un teléfono inteligente con Google Authenticator instalado.",
+ "user.settings.mfa.addHelp": "Puedes requerir de un smartphone basado en token, además de la contraseña para iniciar sesión en Mattermost.<br/><br/>Para habilitarlo, descarga Google Authenticator de <a target='_blank' href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8'>iTunes</a> o <a target='_blank' href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en'>Google Play</a> al teléfono, luego<br/><br/>1. Haz clic en <strong>Agregar AMF a su cuenta</strong> en el botón de arriba.<br/>2. Utiliza Google Authenticator para escanear el código QR que aparece.<br/>3. Escribe el código generado por Google Authenticator y haz clic en <strong>Guardar</strong>.<br/><br/>Al iniciar la sesión, se pedirá que introduzcas un token de Google Authenticator, además de tus credenciales.",
"user.settings.mfa.addHelpQr": "Por favor escanea el código QR con la app de Google Authenticator en tu teléfono inteligente e ingresa el token provisto por la app.",
"user.settings.mfa.enterToken": "Token",
"user.settings.mfa.qrCode": "Código QR",
@@ -1370,6 +1351,7 @@
"user.settings.modal.security": "Seguridad",
"user.settings.modal.title": "Configuración de la Cuenta",
"user.settings.notification.allActivity": "Para toda actividad",
+ "user.settings.notification.push": "Notificaciones push a móviles",
"user.settings.notification.soundConfig": "Por favor configura los sonidos de notificación en las configuraciones de tu navegador",
"user.settings.notifications.channelWide": "Menciones a todo el canal \"@channel\"",
"user.settings.notifications.close": "Cerrar",
@@ -1392,6 +1374,10 @@
"user.settings.notifications.title": "Configuracón de Notificaciones",
"user.settings.notifications.usernameMention": "Tu nombre de usuario mencionado \"@{username}\"",
"user.settings.notifications.wordsTrigger": "Palabras que gatillan menciones",
+ "user.settings.push_notification.allActivity": "Para toda actividad",
+ "user.settings.push_notification.info": "Se enviarán notificaciones a tu dispositivo móvil cuando haya actividad en Mattermost.",
+ "user.settings.push_notification.off": "Apagado",
+ "user.settings.push_notification.onlyMentions": "Sólo para menciones y mensajes directos",
"user.settings.security.close": "Cerrar",
"user.settings.security.currentPassword": "Contraseña Actual",
"user.settings.security.currentPasswordError": "Por favor ingresa tu contraseña actual",
@@ -1417,7 +1403,7 @@
"user.settings.security.switchLdap": "Cambiar a utilizar LDAP",
"user.settings.security.title": "Configuración de Seguridad",
"user.settings.security.viewHistory": "Visualizar historial de acceso",
- "user_list.notFound": "No se encontraron usuarios :(",
+ "user_list.notFound": "No se encontraron usuarios",
"user_profile.notShared": "Correo no compartido",
"view_image.loading": "Cargando ",
"view_image_popover.download": "Descargar",
@@ -1428,5 +1414,6 @@
"web.footer.privacy": "Privacidad",
"web.footer.terms": "Términos",
"web.header.back": "Atrás",
- "web.root.singup_info": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte"
+ "web.root.singup_info": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte",
+ "youtube_video.notFound": "Video no encontrado"
}
diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json
index 752d5bea3..15a410a66 100644
--- a/webapp/i18n/fr.json
+++ b/webapp/i18n/fr.json
@@ -1,59 +1,95 @@
{
"about.close": "Quitter",
+ "about.copyright": "Copyright 2016 Mattermost, Inc. Tout droits réservés",
"about.date": "Date de compilation :",
+ "about.enterpriseEditionLearn": "Apprenez-en plus sur l’édition entreprise à : ",
+ "about.enterpriseEditionSt": "Communication d’entreprise moderne derrière votre pare-feu.",
"about.enterpriseEditione1": "Édition Enterprise",
"about.hash": "Clé de compilation :",
- "about.licensed": "Clé de licence :",
+ "about.hashee": "Clé de compilation EE :",
+ "about.licensed": "Clé de licence :",
"about.number": "Numéro de compilation :",
+ "about.teamEditionLearn": "Rejoignez la communauté Mattermost à : ",
+ "about.teamEditionSt": "Toute la communication de votre équipe à un endroit, accessible de partout.",
"about.teamEditiont0": "Édition Team",
"about.teamEditiont1": "Édition Enterprise",
"about.title": "À propos de Mattermost",
- "about.version": "Version :",
+ "about.version": "Version :",
"access_history.title": "Accéder à l'historique",
"activity_log.activeSessions": "Sessions actives",
- "activity_log.browser": "Navigateur: {browser}",
- "activity_log.firstTime": "Première activité: {date}, {time}",
- "activity_log.lastActivity": "Dernière activité: {date}, {time}",
+ "activity_log.browser": "Navigateur : {browser}",
+ "activity_log.firstTime": "Première activité : {date}, {time}",
+ "activity_log.lastActivity": "Dernière activité : {date}, {time}",
"activity_log.logout": "Se déconnecter",
"activity_log.moreInfo": "Plus d'informations",
- "activity_log.os": "OS: {os}",
- "activity_log.sessionId": "identifiant de session: {id}",
- "activity_log.sessionsDescription": "Les sessions sont créées lors que vous vous connecté depuis un nouveau navigateur. Les sessions vous permettent d'utiliser Mattermost sans devoir vous connecter à nouveau durant un temps défini par l'administrateur système. Si vous souhaitez vous déconnecter plus tôt, utilisez le bouton \"Se déconnecter\" ci-dessous pour mettre fin à votre session.",
+ "activity_log.os": "OS : {os}",
+ "activity_log.sessionId": "Identifiant de session : {id}",
+ "activity_log.sessionsDescription": "Les sessions sont créées lorsque vous vous connectez depuis un nouveau navigateur. Les sessions vous permettent d'utiliser Mattermost sans devoir vous connecter à nouveau durant un temps défini par l'administrateur système. Si vous souhaitez vous déconnecter plus tôt, utilisez le bouton \"Se déconnecter\" ci-dessous pour mettre fin à votre session.",
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Application Android",
"activity_log_modal.iphoneNativeApp": "Application pour iPhone",
- "add_command.autocomplete.help": "Afficher cette commande dans la liste d'auto-complétion",
+ "add_command.autocomplete": "Auto-complétion",
+ "add_command.autocomplete.help": " Afficher cette commande dans la liste d'auto-complétion.",
"add_command.autocompleteDescription": "Description de l'auto-complétion",
- "add_command.autocompleteDescription.help": "Description facultative de la commande slash dans la la liste d'auto-complétion.",
+ "add_command.autocompleteDescription.help": "Description facultative de la commande Slash pour la liste d'auto-complétion.",
"add_command.autocompleteDescription.placeholder": "Exemple : \"Retourne les résultats de recherche de dossiers médicaux\"",
- "add_command.autocompleteHint": "Explication pour l'auto-complétion",
- "add_command.autocompleteHint.help": "Explication facultative pour la liste d'auto-complétion au sujet des paramètres requis par cette commande slash.",
+ "add_command.autocompleteHint": "Indice de l’auto-complétion",
+ "add_command.autocompleteHint.help": "Explication facultative pour la liste d'auto-complétion au sujet des paramètres requis par cette commande Slash.",
"add_command.autocompleteHint.placeholder": "Exemple : [Nom du patient]",
+ "add_command.description": "Description",
+ "add_command.displayName": "Nom d'affichage",
+ "add_command.header": "Ajouter",
"add_command.iconUrl": "Icône de la réponse",
- "add_command.iconUrl.help": "Choisissez une photo de profil pour les réponses à cette commande slash. Entrez l'URL d'un fichier .png ou .jpg d'au moins 128x128 pixels.",
+ "add_command.iconUrl.help": "Choisissez une photo de profil pour les réponses à cette commande Sash. Entrez l'URL d'un fichier .png ou .jpg d'au moins 128x128 pixels.",
+ "add_command.iconUrl.placeholder": "https://www.example.com/myicon.png",
"add_command.method": "Méthode de requête",
"add_command.method.get": "GET",
"add_command.method.help": "Le type de méthode de requête HTTP envoyé à cette URL.",
"add_command.method.post": "POST",
- "add_command.token": "Jeton",
"add_command.trigger": "Mot-clé de déclenchement",
- "add_command.trigger.help1": "Exemples: /patient, /client, /employé",
+ "add_command.trigger.help1": "Exemples : /patient, /client, /employé",
"add_command.trigger.help2": "Mots réservés : /echo, /join, /logout, /me, /shrug",
+ "add_command.trigger.placeholder": "Mot-clé de déclenchement exemple \"hello\"",
+ "add_command.triggerInvalidLength": "un mot-clé doit contenir entre {min} à {max} caractères",
+ "add_command.triggerInvalidSlash": "Un mot-clé ne peut commencer par un /",
+ "add_command.triggerInvalidSpace": "Un mot-clé ne doit pas contenir d’espaces",
+ "add_command.triggerRequired": "Un mot-clé est requis",
"add_command.url": "URL de requête",
- "add_command.url.help": "L'URL de callback qui recevra la requête POST ou GET quand cette commande slash est exécutée.",
+ "add_command.url.help": "L'URL de callback qui recevra la requête POST ou GET quand cette commande Slash est exécutée.",
"add_command.url.placeholder": "Doit commencer par http:// ou https://",
+ "add_command.urlRequired": "Une requête URL est demandée",
"add_command.username": "Utilisateur affiché dans la réponse",
- "add_command.username.help": "Choisissez un nom d'utilisateur qui sera affiché dans la réponse de la commande slash. Les noms d'utilisateurs peuvent contenir jusqu'à 22 caractères, chiffres, lettres minuscules et symboles \"-\", \"_\" et \".\".",
+ "add_command.username.help": "Choisissez un nom d'utilisateur qui sera affiché dans la réponse de la commande Slash. Les noms d'utilisateurs peuvent contenir jusqu'à 22 caractères, chiffres, lettres minuscules et symboles \"-\", \"_\" et \".\" .",
+ "add_command.username.placeholder": "Nom d'utilisateur",
+ "add_incoming_webhook.cancel": "Annuler",
+ "add_incoming_webhook.channel": "Canal",
+ "add_incoming_webhook.channelRequired": "Un canal valide est demandé",
+ "add_incoming_webhook.description": "Description",
+ "add_incoming_webhook.header": "Ajouter",
+ "add_incoming_webhook.name": "Nom",
+ "add_incoming_webhook.save": "Enregistrer",
+ "add_outgoing_webhook.callbackUrls": "URLs de rappel (un par ligne)",
+ "add_outgoing_webhook.callbackUrlsRequired": "Un ou plusieurs rappel d’URL est demandé",
+ "add_outgoing_webhook.cancel": "Annuler",
+ "add_outgoing_webhook.channel": "Canal",
+ "add_outgoing_webhook.description": "Description",
+ "add_outgoing_webhook.header": "Ajouter",
+ "add_outgoing_webhook.name": "Nom",
+ "add_outgoing_webhook.save": "Enregistrer",
+ "add_outgoing_webhook.triggerWOrds": "Mot-clé (Un par ligne)",
+ "add_outgoing_webhook.triggerWords": "Mot-clé (Un par ligne)",
+ "add_outgoing_webhook.triggerWordsOrChannelRequired": "Un canal valide ou une liste de mot-clé est demander",
"admin.audits.reload": "Rafraîchir",
"admin.audits.title": "Activité de l'utilisateur",
- "admin.compliance.directoryDescription": "Répertoire des rapports de conformité. Si non spécifié : ./data/ .",
+ "admin.compliance.directoryDescription": "Répertoire des rapports de conformité. Si non spécifié : ./data/ .",
"admin.compliance.directoryExample": "Ex : \"./data/\"",
"admin.compliance.directoryTitle": "Répertoire des éléments de conformité :",
+ "admin.compliance.enableDailyDesc": "Si activé, Mattermost générera un rapport quotidien de conformité",
"admin.compliance.enableDailyTitle": "Activer le rapport quotidien :",
- "admin.compliance.enableDesc": "Si activé, Mattermost générera un rapport quotidien de conformité.",
+ "admin.compliance.enableDesc": "Si activé, Mattermost permet des rapports de conformité",
"admin.compliance.enableTitle": "Activer la conformité :",
"admin.compliance.false": "non",
- "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Note :</h4><p>La conformité est une option disponible sur l'édition Enterprise. Votre licence ne permet pas d'utiliser cette fonction. Cliquez <a href=\"http://mattermost.com\" target=\"_blank\">ici</a> pour des informations et des prix sur la licence Enterprise.</p>",
+ "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Note :</h4><p>La conformité est une option disponible sur l'édition d’Enterprise. Votre licence ne permet pas d'utiliser cette fonction. Cliquez sur <a href=\"http://mattermost.com\" target=\"_blank\">ici</a> pour des informations et des prix sur la licence d’Enterprise.</p>",
"admin.compliance.save": "Enregistrer",
"admin.compliance.saving": "Enregistrement des paramètres...",
"admin.compliance.title": "Configuration de la conformité",
@@ -61,10 +97,10 @@
"admin.compliance_reports.desc": "Profession :",
"admin.compliance_reports.desc_placeholder": "Ex : \"Audit 445 pour les RH\"",
"admin.compliance_reports.emails": "Adresses électroniques :",
- "admin.compliance_reports.emails_placeholder": "Ex : \"bill@example.com, bob@example.com\"",
+ "admin.compliance_reports.emails_placeholder": "Ex : \"bill@example.com, bob@example.com »",
"admin.compliance_reports.from": "De :",
"admin.compliance_reports.from_placeholder": "Ex : \"2016-03-18\"",
- "admin.compliance_reports.keywords": "Mots-clés :",
+ "admin.compliance_reports.keywords": "Mots-clés :",
"admin.compliance_reports.keywords_placeholder": "Ex : \"État des stocks\"",
"admin.compliance_reports.reload": "Rafraîchir",
"admin.compliance_reports.run": "Exécuter",
@@ -79,44 +115,56 @@
"admin.compliance_table.timestamp": "Horodatage",
"admin.compliance_table.type": "Type",
"admin.compliance_table.userId": "Demandé par",
- "admin.email.allowEmailSignInDescription": "Si vrai, les utilisateurs pourront se connecter avec leur adresse électronique et mot de passe.",
+ "admin.connectionSecurityNone": "Aucun",
+ "admin.connectionSecurityNoneDescription": "Mattermost sera connecté via une connexion non sécurisée.",
+ "admin.connectionSecurityStart": "Départ en mode ‘Transport Layer Security’",
+ "admin.connectionSecurityStartDescription": "Prend une connexion non sécurisée existante et tente de la mettre à niveau vers une connexion sécurisée en utilisant le mode TLS .",
+ "admin.connectionSecurityTest": "test de connexion",
+ "admin.connectionSecurityTitle": "Connexion sécurisée",
+ "admin.connectionSecurityTls": "Transport Layer Security",
+ "admin.connectionSecurityTlsDescription": "Chiffre la communication entre Mattermost et votre serveur.",
+ "admin.email.agreeHPNS": " J’ai compris et j’accepte le service de notifications Push hébergé par Mattermost <a href=\"https://about.mattermost.com/hpns-terms/\" target=\"_blank\">Terms of Service</a> et <a href=\"https://about.mattermost.com/hpns-privacy/\" target=\"_blank\">Privacy Policy</a>.",
+ "admin.email.allowEmailSignInDescription": "Si activé, les utilisateurs pourront se connecter avec leur adresse électronique et mot de passe.",
"admin.email.allowEmailSignInTitle": "Autoriser la connexion avec l'adresse électronique : ",
- "admin.email.allowSignupDescription": "Si vrai, la création d'équipes et de comptes en utilisant une adresse électronique et un mot de passe sera autorisée. Cette valeur devrait être fausse seulement si vous souhaitez limiter les connexions via un service SSO comme OAuth ou LDAP.",
- "admin.email.allowSignupTitle": "Autoriser la création de compte avec une adresse électronique :",
- "admin.email.allowUsernameSignInDescription": "Si vrai, les utilisateurs seront autorisés à se connecter avec leur nom d'utilisateur et leur mot de passe. Cette option n'est généralement utilisée que lorsque la vérification de l'adresse électronique est désactivée.",
- "admin.email.allowUsernameSignInTitle": "Autoriser la connexion avec le nom d'utilisateur:",
+ "admin.email.allowSignupDescription": "Si activé, la création d'équipes et de comptes en utilisant une adresse électronique et un mot de passe sera autorisée. Cette valeur devrait être fausse seulement si vous souhaitez limiter les connexions via un service SSO comme OAuth ou LDAP.",
+ "admin.email.allowSignupTitle": "Autoriser la création de compte avec une adresse électronique : ",
+ "admin.email.allowUsernameSignInDescription": "Si activé, les utilisateurs seront autorisés à se connecter avec leur nom d'utilisateur et leur mot de passe. Cette option n'est généralement utilisée que lorsque la vérification de l'adresse électronique est désactivée.",
+ "admin.email.allowUsernameSignInTitle": "Autoriser la connexion avec le nom d'utilisateur: ",
+ "admin.email.easHelp": "En savoir plus sur la compilation et le déploiement de vos propres applications mobiles à partir de <a href=\"http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas\" target=\"_blank\">Enterprise App Store</a>.",
"admin.email.emailFail": "Echec de la connexion : {error}",
- "admin.email.emailSettings": "Configuration de la messagerie",
"admin.email.emailSuccess": "Aucune erreur signalée lors de l'envoi du courriel. Vérifiez votre boîte de réception ou vos spams.",
- "admin.email.false": "faux",
- "admin.email.fullPushNotification": "Envoyer un message complet",
- "admin.email.genericPushNotification": "Envoyer une description général avec les noms des utilisateurs et des canaux",
+ "admin.email.fullPushNotification": "Envoyer un extrait du message complet",
+ "admin.email.genericPushNotification": "Envoyer une description générale avec les noms des utilisateurs et des canaux",
"admin.email.inviteSaltDescription": "Clé de salage de 32 caractères ajouté aux courriels d'invitation. Générée aléatoirement lors de l'installation. Cliquez sur \"Regénérer\" pour créer une nouvelle clé de salage.",
"admin.email.inviteSaltExample": "Exemple : \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
"admin.email.inviteSaltTitle": "Clé de salage du courriel d'invitation :",
+ "admin.email.mhpns": "La connexion à iOS et aux applications Android est cryptée",
+ "admin.email.mhpnsHelp": "Télécharger <a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target=\"_blank\">Mattermost iOS app</a> Depuis iTunes. Télécharger <a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target=\"_blank\">Mattermost Android app</a> Depuis Google Play. Apprenez en plus sur <a href=\"http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns\" target=\"_blank\">HPNS</a>.",
+ "admin.email.mtpns": "Utilisez iOS et Android sur iTunes et Google Play avec TPNS",
+ "admin.email.mtpnsHelp": "Télécharger <a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target=\"_blank\">Mattermost iOS app</a> Depuis iTunes. Télécharger <a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target=\"_blank\">Mattermost Android app</a> Depuis Google Play. Apprenez en plus sur <a href=\"http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns\" target=\"_blank\">TPNS</a>.",
"admin.email.notificationDisplayDescription": "Afficher le nom du compte de messagerie utilisé lors de l'envoi de notifications par Mattermost.",
"admin.email.notificationDisplayExample": "Exemple : \"Notification Mattermost\", \"Système\", \"No-reply\"",
"admin.email.notificationDisplayTitle": "Nom affiché dans les notifications :",
"admin.email.notificationEmailDescription": "L'adresse électronique affichée sur le compte utilisé lors de l'envoi de notifications par courriel à Mattermost.",
"admin.email.notificationEmailExample": "Exemple : \"mattermost@yourcompany.com\", \"admin@yourcompany.com\"",
- "admin.email.notificationEmailTitle": "Notification par messagerie : ",
+ "admin.email.notificationEmailTitle": "Notification par messagerie :",
"admin.email.notificationsDescription": "En général, activé en production. Si activé, Mattermost essaye d'envoyer des notifications par courrier électronique. Les développeurs peuvent en revanche désactiver cette option pour gagner du temps.<br />Activer cette option retire la bannière \"Mode découverte\" (cela nécessite de se déconnecter puis se re-connecter après avoir activé l'option).",
"admin.email.notificationsTitle": "Envoyer des notifications par courriel : ",
"admin.email.passwordSaltDescription": "Clé de salage de 32 caractères utilisé pour générer la clé de réinitialisation du mot de passe. Générée aléatoirement à l'installation. Cliquez sur \"re-générer\" pour créer une nouvelle clé de salage.",
"admin.email.passwordSaltExample": "Exemple : \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
"admin.email.passwordSaltTitle": "Clé de salage de réinitialisation du mot de passe :",
- "admin.email.pushContentDesc": "Choisir \"Envoyer des descriptions générales avec les utilisateurs et les noms des canaux\" permet aux notifications push d'envoyer des messages sans détail, en incluant juste les noms d'utilisateurs et de canaux.<br /><br />Choisir \"Envoyer un extrait du message complet\" envoie des extraits des messages qui déclenchent les notifications, et peuvent inclure des informations confidentielles visibles sur le terminal des utilisateurs notifiés. Si votre serveur de notifications Push est en dehors de votre pare-feu, il est HAUTEMENT RECOMMANDÉ d'utiliser cette option uniquement avec le protocole \"https\".",
+ "admin.email.pushContentDesc": "Choisir \"Envoyer une description générale avec les noms des utilisateurs et des canaux\" permet aux notifications push d'envoyer des messages sans détail, en incluant juste les noms d'utilisateurs et de canaux.<br /><br />Choisir \"Envoyer un extrait du message complet\" envoie des extraits des messages qui déclenchent les notifications, et peuvent inclure des informations confidentielles visibles sur le terminal des utilisateurs notifiés. Si votre serveur de notifications Push est en dehors de votre pare-feu, il est HAUTEMENT RECOMMANDÉ d'utiliser cette option uniquement avec le protocole \"https\".",
"admin.email.pushContentTitle": "Contenu des notifications push :",
"admin.email.pushDesc": "En général activé en production. Si activé, Mattermost essaye d'envoyer à iOS et Android des notifications push.",
+ "admin.email.pushOff": "Ne pas envoyer de notifications push",
+ "admin.email.pushOffHelp": "Regardez <a href=\"http://docs.mattermost.com/deployment/push.html#push-notifications-and-mobile-devices\" target=\"_blank\">la documentation sur les notifications</a> pour en savoir plus sur les options de configurations.",
"admin.email.pushServerDesc": "Service de notification push utilisé par Mattermost. Vous pouvez l'utilisez derrière votre firewall avec https://github.com/mattermost/push-proxy. Pour vos tests, vous pouvez utiliser http://push-test.mattermost.com, qui se connecte à l'application iOS Mattermost de l'AppStore public. N'utilisez pas ce serveur de test pour vos déploiements en production !",
"admin.email.pushServerEx": "Exemple : \"http://push-test.mattermost.com\"",
"admin.email.pushServerTitle": "Serveur de notifications push :",
- "admin.email.pushTitle": "Envoyer des notifications push :",
- "admin.email.regenerate": "Générer de nouveau",
+ "admin.email.pushTitle": "Envoyer des notifications push : ",
"admin.email.requireVerificationDescription": "En général activé en production. Si activé, Mattermost impose une vérification de l'adresse électronique avant d'autoriser la connexion. Vous pouvez désactiver cette option en développement.",
- "admin.email.requireVerificationTitle": "Imposer la vérification de l'adresse électronique :",
- "admin.email.save": "Enregistrer",
- "admin.email.saving": "Enregistrement des paramètres...",
+ "admin.email.requireVerificationTitle": "Imposer la vérification de l'adresse électronique : ",
+ "admin.email.selfPush": "Configuration manuel du service de notification Push",
"admin.email.smtpPasswordDescription": " Récupérez ces informations de la part de l'administrateur de votre serveur de mails.",
"admin.email.smtpPasswordExample": "Exemple : \"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.email.smtpPasswordTitle": "Mot de passe SMTP :",
@@ -130,27 +178,23 @@
"admin.email.smtpUsernameExample": "Exemple : \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
"admin.email.smtpUsernameTitle": "Nom d'utilisateur SMTP :",
"admin.email.testing": "Essai en cours...",
- "admin.email.true": "vrai",
+ "admin.false": "false",
"admin.gitab.clientSecretDescription": "Récupérer cette information depuis les instructions ci-dessus pour vous connecter à GitLab.",
- "admin.gitlab.EnableHtmlDesc": "<ol><li>Connectez vous à votre compte GitLab et allez dans les réglages de votre profil, puis dans \"Applications\".</li><li>Saisissez les URIs de redirection \"<your-mattermost-url>/login/gitlab/complete\" (exemple: http://localhost:8065/login/gitlab/complete) et \"<your-mattermost-url>/signup/gitlab/complete\". </li><li>Puis utilisez les champs \"Secret\" et \"Id\" de GitLab pour compléter les options ci-dessous.</li><li>Complétez les URL de fin de parcours (Endpoint) ci-dessous. </li></ol>",
- "admin.gitlab.authDescription": "Saissez https://<your-gitlab-url>/oauth/authorize (par exemple https://example.com:3000/oauth/authorize). Vérifiez si vous utilisez HTTP ou HTTPS que votre URL est correctement saisie.",
+ "admin.gitlab.EnableHtmlDesc": "<ol><li>Connectez vous à votre compte GitLab et allez dans les réglages de votre profil, puis dans \"Applications\".</li><li>Saisissez les URLs de redirection \"<your-mattermost-url>/login/gitlab/complete\" (exemple: http://localhost:8065/login/gitlab/complete) et \"<your-mattermost-url>/signup/gitlab/complete\". </li><li>Puis utilisez les champs \"Secret\" et \"Id\" de GitLab pour compléter les options ci-dessous.</li><li>Complétez les URL de fin de parcours (Endpoint) ci-dessous. </li></ol>",
+ "admin.gitlab.authDescription": "Saissez https://<your-gitlab-url>/oauth/authorize (par exemple https://exemple.com:3000/oauth/authorize). Vérifiez si vous utilisez HTTP ou HTTPS que votre URL est correctement saisie.",
"admin.gitlab.authExample": "Exemple : \"\"",
"admin.gitlab.authTitle": "URL d'authentification (auth endpoint) :",
"admin.gitlab.clientIdDescription": "Récupérez cette information depuis les informations ci-dessus pour vous connecter à GitLab",
"admin.gitlab.clientIdExample": "Exemple : \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
- "admin.gitlab.clientIdTitle": "Identifiant :",
+ "admin.gitlab.clientIdTitle": "Identifiant :",
"admin.gitlab.clientSecretExample": "Exemple : \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.gitlab.clientSecretTitle": "Secret :",
- "admin.gitlab.enableDescription": "Si vrai, Mattermost autorise la création d'une équipe et l'inscription avec le service OAuth de GitLab.",
+ "admin.gitlab.enableDescription": "Si activé, Mattermost autorise la création d'une équipe et l'inscription avec le service OAuth de GitLab.",
"admin.gitlab.enableTitle": "Autoriser l'inscription avec GitLab :",
- "admin.gitlab.false": "non",
- "admin.gitlab.save": "Enregistrer",
- "admin.gitlab.saving": "Enregistrement des paramètres...",
"admin.gitlab.settingsTitle": "Configuration de GitLab",
"admin.gitlab.tokenDescription": "Saisissez https://<your-gitlab-url>/oauth/token. Veillez à saisir HTTP ou HTTPS dans l'URL suivant votre configuration.",
"admin.gitlab.tokenExample": "Exemple : \"\"",
"admin.gitlab.tokenTitle": "URL du jeton (token endpoint) :",
- "admin.gitlab.true": "vrai",
"admin.gitlab.userDescription": "Saisissez https://<your-gitlab-url>/api/v3/user. Veillez à saisir HTTP ou HTTPS dans l'URL suivant votre configuration.",
"admin.gitlab.userExample": "Exemple : \"\"",
"admin.gitlab.userTitle": "URL de l'API (User API endpoint) :",
@@ -159,16 +203,14 @@
"admin.image.amazonS3BucketTitle": "Bucket S3 Amazon :",
"admin.image.amazonS3IdDescription": "Demandez cette information à votre administrateur AWS.",
"admin.image.amazonS3IdExample": "Exemple : \"AKIADTOVBGERKLCBV\"",
- "admin.image.amazonS3IdTitle": "Amazon S3 Access Key Id :",
+ "admin.image.amazonS3IdTitle": "Access Key ID dAmazon S3 :",
"admin.image.amazonS3RegionDescription": "Région AWS dans laquelle votre bucket S3 est hébergé.",
"admin.image.amazonS3RegionExample": "Exemple : \"us-east-1\"",
"admin.image.amazonS3RegionTitle": "Région AWS S3 :",
"admin.image.amazonS3SecretDescription": "Demandez cette information à votre administrateur AWS.",
"admin.image.amazonS3SecretExample": "Exemple : \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.image.amazonS3SecretTitle": "Secret Access Key d'Amazon S3 :",
- "admin.image.false": "non",
- "admin.image.fileSettings": "Configuration du fichier",
- "admin.image.localDescription": "Répertoire des rapports de conformité. Si non spécifié : ./data/ .",
+ "admin.image.localDescription": "Répertoire des rapports de conformité. Si non spécifié : ./data/ .",
"admin.image.localExample": "Ex : \"./data/\"",
"admin.image.localTitle": "Emplacement du répertoire local :",
"admin.image.previewHeightDescription": "Hauteur maxi de l'aperçu d'image (\"0\" pour taille automatique). Mettre cette valeur à jour change la manière dont les aperçus d'image seront générés par la suite, mais ne modifie pas les aperçus déjà générés.",
@@ -178,7 +220,7 @@
"admin.image.previewWidthExample": "Exemple : \"1024\"",
"admin.image.previewWidthTitle": "Largeur d'aperçu :",
"admin.image.profileHeightDescription": "Hauteur de la photo de profil",
- "admin.image.profileHeightExample": "Exemple : \"\"",
+ "admin.image.profileHeightExample": "Exemple : \"0\"",
"admin.image.profileHeightTitle": "Hauteur :",
"admin.image.profileWidthDescription": "Largeur de la photo de profil.",
"admin.image.profileWidthExample": "Exemple : \"1024\"",
@@ -186,11 +228,8 @@
"admin.image.publicLinkDescription": "Clé de salage de 32 caractères ajoutée aux courriels d'invitation. Générée aléatoirement lors de l'installation. Cliquez sur \"Regénérer\" pour créer une nouvelle clé de salage.",
"admin.image.publicLinkExample": "Exemple : \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
"admin.image.publicLinkTitle": "Clé de salage publique :",
- "admin.image.regenerate": "Générer de nouveau",
- "admin.image.save": "Enregistrer",
- "admin.image.saving": "Enregistrement des paramètres...",
"admin.image.shareDescription": "Permettre aux utilisateurs de partager des liens et images publics.",
- "admin.image.shareTitle": "Partager liens publics :",
+ "admin.image.shareTitle": "Partager liens publics : ",
"admin.image.storeAmazonS3": "Amazon S3",
"admin.image.storeLocal": "Disque local",
"admin.image.storeTitle": "Enregistrer les fichiers dans :",
@@ -200,9 +239,8 @@
"admin.image.thumbWidthDescription": "Largeur des vignettes générées pour les images. Changer cette valeur ne modifie pas les vignettes de l'historique.",
"admin.image.thumbWidthExample": "Exemple : \"120\"",
"admin.image.thumbWidthTitle": "Largeur des vignettes :",
- "admin.image.true": "vrai",
"admin.ldap.bannerDesc": "Si une propriété de l'utilisateur est modifiée sur le serveur LDAP, elle sera mise à jour la prochaine fois que l'utilisateur se loguera à Mattermost. Cela concerne également le fait de désactiver ou supprimer un utilisateur du serveur LDAP. La synchronisation continue sera disponible ultérieurement.",
- "admin.ldap.bannerHeading": "Remarque :",
+ "admin.ldap.bannerHeading": "Remarque :",
"admin.ldap.baseDesc": "Le DN de base est le Distinguished Name à partir du quel Mattermost doit rechercher les utilisateurs dans l'arborescence LDAP.",
"admin.ldap.baseEx": "Exemple : \"ou=Unit Name, dc=example, dc=com\"",
"admin.ldap.baseTitle": "DN de base :",
@@ -215,50 +253,58 @@
"admin.ldap.emailAttrTitle": "Attribut \"adresse électronique\" :",
"admin.ldap.enableDesc": "Si activé, Mattermost permet de s'authentifier avec le serveur LDAP",
"admin.ldap.enableTitle": "Activer la connexion avec LDAP :",
- "admin.ldap.false": "faux",
"admin.ldap.firstnameAttrDesc": "Attribut du serveur LDAP utilisé pour le champ \"prénom\" des utilisateurs dans Mattermost.",
"admin.ldap.firstnameAttrEx": "Exemple : \"givenName\"",
"admin.ldap.firstnameAttrTitle": "Attribut prénom",
"admin.ldap.idAttrDesc": "Attribut du serveur LDAP utilisé comme identifiant unique d'utilisateur dans Mattermost. Cela doit être un attribut dont la valeur ne change pas, tel que le username ou un uid. Si cette valeur change dans l'annuaire LDAP, un nouveau compte Mattermost sera créé avec l'ancien utilisateur. Il s'agit de la valeur à saisir dans le champ \"Utilisateur LDAP\" de la page de connexion. Normalement cet attribut est le même que celui du \"nom d'utilisateur\" ci-dessus. Si votre équipe utilise domain\\\\username pour se connecter à d'autres services avec LDAP, vous pouvez utiliser domain\\\\username pour rester cohérent.",
"admin.ldap.idAttrEx": "Exemple : \"sAMAccountName\"",
- "admin.ldap.idAttrTitle": "Attribut id :",
+ "admin.ldap.idAttrTitle": "Attribut id : ",
"admin.ldap.lastnameAttrDesc": "Attribut du serveur LDAP utilisé pour le champ \"nom de famille\" des utilisateurs dans Mattermost.",
"admin.ldap.lastnameAttrEx": "Exemple : \"sn\"",
"admin.ldap.lastnameAttrTitle": "Attribut \"nom de famille\" :",
- "admin.ldap.noLicense": "<h4 class=\"banner__heading\">Note :</h4><p>LDAP est une fonctionnalité de l'édition Enterprise. Votre license actuelle ne permet pas d'utiliser LDAP. Voir <a href=\"http://mattermost.com\" target=\"_blank\">ici</a> pour plus d'informations et tarifs sur l'édition Enterprise.</p>",
+ "admin.ldap.loginNameDesc": "L’espace texte réservé apparaît dans le champ de connexion sur la page de connexion. Par défaut \"LDAP Nom d'utilisateur\".",
+ "admin.ldap.loginNameEx": "Exemple : \"LDAP nom d’utilisateur\".",
+ "admin.ldap.loginNameTitle": "Champ de connexion :",
+ "admin.ldap.nicknameAttrDesc": "(Optionnel) Attribut du serveur LDAP utilisé pour le champ \"nom de famille\" des utilisateurs dans Mattermost.",
+ "admin.ldap.nicknameAttrEx": "Exemple : \"surnom\"",
+ "admin.ldap.nicknameAttrTitle": "Attribut \"nom d'utilisateur\" :",
+ "admin.ldap.noLicense": "<h4 class=\"banner__heading\">Note :</h4><p>LDAP est une fonctionnalitée de l'édition d’Enterprise. Votre licence actuelle ne permet pas d'utiliser LDAP. Voir <a href=\"http://mattermost.com\" target=\"_blank\">ici</a> pour plus d'informations et tarifs sur l'édition d’Enterprise.</p>",
"admin.ldap.portDesc": "Le port utilisé par Mattermost pour vous connecter au serveur LDAP. Par défaut : 389.",
"admin.ldap.portEx": "Exemple : \"389\"",
"admin.ldap.portTitle": "Port du serveur LDAP :",
"admin.ldap.queryDesc": "Valeur de timeout en secondes pour les requêtes au serveur LDAP. Augmentez cette valeur si votre serveur LDAP est lent.",
"admin.ldap.queryEx": "Exemple : \"60\"",
"admin.ldap.queryTitle": "Timeout (en secondes) :",
- "admin.ldap.save": "Enregistrer",
- "admin.ldap.saving": "Enregistrement des paramètres...",
"admin.ldap.serverDesc": "Nom DNS ou adresse IP du serveur LDAP.",
"admin.ldap.serverEx": "Exemple : \"10.0.0.23\"",
"admin.ldap.serverTitle": "Serveur LDAP :",
- "admin.ldap.title": "Paramètres LDAP",
- "admin.ldap.true": "vrai",
+ "admin.ldap.skipCertificateVerification": "Passer la vérification",
+ "admin.ldap.skipCertificateVerificationDesc": "Saute l'étape de vérification des certificats pour les connexions TLS ou STARTTLS . Non recommandé pour les environnements de production où TLS est nécessaire. Pour tester seulement.",
"admin.ldap.uernameAttrDesc": "Attribut du serveur LDAP utilisé pour le champ \"nom d'utilisateur\" dans Mattermost. Utilisez de préférence le même champ que l'attribut \"identifiant d'utilisateur\".",
+ "admin.ldap.userFilterDisc": "Entrez éventuellement un filtre LDAP à utiliser lors de la recherche d'objets utilisateur. Seuls les utilisateurs sélectionnés par la requête seront en mesure d'accéder à Mattermost . Pour Active Directory, la requête pour filtrer les utilisateurs désactivés est (&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).",
+ "admin.ldap.userFilterEx": "Exemple : \"(objectClass=utilisateur)\"",
+ "admin.ldap.userFilterTitle": "Filtre utilisateur :",
"admin.ldap.usernameAttrEx": "Exemple : \"sAMAccountName\"",
"admin.ldap.usernameAttrTitle": "Attribut \"nom d'utilisateur\" :",
+ "admin.license.choose": "Parcourir",
"admin.license.chooseFile": "Parcourir",
- "admin.license.keyRemove": "Remove Enterprise License and Downgrade Server",
+ "admin.license.edition": "Edition: ",
+ "admin.license.key": "Clé de licence: ",
+ "admin.license.keyRemove": "Supprimer la Licence Enterprise et rétrograder le serveur",
"admin.license.noFile": "Aucun fichier chargé",
- "admin.license.removing": "Suppression de la license...",
+ "admin.license.removing": "Suppression de la licence...",
"admin.license.title": "Édition et licence",
- "admin.license.type": "Licence :",
+ "admin.license.type": "Licence : ",
"admin.license.upload": "Télécharger",
"admin.license.uploadDesc": "Téléchargez une licence de Mattermost pour mettre à jour ce serveur. Consultez <a href=\"http://mattermost.com\" target=\"_blank\">Mattermoste</a> pour plus d'informations.",
- "admin.license.uploading": "Téléchargement de la license...",
- "admin.log.consoleDescription": "En principe désactivé en production. Utilisé pour le développement, pour afficher les messages d'information sur la console (stdout).",
- "admin.log.consoleTitle": "Afficher les messages sur la console :",
- "admin.log.false": "non",
+ "admin.license.uploading": "Téléchargement de la licence…",
+ "admin.log.consoleDescription": "En principe désactivée en production. Utilisé pour le développement, pour afficher les messages d'information sur la console (stdout).",
+ "admin.log.consoleTitle": "Afficher les messages sur la console : ",
"admin.log.fileDescription": "En principe activé en production. Si activé, les journaux sont écrits dans le fichier spécifié ci-dessous.",
"admin.log.fileLevelDescription": "Ce paramètre indique le niveau de détail des journaux. ERROR : N'enregistre que les messages d'erreur. INFO : Affiche les erreurs et des informations sur le démarrage et l'initialisation du serveur. DEBUG : Affiche des informations utiles aux développeurs.",
"admin.log.fileLevelTitle": "Niveau de détail des journaux :",
- "admin.log.fileTitle": "Enregistrer le journal dans un fichier :",
- "admin.log.formatDateLong": "Date (2006/01/22)",
+ "admin.log.fileTitle": "Enregistrer le journal dans un fichier : ",
+ "admin.log.formatDateLong": "Date (2006/01/02)",
"admin.log.formatDateShort": "Date (21/02/06)",
"admin.log.formatDescription": "Format des journaux d'erreur. Si vide, sera \"[%D %T] [%L] %M\", où :",
"admin.log.formatLevel": "Niveau (DEBG, INFO, EROR)",
@@ -266,34 +312,25 @@
"admin.log.formatPlaceholder": "Entrez le format du fichier",
"admin.log.formatSource": "Source",
"admin.log.formatTime": "Heure (15:04:05 MST)",
- "admin.log.formatTitle": "Format de fichier :",
+ "admin.log.formatTitle": "Format de fichier :",
"admin.log.levelDescription": "Ce paramètre détermine le niveau de détail des événements du journal affichés sur la console. ERROR : Affiche seulement les erreurs. INFO : Affiche les messages d'erreur ainsi que des informations sur le démarrage et l'initialisation du serveur. DEBUG : Affiche un haut niveau de détail pour les développeurs.",
"admin.log.levelTitle": "Niveau de détail affiché sur la console :",
"admin.log.locationDescription": "Fichier des journaux. Si vide, les journaux seront enregistrés dans \"./logs/mattermost.log\", avec une rotation toutes les 10000 lignes.",
"admin.log.locationPlaceholder": "Saisir l'emplacement du fichier",
- "admin.log.locationTitle": "Emplacement du fichier :",
+ "admin.log.locationTitle": "Emplacement du fichier :",
"admin.log.logSettings": "Configuration du journal (log)",
- "admin.log.save": "Enregistrer",
- "admin.log.saving": "Enregistrement des paramètres...",
- "admin.log.true": "vrai",
"admin.logs.reload": "Recharger",
"admin.logs.title": "Journaux du serveur",
"admin.nav.help": "Aide",
"admin.nav.logout": "Se déconnecter",
"admin.nav.report": "Signaler un problème",
- "admin.nav.switch": "Aller sur {display_name}",
- "admin.privacy.false": "faux",
- "admin.privacy.save": "Enregistrer",
- "admin.privacy.saving": "Enregistrement des paramètres...",
+ "admin.nav.switch": "Team Selection",
"admin.privacy.showEmailDescription": "Si désactivé, masque les adresses électronique des utilisateurs dans l'interface, y compris pour les responsables d'équipe. Cette option est pratique quand Mattermost est utilisé pour gérer des équipes dans lesquelles les utilisateurs préfèrent garder leurs informations privées.",
- "admin.privacy.showEmailTitle": "Afficher l'adresse électronique :",
+ "admin.privacy.showEmailTitle": "Afficher l'adresse électronique : ",
"admin.privacy.showFullNameDescription": "Si désactivé, les utilisateurs ne peuvent voir le nom complet des autres utilisateurs (y compris le propriétaire de l'équipe et les adminsitrateurs). Le nom d'utilisateur est affiché à la place du nom de la personne.",
- "admin.privacy.showFullNameTitle": "Afficher le nom complet :",
- "admin.privacy.title": "Paramètres de confidentialité",
- "admin.privacy.true": "vrai",
+ "admin.privacy.showFullNameTitle": "Afficher le nom complet : ",
"admin.rate.enableLimiterDescription": "Si activé, les APIs sont limitées comme spécifié ci-dessous.",
- "admin.rate.enableLimiterTitle": "Limiter l'accès aux API :",
- "admin.rate.false": "faux",
+ "admin.rate.enableLimiterTitle": "Limiter l'accès aux API : ",
"admin.rate.httpHeaderDescription": "Quand ce champ est rempli, les flux des requêtes sont limités par l'en-tête HTTP spécifié (par exemple \"X-Real-IP\" avec Nginx, \"X-Forwarded-For\" pour AmazonELB)",
"admin.rate.httpHeaderExample": "Exemple : \"X-Real-IP\", \"X-Forwarded-For\"",
"admin.rate.httpHeaderTitle": "En-tête HTTP Vary By :",
@@ -301,21 +338,25 @@
"admin.rate.memoryExample": "Exemple : \"10000\"",
"admin.rate.memoryTitle": "Taille du stockage en mémoire :",
"admin.rate.noteDescription": "Modifier des paramètres dans cette section nécessite un redémarrage du serveur.",
- "admin.rate.noteTitle": "Remarque :",
+ "admin.rate.noteTitle": "Remarque :",
"admin.rate.queriesDescription": "Limite l'API à ce nombre de requêtes par seconde.",
"admin.rate.queriesExample": "Exemple : \"10\"",
"admin.rate.queriesTitle": "Nombre de requêtes par seconde :",
"admin.rate.remoteDescription": "Si activé, l'API est limitée au moyen de l'adresse IP du client.",
- "admin.rate.remoteTitle": "Adresse distante Vary By :",
- "admin.rate.save": "Enregistrer",
- "admin.rate.saving": "Enregistrement des paramètres...",
- "admin.rate.title": "Configuration des limites de débit",
- "admin.rate.true": "vrai",
+ "admin.rate.remoteTitle": "Adresse distante Vary By : ",
+ "admin.recycle.button": "Recycle Database Connections",
+ "admin.recycle.loading": " Recycling...",
+ "admin.recycle.reloadFail": "Recycling unsuccessful: {error}",
+ "admin.regenerate": "Re-Generate",
+ "admin.reload.button": "Reload Configuration From Disk",
+ "admin.reload.loading": " Loading...",
+ "admin.reload.reloadFail": "Reloading unsuccessful: {error}",
"admin.reset_password.close": "Quitter",
"admin.reset_password.newPassword": "Nouveau mot de passe",
"admin.reset_password.select": "Sélectionner",
"admin.reset_password.submit": "Veuillez saisir au moins {chars} caractères.",
- "admin.reset_password.title": "Réinitialiser le mot de passe",
+ "admin.reset_password.titleReset": "Réinitialiser le mot de passe",
+ "admin.reset_password.titleSwitch": "Basculez vers l’addresse électronique / mot de passe de votre compte",
"admin.select_team.close": "Quitter",
"admin.select_team.select": "Sélectionner",
"admin.select_team.selectTeam": "Choisir une équipe",
@@ -323,35 +364,34 @@
"admin.service.attemptExample": "Exemple : \"10\"",
"admin.service.attemptTitle": "Nombre maximum de tentatives de connexion :",
"admin.service.cmdsDesc": "Si activé, les utilisateurs peuvent créer des commandes slash.",
- "admin.service.cmdsTitle": "Activer les commandes slash :",
+ "admin.service.cmdsTitle": "Activer les commandes slash : ",
"admin.service.corsDescription": "Autoriser les requête HTTP cross-origin depuis des domaines spécifiques (séparés par des espaces). Utilisez \"*\" pour autoriser les CORS pour n'importe quel domaine (pas recommandé) ou laissez vide pour les désactiver.",
- "admin.service.corsEx": "http://example.com https://example.com",
+ "admin.service.corsEx": "http://exemple.com ou https://exemple.com",
"admin.service.corsTitle": "Autoriser les requêtes cross-origin depuis :",
"admin.service.developerDesc": "(Option de développement) Si activé, des informations complémentaires sur les erreurs sont affichées dans l'interface.",
- "admin.service.developerTitle": "Activer le mode développeur :",
- "admin.service.false": "faux",
+ "admin.service.developerTitle": "Activer le mode développeur : ",
"admin.service.googleDescription": "Saisissez cette clé pour permettre l'intégration de vidéos YouTube et d'aperçus basés sur les liens postés dans les messages ou commentaires. Pour obtenir cette clé, rendez-vous sur <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Laissez le champ vide pour désactiver la génération automatique des vignettes YouTube.",
"admin.service.googleExample": "Exemple : \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"",
"admin.service.googleTitle": "Clé développeur Google :",
- "admin.service.iconDescription": "Si activé, les webhooks et commandes slash pourront changer l'icône avec lequel ils postent. Attention, si vous autorisez également à changer le nom d'utilisateur, cela augmente le risque d'une attaque par hameçonnage.",
- "admin.service.iconTitle": "Autoriser le remplacement de l'icône par les webhooks et les commandes slash :",
+ "admin.service.iconDescription": "Si activé, les webhooks et commandes slash pourront changer l'icône avec lequel ils postent. Attention, si vous autorisez également le changer du nom d'utilisateur, cela augmente le risque d'une attaque par hameçonnage.",
+ "admin.service.iconTitle": "Autoriser le remplacement de l'icône par les webhooks et les commandes slash : ",
"admin.service.insecureTlsDesc": "Si activé, toute requête sortante HTTPS acceptera les certificats non-vérifiés et auto-signés. Par exemple, les requêtes webhook sortantes avec un certificat TLS auto-signé, vers n'importe quel domaine, seront autorisées. Attention, cela rend votre serveur vulnérable aux attaques de type \"man in the middle\".",
- "admin.service.insecureTlsTitle": "Autoriser les connexions sortantes non-sécurisées :",
- "admin.service.integrationAdmin": "Autoriser les intégrations seulement pour les administrateurs :",
+ "admin.service.insecureTlsTitle": "Autoriser les connexions sortantes non-sécurisées : ",
+ "admin.service.integrationAdmin": "Autoriser les intégrations seulement pour les administrateurs : ",
"admin.service.integrationAdminDesc": "Si activé, les intégrations ne peuvent être configurées que par les administrateurs.",
- "admin.service.listenAddress": "Adresse IP du serveur :",
+ "admin.service.listenAddress": "Adresse IP du serveur :",
"admin.service.listenDescription": "Adresse IP à laquelle le serveur écoute. Saisir \":8065\" connectera le serveur depuis toutes les interfaces réseau de la machine. Vous pouvez également choisir une adresse, par exemple \"127.0.0.1:8065\". Ce paramètre nécessite un redémarrage du serveur.",
"admin.service.listenExample": "Exemple : \":8065\"",
+ "admin.service.mfaDesc": "Si activé, Les utilisateurs auront la possibilité d'ajouter l'authentification multi- facteur à leur compte . Ils auront besoin d'un smartphone et une application d'authentification tels que Google Authenticator .",
+ "admin.service.mfaTitle": "Activité l’authentification multi-facteurs:",
"admin.service.mobileSessionDays": "Durée de session pour les appareils mobiles (en jours) :",
"admin.service.mobileSessionDaysDesc": "La session d'un appareil mobile expirera après le nombre de jours indiqué et nécessitera une reconnexion de l'utilisateur.",
"admin.service.outWebhooksDesc": "Si activé, les webhooks sortants sont autorisés.",
- "admin.service.outWebhooksTitle": "Activer les webhooks sortants :",
+ "admin.service.outWebhooksTitle": "Activer les webhooks sortants : ",
"admin.service.overrideDescription": "Si activé, les webhooks et commandes slash peuvent changer le nom d'utilisateur avec lequel ils postent. Attention, si le changement d'icône est également autorisé, cela peut conduire à des attaques de type hameçonnage !",
- "admin.service.overrideTitle": "Autoriser les webhooks et commandes slash à changer leur nom d'utilisateur :",
- "admin.service.save": "Enregistrer",
- "admin.service.saving": "Enregistrement des paramètres...",
+ "admin.service.overrideTitle": "Autoriser les webhooks et commandes slash à changer leur nom d'utilisateur : ",
"admin.service.securityDesc": "Si activé, les administrateurs système sont notifiés par courriel si une alerte de sécurité a été annoncée ces 12 dernières heures. La messagerie doit être activée.",
- "admin.service.securityTitle": "Activer les alertes de sécurité :",
+ "admin.service.securityTitle": "Activer les alertes de sécurité : ",
"admin.service.segmentDescription": "Pour les utilisateurs d'un service SaaS, inscrivez-vous sur segment.com pour analyser le trafic.",
"admin.service.segmentExample": "Exemple : \"g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs\"",
"admin.service.segmentTitle": "Clé développeur Segment.com :",
@@ -361,61 +401,70 @@
"admin.service.ssoSessionDays": "Durée des sessions SSO en jours :",
"admin.service.ssoSessionDaysDesc": "Les sessions SSO expireront après le nombre de jours indiqué et nécessiteront une reconnexion de l'utilisateur.",
"admin.service.testingDescription": "(Option de développement) Si activé, la commande \"/loadtest\" est active et charge des données de test. Changer ce paramètre nécessite de redémarrer le serveur.",
- "admin.service.testingTitle": "Activer le mode test :",
- "admin.service.title": "Configuration du service",
- "admin.service.true": "vrai",
+ "admin.service.testingTitle": "Activer le mode test : ",
"admin.service.webSessionDays": "Durée de session pour les navigateurs web en jours :",
"admin.service.webSessionDaysDesc": "Les sessions web expireront après le nombre de jours indiqué et nécessiteront une reconnexion de l'utilisateur.",
"admin.service.webhooksDescription": "Si activé, les webhooks entrants ont autorisés. Pour éviter le hameçonnage, tous les messages provenant de webhooks sont marqués avec l'étiquette \"BOT\".",
- "admin.service.webhooksTitle": "Activer les webhooks entrants :",
+ "admin.service.webhooksTitle": "Activer les webhooks entrants : ",
"admin.sidebar.addTeamSidebar": "Afficher l'équipe dans le menu",
"admin.sidebar.audits": "Conformité et vérification",
- "admin.sidebar.compliance": "Paramètres de conformité",
- "admin.sidebar.email": "Configuration messagerie",
- "admin.sidebar.file": "Configuration du fichier",
- "admin.sidebar.gitlab": "Configuration de GitLab",
- "admin.sidebar.ldap": "Paramètres LDAP",
+ "admin.sidebar.authentication": "authentification",
+ "admin.sidebar.compliance": "Conformité",
+ "admin.sidebar.configuration": "configuration",
+ "admin.sidebar.connections": "Connexions",
+ "admin.sidebar.customBrand": "Image de marque personnalisée",
+ "admin.sidebar.customization": "Personnalisation",
+ "admin.sidebar.database": "Base de données",
+ "admin.sidebar.developer": "Développeur",
+ "admin.sidebar.email": "Adresse électronique",
+ "admin.sidebar.external": "Services externes",
+ "admin.sidebar.files": "Ficher",
+ "admin.sidebar.general": "Général",
+ "admin.sidebar.gitlab": "GitLab",
+ "admin.sidebar.images": "Images",
+ "admin.sidebar.integrations": "Intégrations",
+ "admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Édition et licence",
- "admin.sidebar.loading": "Chargement",
- "admin.sidebar.log": "Configuration du journal (log)",
+ "admin.sidebar.logging": "Enregistrement",
+ "admin.sidebar.login": "S’identifier",
"admin.sidebar.logs": "Journaux",
+ "admin.sidebar.notifications": "Notifications",
"admin.sidebar.other": "AUTRES",
- "admin.sidebar.privacy": "Paramètres de confidentialité",
- "admin.sidebar.rate_limit": "Configuration des limites de débits",
+ "admin.sidebar.privacy": "Confidentialité",
+ "admin.sidebar.publicLinks": "Liens publics",
+ "admin.sidebar.push": "notifications Push",
+ "admin.sidebar.rateLimiting": "Limitation de débit",
"admin.sidebar.reports": "RAPPORTS DU SITE",
"admin.sidebar.rmTeamSidebar": "Ne plus afficher l'équipe dans le menu",
- "admin.sidebar.service": "Configuration du service",
+ "admin.sidebar.security": "Sécurité",
+ "admin.sidebar.sessions": "sessions",
"admin.sidebar.settings": "REGLAGES",
- "admin.sidebar.sql": "Configuration SQL",
- "admin.sidebar.statistics": "- Statistiques",
- "admin.sidebar.support": "Paramètres légaux et support",
- "admin.sidebar.team": "Configuration de l'équipe",
- "admin.sidebar.teams": "ÉQUIPES ({count})",
- "admin.sidebar.users": "- Utilisateurs",
+ "admin.sidebar.sign_up": "S’inscrire",
+ "admin.sidebar.statistics": "Statistiques",
+ "admin.sidebar.storage": "Stockage",
+ "admin.sidebar.support": "Juridique et de soutien",
+ "admin.sidebar.teams": "Equipes ({count, number})",
+ "admin.sidebar.users": "Utilisateurs",
+ "admin.sidebar.usersAndTeams": "Utilisateur et équipes",
"admin.sidebar.view_statistics": "Voir les statistiques",
+ "admin.sidebar.webhooks": "Webhooks et commandes",
"admin.sidebarHeader.systemConsole": "Console système",
- "admin.sql.dataSource": "Source de données :",
+ "admin.sql.dataSource": "Source de données :",
"admin.sql.driverName": "Nom du pilote :",
- "admin.sql.false": "faux",
- "admin.sql.keyDescription": "Clé de salage de 32 caractères utilisée pour crypter et décrypter les champs en base de données.",
+ "admin.sql.keyDescription": "Clé de salage de 32 caractères utilisée pour chiffrer et déchiffrer les champs en base de données.",
"admin.sql.keyExample": "Exemple : \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
- "admin.sql.keyTitle": "Clé de cryptage des données au repos :",
+ "admin.sql.keyTitle": "Clé de chiffrement des données au repos :",
"admin.sql.maxConnectionsDescription": "Nombre maximum de connexion inactives à la base de données gardées ouvertes.",
"admin.sql.maxConnectionsExample": "Exemple : \"10\"",
"admin.sql.maxConnectionsTitle": "Nombre maximum de connexions inactives :",
"admin.sql.maxOpenDescription": "Nombre maximum de connexions ouvertes à la base de données.",
"admin.sql.maxOpenExample": "Exemple : \"10\"",
- "admin.sql.maxOpenTitle": "Nombre max. de connexions ouvertes :",
+ "admin.sql.maxOpenTitle": "Nombre max. de connexions ouvertes :",
"admin.sql.noteDescription": "Modifier ces paramètres nécessite de redémarrer le serveur.",
- "admin.sql.noteTitle": "Remarque :",
- "admin.sql.regenerate": "Générer de nouveau",
+ "admin.sql.noteTitle": "Remarque :",
"admin.sql.replicas": "Replicas de la base de données :",
- "admin.sql.save": "Enregistrer",
- "admin.sql.saving": "Enregistrement des paramètres...",
- "admin.sql.title": "Configuration SQL",
"admin.sql.traceDescription": "(Mode développeur) Si activé, toutes les commandes SQL sont enregistrées dans le journal.",
- "admin.sql.traceTitle": "Tracer :",
- "admin.sql.true": "vrai",
+ "admin.sql.traceTitle": "Tracer : ",
"admin.sql.warning": "Attention : Ré-générer cette clé de salage peut provoquer des valeurs vides dans certaines colones de la base de données.",
"admin.support.aboutDesc": "Faites un lien vers la page À propos pour plus d'informations sur votre déploiement Mattermost, par exemple son utilisation et son public dans votre organisation. Par défaut c'est un lien vers la page d'information de Mattermost.",
"admin.support.aboutTitle": "Lien \"À propos\" :",
@@ -424,49 +473,62 @@
"admin.support.helpDesc": "Lien vers la documentation utilisateur de l'équipe. Laissez cette valeur par défaut sauf si votre organisation souhaite créer elle-même cette documentation.",
"admin.support.helpTitle": "Lien d'aide :",
"admin.support.noteDescription": "Si vous faites un lien vers un site externe, les URLs doivent commencer par http:// ou https://.",
- "admin.support.noteTitle": "Remarque :",
+ "admin.support.noteTitle": "Remarque :",
"admin.support.privacyDesc": "Lien vers la Politique de Confidentialité pour les utilisateurs de la version bureau et mobile. Laissez cette option vide pour masquer le lien.",
"admin.support.privacyTitle": "Lien vers la politique de confidentialité :",
"admin.support.problemDesc": "Lien vers la documentation depuis le menu principal de l'équipe. Par défaut ce lien renvoie vers le forum d'entraide où les utilisateurs peuvent trouver de l'aide pour leurs problèmes techniques.",
"admin.support.problemTitle": "Lien \"Signaler un problème\" :",
- "admin.support.save": "Enregistrer",
- "admin.support.saving": "Enregistrement des paramètres...",
"admin.support.termsDesc": "Le lien \"Conditions d'utilisation\" est disponible aux utilisateur des versions bureau et mobile. Laissez cette option vide pour masquer l'affichage du lien.",
"admin.support.termsTitle": "Lien \"Conditions d'utilisation\" :",
- "admin.support.title": "Paramètres légaux et support",
"admin.system_analytics.activeUsers": "Utilisateurs actifs avec des messages",
"admin.system_analytics.title": "le Système",
"admin.system_analytics.totalPosts": "Nombre total de messages",
+ "admin.team.brandDesc": "Activer l'image nouvelle personnalisé pour afficher une image de votre choix , téléchargé ci-dessous , et un texte d'aide , écrit ci-dessous , sur la page de connexion .",
+ "admin.team.brandImageTitle": "Nouvelle image personnalisée :",
+ "admin.team.brandTextDescription": "Le texte de Markdown formaté marque personnalisée que vous souhaitez apparaître votre image de marque personnalisée ci-dessous sur votre écran de connexion .",
+ "admin.team.brandTextTitle": "Nouveau texte personnalisé",
+ "admin.team.brandTitle": "activer une image personnalisée : ",
+ "admin.team.chooseImage": "choisit une nouvelle image",
"admin.team.dirDesc": "Si activé, les équipes configurées pour apparaitre dans l'annuaire des équipes apparaitront sur la page d'accueil à la place de la création d'une nouvelle équipe.",
- "admin.team.dirTitle": "Activé l'annuaire des équipes :",
- "admin.team.false": "non",
+ "admin.team.dirTitle": "Activé l'annuaire des équipes : ",
"admin.team.maxUsersDescription": "Nombre maximum d'utilisateurs par équipe, actifs et inactifs.",
"admin.team.maxUsersExample": "Exemple : \"25\"",
"admin.team.maxUsersTitle": "Nombre max. d'utilisateurs par équipe :",
+ "admin.team.noBrandImage": "Pas de nouvelle image a télécharger",
+ "admin.team.openServerDescription": "Si activé, tout le monde peut s’enregistrer pour un compte d'utilisateur sur ce serveur sans qu'il soit nécessaire d'être invité.",
+ "admin.team.openServerTitle": "Activé le serveur ouvert: ",
"admin.team.restrictDescription": "Les équipes et comptes utilisateur ne peuvent être créés que depuis un domaine spécifique (par ex. \"mattermost.org\") ou une liste de domaines séparés par des virgules (ex \"corp.mattermost.com, mattermost.org\").",
+ "admin.team.restrictDirectMessage": "Permettre aux utilisateurs d'ouvrir des canaux de message avec :",
+ "admin.team.restrictDirectMessageDesc": "‘Tout utilisateur sur le serveur Mattermost' permet aux utilisateurs d' ouvrir un canal de message direct avec un utilisateur sur le serveur, même si elles ne sont pas sur les équipes. ‘Tout membre de l'équipe’ limite la capacité d'ouvrir des canaux de messages directs aux seuls utilisateurs qui sont dans la même équipe.",
"admin.team.restrictExample": "Exemple : \"corp.mattermost.com, mattermost.org\"",
"admin.team.restrictNameDesc": "Si activé, vous ne pourrez pas créer une équipe portant un nom réservé (comme www, admin, support, test, channel, etc).",
- "admin.team.restrictNameTitle": "Noms d'équipes restreints :",
+ "admin.team.restrictNameTitle": "Noms d'équipes restreints : ",
"admin.team.restrictTitle": "Restreindre la création aux domaines :",
- "admin.team.save": "Enregistrer",
- "admin.team.saving": "Enregistrement des paramètres...",
+ "admin.team.restrict_direct_message_any": "Tout utilisateur sur le serveur Mattermost",
+ "admin.team.restrict_direct_message_team": "Tout les membres de l’équipe",
"admin.team.siteNameDescription": "Nom du service affiché dans les écrans de connexion et l'interface.",
"admin.team.siteNameExample": "Exemple : \"Mattermost\"",
"admin.team.siteNameTitle": "Nom du site :",
"admin.team.teamCreationDescription": "Si désactivé, la création d'équipes est interdite.",
- "admin.team.teamCreationTitle": "Autoriser la création d'équipes :",
- "admin.team.title": "Configuration de l'équipe",
- "admin.team.true": "vrai",
+ "admin.team.teamCreationTitle": "Autoriser la création d'équipes : ",
+ "admin.team.upload": "Télécharger",
+ "admin.team.uploadDesc": "Personnalisez votre expérience en ajoutant une image personnalisée à votre écran de connexion . Voir les exemples à <a href='http://docs.mattermost.com/administration/config-settings.html#custom-branding' target='_blank'>docs.mattermost.com/administration/config-settings.html#custom-branding</a>.",
+ "admin.team.uploaded": "téléchargement terminé!",
+ "admin.team.uploading": "téléchargement..",
"admin.team.userCreationDescription": "Si désactivé, la possibilité de créer des comptes est désactivée.",
- "admin.team.userCreationTitle": "Autoriser la création de comptes :",
+ "admin.team.userCreationTitle": "Autoriser la création de comptes : ",
"admin.team_analytics.activeUsers": "Utilisateurs actifs avec messages",
"admin.team_analytics.totalPosts": "Nombre total de messages",
+ "admin.true": "true",
"admin.userList.title": "Utilisateurs de {team}",
"admin.userList.title2": "Utilisateurs de {team} ({count})",
+ "admin.user_item.authServiceEmail": ", <strong>Sign-in Method:</strong> Courriel",
+ "admin.user_item.authServiceNotEmail": ", <strong>Sign-in Method:</strong> {service}",
"admin.user_item.confirmDemoteDescription": "Si vous vous retirez le rôle d'administrateur et qu'il n'y a aucun autre administrateur désigné, vous devrez en désigner un en utilisant les outils en ligne de commande depuis un terminal sur le serveur.",
"admin.user_item.confirmDemoteRoleTitle": "Confirmez le retrait de votre rôle d'administrateur",
"admin.user_item.confirmDemotion": "Confirmer le retrait",
"admin.user_item.confirmDemotionCmd": "platform -assign_role -team_name=\"yourteam\" -email=\"name@yourcompany.com\" -role=\"system_admin\"",
+ "admin.user_item.emailTitle": "<strong>Email:</strong> {email}",
"admin.user_item.inactive": "Inactif",
"admin.user_item.makeActive": "Activer",
"admin.user_item.makeInactive": "Désactiver",
@@ -474,7 +536,11 @@
"admin.user_item.makeSysAdmin": "Assigner le rôle administrateur système",
"admin.user_item.makeTeamAdmin": "Assigner le rôle \"Administrateur d'équipe\"",
"admin.user_item.member": "Membre",
+ "admin.user_item.mfaNo": ", <strong>MFA</strong> : Non",
+ "admin.user_item.mfaYes": ", <strong>MFA</strong> : Oui",
+ "admin.user_item.resetMfa": "supprimer l’identification à facteurs-multiples",
"admin.user_item.resetPwd": "Réinitialiser le mot de passe",
+ "admin.user_item.switchToEmail": "Basculer vers votre adresse électronique / mot de passe",
"admin.user_item.sysAdmin": "Administrateur système",
"admin.user_item.teamAdmin": "Administrateur d'équipe",
"analytics.chart.loading": "Chargement…",
@@ -501,6 +567,7 @@
"analytics.team.privateGroups": "Groupes privés",
"analytics.team.publicChannels": "Canaux publics",
"analytics.team.recentActive": "Utilisateurs actifs récemment",
+ "analytics.team.recentUsers": "Utilisateurs actifs récemment",
"analytics.team.title": "Statistiques d'équipe pour {team}",
"analytics.team.totalPosts": "Nombre de messages",
"analytics.team.totalUsers": "Nombre d'utilisateurs",
@@ -523,7 +590,7 @@
"audit_table.establishedDM": "Envoyé un message privé avec {username}",
"audit_table.failedExpiredLicenseAdd": "Echec d'ajout d'une licence (elle a déjà expiré ou pas encore démarré)",
"audit_table.failedInvalidLicenseAdd": "Échec de l'ajout d'une licence",
- "audit_table.failedLogin": "Échec de la connexion",
+ "audit_table.failedLogin": "ÉCHEC de la connexion",
"audit_table.failedOAuthAccess": "Impossible d'autoriser un nouvel accès à un service OAuth - L'URI de redirection ne correspond pas à l'URI de callback déjà enregistrée",
"audit_table.failedPassword": "Impossible de modifier le mot de passe pour un utilisateur connecté via OAuth",
"audit_table.failedWebhookCreate": "Impossible de créer le webhook - autorisation insuffisante pour ce canal",
@@ -532,7 +599,7 @@
"audit_table.ip": "Adresse IP",
"audit_table.licenseRemoved": "Licence supprimée avec succès",
"audit_table.loginAttempt": " (Tentative de connexion)",
- "audit_table.loginFailure": "(Échec de la connexion)",
+ "audit_table.loginFailure": " (Échec de la connexion)",
"audit_table.logout": "Déconnecté de votre compte",
"audit_table.member": "membre",
"audit_table.nameUpdated": "Mis à jour le nom du canal/groupe {channelName}",
@@ -553,7 +620,7 @@
"audit_table.updateGeneral": "Les paramètres de votre compte ont été mis à jour",
"audit_table.updateGlobalNotifications": "Vos paramètres de notification ont été mis à jour",
"audit_table.updatePicture": "Mettre à jour votre photo de profil",
- "audit_table.updatedRol": "Mettre à jour les rôles à",
+ "audit_table.updatedRol": "Mettre à jour les rôles à ",
"audit_table.userAdded": "{username} ajouté au canal/groupe {channelName}",
"audit_table.userId": "Identifiant de l'utilisateur",
"audit_table.userRemoved": "{username} retiré du canal/groupe {channelName}",
@@ -563,8 +630,13 @@
"authorize.app": "L'application <strong>{appName}</strong> souhaite accéder et modifier vos informations de base.",
"authorize.deny": "Refuser",
"authorize.title": "Une application essayer de se connecter au compte de {teamName}",
- "center_panel.recent": "Cliquez ici pour voir les messages les plus récents.",
- "chanel_header.addMembers": "Ajouter des membres...",
+ "backstage_navbar.backToMattermost": "retour à {siteName}",
+ "backstage_sidebar.integrations": "Intégrations",
+ "backstage_sidebar.integrations.commands": "Commande Slash",
+ "backstage_sidebar.integrations.incoming_webhooks": "Webhooks entrants",
+ "backstage_sidebar.integrations.outgoing_webhooks": "Webhooks sortants",
+ "center_panel.recent": "Cliquez ici pour voir les messages les plus récents. ",
+ "chanel_header.addMembers": "Ajouter des membres",
"change_url.close": "Fermer",
"change_url.endWithLetter": "Doit se terminer par une lettre ou un nombre",
"change_url.invalidUrl": "URL non valide",
@@ -596,8 +668,8 @@
"channel_info.name": "Nom du canal :",
"channel_info.notFound": "Aucun canal",
"channel_info.purpose": "Description du canal :",
- "channel_info.url": "URL du canal : ",
- "channel_invite.add": "Ajouter",
+ "channel_info.url": "URL du canal :",
+ "channel_invite.add": " Ajouter",
"channel_invite.addNewMembers": "Ajouter de nouveaux membres à ",
"channel_invite.close": "Fermer",
"channel_loader.connection_error": "Oups... Il semble que votre connexion internet ait un problème...",
@@ -608,26 +680,26 @@
"channel_loader.unknown_error": "Réponse incorrecte de la part du serveur.",
"channel_loader.uploadedFile": " téléchargé un fichier",
"channel_loader.uploadedImage": " téléchargé une image",
- "channel_loader.wrote": " a écrit :",
- "channel_members_modal.addNew": "Ajouter de nouveaux membres",
+ "channel_loader.wrote": " a écrit : ",
+ "channel_members_modal.addNew": " Ajouter de nouveaux membres",
"channel_members_modal.close": "Fermer",
"channel_members_modal.remove": "Supprimer",
- "channel_memebers_modal.members": "Membres",
+ "channel_memebers_modal.members": " Membres",
"channel_modal.cancel": "Annuler",
"channel_modal.channel": "Canal",
"channel_modal.createNew": "Créer un(e) nouveau(elle) ",
- "channel_modal.descriptionHelp": "Décrivez comment {term} doit être utilisé.",
+ "channel_modal.descriptionHelp": "Décrivez comment ce {term} doit être utilisé.",
"channel_modal.displayNameError": "Ce champ est obligatoire",
"channel_modal.edit": "Modifier",
"channel_modal.group": "Groupe",
- "channel_modal.modalTitle": "Nouveau",
+ "channel_modal.modalTitle": "Nouveau ",
"channel_modal.name": "Nom",
"channel_modal.nameEx": "Exemple : \"Problèmes\", \"Marketing\", \"Éducation\"",
"channel_modal.optional": "(facultatif)",
- "channel_modal.privateGroup1": "Créer un nouveau groupe privé avec des membres restreints.",
+ "channel_modal.privateGroup1": "Créer un nouveau groupe privé avec des membres restreints. ",
"channel_modal.privateGroup2": "Créer un groupe privé",
"channel_modal.publicChannel1": "Créer un canal public",
- "channel_modal.publicChannel2": "Créer un canal public que tout le monde peut rejoindre.",
+ "channel_modal.publicChannel2": "Créer un canal public que tout le monde peut rejoindre. ",
"channel_modal.purpose": "Description",
"channel_notifications.allActivity": "Pour toute l'activité",
"channel_notifications.allUnread": "Pour les messages non-lus",
@@ -639,6 +711,7 @@
"channel_notifications.preferences": "Préférences de notification pour ",
"channel_notifications.sendDesktop": "Envoyer des notifications sur le bureau",
"channel_notifications.unreadInfo": "Le nom du canal est en gras dans la barre latérale lorsqu'il y a des messages non-plus. Choisir \"Seulement pour les mentions\" mettra en gras le canal seulement si vous être mentionné.",
+ "channel_select.placeholder": "--- Sélectionnez un canal ---",
"choose_auth_page.emailCreate": "Créer une nouvelle équipe avec une adresse électronique",
"choose_auth_page.find": "Trouver mes équipes",
"choose_auth_page.gitlabCreate": "Créer une nouvelle équipe avec un compte GitLab",
@@ -667,6 +740,7 @@
"claim.email_to_oauth.title": "Changer l'adresse électronique/mot de passe pour {uiType}",
"claim.ldap_to_email.confirm": "Confirmer le mot de passe",
"claim.ldap_to_email.email": "Vous devrez utiliser l'adresse électronique {email} pour vous connecter.",
+ "claim.ldap_to_email.enterLdapPwd": "Saisissez votre {ldapPassword} pour votre compte {site}",
"claim.ldap_to_email.enterPwd": "Saisissez un nouveau mot de passe pour votre compte",
"claim.ldap_to_email.ldapPasswordError": "Veuillez saisir votre mot de passe LDAP.",
"claim.ldap_to_email.ldapPwd": "Mot de passe LDAP",
@@ -678,8 +752,9 @@
"claim.ldap_to_email.title": "Basculer votre compte de LDAP vers adresse électronique / mot de passe",
"claim.oauth_to_email.confirm": "Confirmez le mot de passe",
"claim.oauth_to_email.description": "Une fois votre compte modifié, vous ne pourrez plus vous connecter qu'à l'aide de votre adresse électronique et votre mot de passe.",
+ "claim.oauth_to_email.enterNewPwd": "Saisissez le mot de passe pour votre compte {site}",
"claim.oauth_to_email.enterPwd": "Veuillez saisir un mot de passe.",
- "claim.oauth_to_email.newPwd": "Saisissez un nouveau mot de passe pour votre compte {site}",
+ "claim.oauth_to_email.newPwd": "Nouveau mot de passe",
"claim.oauth_to_email.pwdNotMatch": "Le mot de passe ne correspond pas.",
"claim.oauth_to_email.switchTo": "Basculer de {type} vers adresse électronique et mot de passe",
"claim.oauth_to_email.title": "Basculer du compte {type} vers l'adresse électronique",
@@ -694,6 +769,23 @@
"create_post.post": "Article",
"create_post.tutorialTip": "<h4>Envoyer des messages</h4><p>Entrez votre message ici et tapez <strong>Entrée</strong> pour l'envoyer.</p><p>Cliquez sur le bouton <strong>pièce-jointe</strong> pour télécharger une image ou un fichier.</p>",
"create_post.write": "Écrire un message...",
+ "create_team.agreement": "En créant votre compte et en utilisant votre {siteName}, vous acceptez nos <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. Si vous n’acceptez pas, vous ne pouvez utilisé votre {siteName}.",
+ "create_team.display_name.back": "Retour à l’étape précedente",
+ "create_team.display_name.charLength": "Le nom doit contenir de 4 à 15 caractères",
+ "create_team.display_name.nameHelp": "Nommez votre équipe dans toutes les langues. Votre nom d'équipe sera montré dans les menus et rubriques.",
+ "create_team.display_name.next": "Suivant",
+ "create_team.display_name.required": "Ce champ est obligatoire",
+ "create_team.display_name.teamName": "Nom de l'équipe",
+ "create_team.team_url.back": "Retour à l’étape précedente",
+ "create_team.team_url.charLength": "Le nom doit contenir de 4 à 15 caractères",
+ "create_team.team_url.finish": "Terminer",
+ "create_team.team_url.hint": "<li>Courte et facile à retenir</li><li>Utilisez des lettres minuscules, des chiffres et des tirets </li><li>Doit commencer par une lettre et ne peut pas se terminer par un tiret</li>",
+ "create_team.team_url.regex": "Utilisez uniquement des lettres minuscules , des chiffres et des tirets. Doit commencer par une lettre et ne peut pas se terminer par un tiret.",
+ "create_team.team_url.required": "Ce champ est obligatoire",
+ "create_team.team_url.taken": "Cette URL est déjà prise ou contient un mot réservé",
+ "create_team.team_url.teamUrl": "URL d’équipe",
+ "create_team.team_url.unavailable": "cette url est invalide. Essayer en une autre.",
+ "create_team.team_url.webAddress": "Choisissez l’adresse Web de votre nouvelle équipe :",
"delete_channel.cancel": "Annuler",
"delete_channel.channel": "canal",
"delete_channel.confirm": "Confirmez la SUPPRESSION du canal",
@@ -732,21 +824,25 @@
"email_verify.failed": " Échec de l'envoi du courriel de vérification.",
"email_verify.notVerifiedBody": "Vérifiez votre adresse électronique. Cherchez le courriel dans votre boîte de réception.",
"email_verify.resend": "Renvoyer le courriel",
- "email_verify.sent": "Le courriel de vérification a été envoyé.",
+ "email_verify.sent": " Le courriel de vérification a été envoyé.",
"email_verify.verified": "L'adresse électronique de {siteName} a été vérifié",
"email_verify.verifiedBody": "<p>Votre adresse électronique a été vérifiée ! <a href={url}>Cliquez ici</a> pour vous connecter.</p>",
"email_verify.verifyFailed": "Impossible de vérifier votre adresse électronique.",
"error_bar.preview_mode": "Mode découverte : Les notifications par courriel ne sont pas configurées.",
"file_attachment.download": "Télécharger",
- "file_info_preview.size": "Taille",
- "file_info_preview.type": "Type de fichier :",
+ "file_info_preview.size": "Taille ",
+ "file_info_preview.type": "Type de fichier ",
"file_upload.fileAbove": "Le fichier plus grand que {max}Mo ne peut pas être téléchargé : {filename}",
"file_upload.filesAbove": "Les fichiers plus grands que {max}Mo ne peuvent pas être téléchargés : {filenames}",
"file_upload.limited": "Les téléchargements sont limités à {count} fichiers par message. Envoyez d'autres messages pour ajouter d'autres fichiers.",
"file_upload.pasted": "Image collée à ",
- "filtered_user_list.count": "{count, number} {count, plural, one {member} other {members}}",
- "filtered_user_list.countTotal": "{count, number} {count, plural, one {member} other {members}} of {total} au total",
+ "filtered_user_list.any_team": "Tous les utilisateurs",
+ "filtered_user_list.count": "{count} {count, plural, =0 {0 members} un {member} autres {members}}",
+ "filtered_user_list.countTotal": "{count} {count, plural, =0 {0 members} un {member} autres {members}} sur {total} au total",
+ "filtered_user_list.member": "Membre",
"filtered_user_list.search": "Rechercher les membres",
+ "filtered_user_list.show": "Filtre :",
+ "filtered_user_list.team_only": "Membre de l’équipe",
"find_team.email": "Adresse électronique",
"find_team.findDescription": "Un courriel a été envoyé avec les liens vers les équipes dont vous êtes membre.",
"find_team.findTitle": "Trouvez votre équipe",
@@ -756,7 +852,7 @@
"find_team.submitError": "Veuillez entrer une adresse électronique valide",
"general_tab.chooseName": "Choisissez un nom pour votre équipe",
"general_tab.codeDesc": "Cliquez sur \"Modifier\" pour réinitialiser le code d'invitation",
- "general_tab.codeLongDesc": "Le code d'invitation est utilisé dans l'URL contenant l'invitation à votre équipe créé depuis le menu principal. Regénérer cette clé rend toutes les invitations déjà envoyées invalides.",
+ "general_tab.codeLongDesc": "Le code d'invitation est utilisé dans l'URL contenant l'invitation à votre équipe créé depuis le menu principal grâce à <strong> Obtenir un lien d’invitation d’équipe </strong>. Regénérer cette clé rend toutes les invitations déjà envoyées invalide.",
"general_tab.codeTitle": "Code d'invitation",
"general_tab.dirContact": "Contactez votre administrateur système pour activer l'annuaire des équipes sur la page d'accueil du système.",
"general_tab.dirDisabled": "L'annuaire d'équipe a été désactivé. Veuillez demander à un administrateur système d'activer l'annuaire d'équipe dans la console système.",
@@ -764,13 +860,15 @@
"general_tab.includeDirDesc": "Inclure cette équipe affichera le nom de l'équipe dans l'annuaire sur la page d'accueil, ainsi qu'un lien pour rejoindre cette équipe.",
"general_tab.includeDirTitle": "Afficher cette équipe dans l'annuaire",
"general_tab.no": "Non",
+ "general_tab.openInviteDesc": "Lorsque autorisé, un lien vers cette équipe sera compris sur la page d'acceuil permettant à quiconque avec un compte de rejoindre cette équipe.",
+ "general_tab.openInviteTitle": "Autoriser n’importe qui à rejoindre à cette équipe",
"general_tab.regenerate": "Générer de nouveau",
"general_tab.required": "Ce champ est obligatoire",
"general_tab.teamName": "Nom de l'équipe",
- "general_tab.teamNameInfo": "Choisissez le nom de l'équipe tel qu'il apparait sur le pages de connexion et en haut de la barre latérale.",
+ "general_tab.teamNameInfo": "Choisissez le nom de l'équipe tel qu'il apparait sur les pages de connexion et en haut de la barre latérale.",
"general_tab.title": "Paramètres généraux",
"general_tab.yes": "Oui",
- "get_link.clipboard": "URL copiée dans le presse-papiers.",
+ "get_link.clipboard": " URL copiée dans le presse-papiers.",
"get_link.close": "Quitter",
"get_link.copy": "Copier l'URL",
"get_post_link_modal.help": "Le lien ci-dessous permet aux utilisateurs de voir votre message.",
@@ -778,8 +876,30 @@
"get_team_invite_link_modal.help": "Envoyez à vos collègues le lien ci-dessous pour leur permettre de s'inscrire à votre équipe. Le lien d'invitation peut être partagé par plusieurs personnes et ne change pas tant qu'il n'a pas été regénéré dans les paramètres de l'équipe par un responsable d'équipe.",
"get_team_invite_link_modal.helpDisabled": "La création d'utilisateurs est désactivée pour votre équipe. Veuillez contacter votre administrateur système.",
"get_team_invite_link_modal.title": "Lien d'invitation à l'équipe",
+ "installed_commands.add": "Ajout de la commande Slash",
+ "installed_commands.empty": "Pas de commande trouvée",
+ "installed_commands.header": "Commande Slash",
+ "installed_incoming_webhooks.add": "Ajouter des Webhooks entrants",
+ "installed_incoming_webhooks.empty": "Aucun webhooks entrants trouvés",
+ "installed_incoming_webhooks.header": "Webhooks entrants",
+ "installed_integrations.creation": "Créé par {creator} le {createAt, date, full}",
+ "installed_integrations.delete": "Supprimer",
+ "installed_integrations.regenToken": "Régénérer le Token",
+ "installed_integrations.search": "Recherche d’intégrations",
+ "installed_integrations.token": "Token : {token}",
+ "installed_integrations.url": "URL : {url}",
+ "installed_outgoing_webhooks.add": "Ajouter des Webhooks sortants",
+ "installed_outgoing_webhooks.empty": "Aucun webhooks sortants trouvés",
+ "installed_outgoing_webhooks.header": "Webhooks sortants",
+ "integrations.command.description": "Les commandes Slash envoient des évènements à des intégrations extérieur",
+ "integrations.command.title": "Commande Slash",
+ "integrations.header": "Intégrations",
+ "integrations.incomingWebhook.description": "Les webhooks entrants permettent des intégrations externes afin d'envoyer des messages",
+ "integrations.incomingWebhook.title": "Webhooks entrants",
+ "integrations.outgoingWebhook.description": "Les webhooks sortants permettent des intégrations externes afin de recevoir et de répondre aux messages",
+ "integrations.outgoingWebhook.title": "Webhooks sortants",
"intro_messages.DM": "Vous êtes au début de votre historique de messages avec {teammate}.<br />Les messages privés et les fichiers partagés ici ne sont visible à personne d'autre.",
- "intro_messages.anyMember": "Tout membre peut rejoindre et lire ce canal.",
+ "intro_messages.anyMember": " Tout membre peut rejoindre et lire ce canal.",
"intro_messages.beginning": "Début de {name}",
"intro_messages.channel": "canal",
"intro_messages.creator": "Voici le début de <strong>{name}</strong> {type}, créé par <strong>{creator}</strong> le <strong>{date}</strong>",
@@ -789,7 +909,7 @@
"intro_messages.inviteOthers": "Inviter d'autres membres dans cette équipe",
"intro_messages.noCreator": "Ceci est le début de {name} {type}, créé le {date}.",
"intro_messages.offTopic": "<h4 class=\"channel-intro__title\">Début de {display_name}</h4><p class=\"channel-intro__content\">Ceci est le début de {display_name}, un canal destiné aux conversations extra-professionnelles.<br/></p>",
- "intro_messages.onlyInvited": "Seuls les membres invités peuvent voir ce groupe privé.",
+ "intro_messages.onlyInvited": " Seuls les membres invités peuvent voir ce groupe privé.",
"intro_messages.setHeader": "Définir l'en-tête",
"intro_messages.teammate": "Vous êtes au début de votre historique de messages avec cette personne. Les messages privés et les fichiers partagés ici ne sont visible à personne d'autre.",
"invite_member.addAnother": "Ajouter un autre",
@@ -807,7 +927,7 @@
"invite_member.newMember": "Inviter un nouveau membre",
"invite_member.send": "Ajouter une invitation",
"invite_member.send2": "Ajouter une invitation",
- "invite_member.sending": "Envoi en cours",
+ "invite_member.sending": " Envoi en cours",
"invite_member.teamInviteLink": "Vous pouvez également inviter des membres en utilisant le {link}.",
"ldap_signup.find": "Trouver mes équipes",
"ldap_signup.ldap": "Créer une nouvelle équipe avec un compte LDAP",
@@ -815,7 +935,7 @@
"ldap_signup.teamName": "Entrez le nom de votre nouvelle équipe",
"ldap_signup.team_error": "Saisissez le nom de votre équipe",
"loading_screen.loading": "Chargement",
- "login.changed": "Méthode de connexion changée",
+ "login.changed": " Méthode de connexion changée",
"login.create": "Créer maintenant",
"login.createTeam": "Créer une nouvelle équipe",
"login.email": "Adresse électronique",
@@ -823,15 +943,23 @@
"login.forgot": "Mot de passe perdu",
"login.gitlab": "GitLab",
"login.google": "Google Apps",
- "login.ldapUsername": "Utilisateur LDAP",
- "login.noAccount": "Pas de compte utilisateur ?",
- "login.on": "sur {siteName}",
+ "login.invalidPassword": "Votre mot de passe est incorrect.",
+ "login.ldapUsername": "Nom d’utilisateur LDAP",
+ "login.noAccount": "Pas de compte utilisateur ? ",
+ "login.on": "activé {siteName}",
"login.or": "ou",
"login.password": "Mot de passe",
- "login.session_expired": "Votre session a expiré. Merci de vous reconnecter.",
+ "login.passwordChanged": " Mot de passe mis a jour avec succès",
+ "login.session_expired": " Votre session a expiré. Merci de vous reconnecter.",
"login.signIn": "Connexion",
+ "login.signInWith": "Se connecter avec:",
+ "login.userNotFound": "Nous ne trouvons aucun compte correspondants a vos identifiants de connexions.",
"login.username": "Nom d'utilisateur",
- "login.verified": "Adresse électronique vérifiée",
+ "login.verified": " Adresse électronique vérifiée",
+ "login_mfa.enterToken": "Pour compléter le processus de connexion , entrer un token de la authentificateur de votre smartphone",
+ "login_mfa.submit": "Envoyer",
+ "login_mfa.token": "MFA Token",
+ "login_mfa.tokenReq": "Entrer un MFA Token",
"member_item.makeAdmin": "Passer Administrateur",
"member_item.member": "Membre",
"member_list.noUsersAdd": "Aucun utilisateur à ajouter.",
@@ -867,11 +995,13 @@
"navbar_dropdown.console": "Console système",
"navbar_dropdown.create": "Créer une nouvelle équipe",
"navbar_dropdown.help": "Aide",
+ "navbar_dropdown.integrations": "Intégrations",
"navbar_dropdown.inviteMember": "Inviter un nouveau membre",
"navbar_dropdown.logout": "Se déconnecter",
"navbar_dropdown.manageMembers": "Gérer les membres",
"navbar_dropdown.report": "Signaler un problème",
"navbar_dropdown.switchTeam": "Basculer sur {team}",
+ "navbar_dropdown.switchTo": "Switch to ",
"navbar_dropdown.teamLink": "Obtenir un lien d'invitation d'équipe",
"navbar_dropdown.teamSettings": "Configuration de l'équipe",
"password_form.change": "Modifier mon mot de passe",
@@ -888,13 +1018,15 @@
"password_send.link": "<p>Un lien de réinitialisation de mot de passe a été envoyé à <b>{email}</b></p>",
"password_send.reset": "Réinitialiser mon mot de passe",
"password_send.title": "Réinitialisation mot de passe",
+ "pending_post_actions.cancel": "Annuler",
+ "pending_post_actions.retry": "Réessayer",
+ "permalink.error.access": "Permalink appartient à un canal dont vous n’avez pas accès",
"post_attachment.collapse": "▲ réduire le texte",
"post_attachment.more": "▼ lire la suite",
"post_body.commentedOn": "A commenté le message de {name}{apostrophe} : ",
"post_body.deleted": "(message supprimé)",
"post_body.plusMore": " et {count} autres fichiers",
"post_body.plusOne": " et 1 autre fichier",
- "post_body.retry": "Réessayer",
"post_delete.notPosted": "Le commentaire n'a pu être envoyé",
"post_delete.okay": "Ok",
"post_delete.someone": "Quelqu'un a supprimé le message sur lequel vous tentiez d'envoyer un commentaire.",
@@ -937,14 +1069,13 @@
"rename_channel.handleHolder": "minuscules alphanumériques seulement",
"rename_channel.lowercase": "Doit être en caractères alphanumériques minuscules",
"rename_channel.maxLength": "Ce champ doit faire moins de 22 caractères",
- "rename_channel.required": "Ce champ est requis",
+ "rename_channel.required": "Ce champ est obligatoire",
"rename_channel.save": "Enregistrer",
"rename_channel.title": "Renommer le canal",
"rhs_comment.comment": "Commentaire",
"rhs_comment.del": "Supprimer",
"rhs_comment.edit": "Éditer",
"rhs_comment.permalink": "Lien permanent",
- "rhs_comment.retry": "Réessayer",
"rhs_header.details": "Détails du message",
"rhs_root.del": "Supprimer",
"rhs_root.direct": "Messages privés",
@@ -974,8 +1105,9 @@
"sidebar.createChannel": "Créer un nouveau canal",
"sidebar.createGroup": "Créer un nouveau groupe",
"sidebar.direct": "Messages privés",
- "sidebar.more": "Plus",
+ "sidebar.more": "Plus…",
"sidebar.moreElips": "Plus...",
+ "sidebar.otherMembers": "En dehors de l’équipe",
"sidebar.pg": "Groupes privés",
"sidebar.removeList": "Retirer de la liste",
"sidebar.tutorialScreen1": "<h4>Canaux</h4><p><strong>Les canaux</strong> organisent les conversations en sujets distincts. Ils sont ouverts à tout le monde dans votre équipe. Pour envoyer des messages privés, utilisez <strong>Messages Privés</strong> pour une personne ou <strong>Groupes Privés</strong> pour plusieurs personnes.</p>",
@@ -991,11 +1123,15 @@
"sidebar_right_menu.logout": "Se déconnecter",
"sidebar_right_menu.manageMembers": "Gérer les membres",
"sidebar_right_menu.report": "Signaler un problème",
+ "sidebar_right_menu.switch_team": "Team Selection",
"sidebar_right_menu.teamLink": "Obtenir un lien d'invitation d'équipe",
"sidebar_right_menu.teamSettings": "Configuration de l'équipe",
+ "signup_team.choose": "Les équipes dont vous êtes membre sont: ",
"signup_team.createTeam": "Ou créez une équipe",
"signup_team.disabled": "Aucune méthode de création d'utilisateur n'est disponible. Veuillez contacter votre administrateur système pour obtenir un accès.",
+ "signup_team.join_open": "Vous montre les équipes que vous pouvez rejoindre: ",
"signup_team.noTeams": "Il n'y a aucune équipe dans l'annuaire, et la création d'équipe n'est pas autorisée.",
+ "signup_team.no_teams": "Vous n’avez pas l’aire d’appartenir à une équipe. Demandez à votre administrateur pour une invitation, rejoignez une équipe ouverte si une existe ou alors crééz une nouvelle équipe.",
"signup_team.none": "Aucune méthode de création d'utilisateur n'est disponible. Veuillez contacter votre administrateur système pour obtenir un accès.",
"signup_team_complete.completed": "Vous avez déjà utilisé cette invitation pour vous inscrire, ou bien l'invitation a expiré.",
"signup_team_confirm.checkEmail": "Veuillez vérifier votre messagerie : <strong>{email}</strong><br />Votre courriel contient un lien pour configurer votre équipe",
@@ -1008,9 +1144,11 @@
"signup_user_completed.expired": "Vous avez déjà utilisé cette invitation pour vous inscrire, ou bien l'invitation a expiré.",
"signup_user_completed.gitlab": "avec GitLab",
"signup_user_completed.google": "avec Google",
+ "signup_user_completed.invalid_invite": "Le lien d'invitation était invalide. Aller voir avec votre administrateur pour recevoir une invitation ",
"signup_user_completed.lets": "Créer votre compte",
+ "signup_user_completed.no_open_server": "Ce serveur ne permet pas d'inscriptions ouvertes. Aller voir avec votre Administrateur pour recevoir une invitation",
"signup_user_completed.none": "Aucune méthode de création d'utilisateur n'est disponible. Veuillez contacter votre administrateur système pour obtenir un accès.",
- "signup_user_completed.onSite": "sur {siteName}",
+ "signup_user_completed.onSite": "activé {siteName}",
"signup_user_completed.or": "ou",
"signup_user_completed.passwordLength": "Veuillez entrer au moins {min} caractères",
"signup_user_completed.required": "Champ obligatoire",
@@ -1020,6 +1158,7 @@
"signup_user_completed.validEmail": "Veuillez entrer une adresse électronique valide",
"signup_user_completed.welcome": "Bienvenue sur :",
"signup_user_completed.whatis": "Quelle est votre adresse électronique ?",
+ "signup_user_completed.withLdap": "Avec vos information d’identifications LDAP",
"sso_signup.find": "Trouver mes équipes",
"sso_signup.gitlab": "Créer une équipe avec un compte GitLab",
"sso_signup.google": "Créer une équipe avec un compte Google Apps",
@@ -1033,15 +1172,15 @@
"team_export_tab.download": "Télécharger",
"team_export_tab.export": "Exporter",
"team_export_tab.exportTeam": "Exporter votre équipe",
- "team_export_tab.exporting": "Export...",
- "team_export_tab.ready": "Prêt pour",
- "team_export_tab.unable": "Impossible d'exporter : {error}",
- "team_import_tab.failure": "Échec de l'import",
+ "team_export_tab.exporting": " Export...",
+ "team_export_tab.ready": " Prêt pour ",
+ "team_export_tab.unable": " Impossible d'exporter : {error}",
+ "team_import_tab.failure": " Échec de l'import ",
"team_import_tab.import": "Importer",
- "team_import_tab.importHelp": "<p>Pour importer une équipe depuis Slack, veuillez aller dans Slack > Team Settings > Import/Export Data > Export > Start Export. Slack ne permet pas d'exporter les fichiers, images, groupes privés ou messages privés. L'import de Mattermost ne prend donc en charge que l'import des messages dans les public channels de slack.</p><p>L'import depuis Slack est actuellement en \"Beta\". Les bot Slack ne sont pas importés et les @mentions ne sont pas supportées.</p>",
+ "team_import_tab.importHelp": "<p>Pour importer une équipe depuis Slack, veuillez aller dans Slack > Team Settings > Import/Export Data > Export > Start Export. Slack ne permet pas d'exporter les fichiers, images, groupes privés ou messages privés. L'import de Mattermost ne prend donc en charge que l'import des messages dans les public channels de Slack.</p><p>L'import depuis Slack est actuellement en \"Beta\". Les bot Slack ne sont pas importés et les @mentions ne sont pas supportées.</p>",
"team_import_tab.importSlack": "Importer depuis Slack (Beta)",
- "team_import_tab.importing": "Import...",
- "team_import_tab.successful": "Import réussi :",
+ "team_import_tab.importing": " Import...",
+ "team_import_tab.successful": " Import réussi : ",
"team_import_tab.summary": "Afficher le récapitulatif",
"team_member_modal.close": "Quitter",
"team_member_modal.members": "Membres de {team}",
@@ -1074,25 +1213,25 @@
"tutorial_intro.end": "Cliquez sur \"Suivant\" pour entrer dans {channel}. C'est le premier canal que les membres voient quand ils s'inscrivent. Utilisez-le pour poster des messages que tout le monde doit lire en premier.",
"tutorial_intro.invite": "Inviter des membres",
"tutorial_intro.next": "Suivant",
- "tutorial_intro.screenOne": "<h3>Bienvenue sur :</h3><h1>Mattermost</h1><p>Toute la communication de votre équipe à un seul endroit, Your team communication all in one place, instantanément consultable et disponible partout.</p><p>Gardez le lien avec votre équipe pour accomplir les tâches les plus importantes.</p>",
+ "tutorial_intro.screenOne": "<h3>Bienvenue sur :</h3><h1>Mattermost</h1><p>Toute la communication de votre équipe à un seul endroit, instantanément consultable et disponible partout.</p><p>Gardez le lien avec votre équipe pour accomplir les tâches les plus importantes.</p>",
"tutorial_intro.screenTwo": "<h3>Comment fonctionne Mattermost :</h3><p>Vous pouvez échanger dans des canaux publics, des groupes privés ou des messages privés.</p><p>Tout est archivé et peut être recherché depuis n'importe quel navigateur web de bureau, tablette ou mobile.</p>",
"tutorial_intro.skip": "Passer le tutoriel",
- "tutorial_intro.support": "Besoin de quoi que ce soit, envoyez-nous un courriel à :",
- "tutorial_intro.teamInvite": "Inviter des membres",
- "tutorial_intro.whenReady": "quand vous serez prêt.",
+ "tutorial_intro.support": "Besoin de quoi que ce soit, envoyez-nous un courriel à : ",
+ "tutorial_intro.teamInvite": "Inviter des collègues",
+ "tutorial_intro.whenReady": " quand vous serez prêt.",
"tutorial_tip.next": "Suivant",
"tutorial_tip.ok": "D'accord",
"tutorial_tip.out": "Ne plus voir ces astuces.",
- "tutorial_tip.seen": "Déjà vu ?",
+ "tutorial_tip.seen": "Déjà vu ? ",
"upload_overlay.info": "Faites glisser un fichier pour le télécharger.",
"user.settings.advance.embed_preview": "Voir un aperçu des liens sous le message",
"user.settings.advance.embed_toggle": "Voir un aperçu pour tous les messages inclus",
"user.settings.advance.enabled": "activé",
- "user.settings.advance.feature": "Fonctionnalité",
- "user.settings.advance.features": "Fonctionnalités",
+ "user.settings.advance.feature": " Fonctionnalité ",
+ "user.settings.advance.features": " Fonctionnalités ",
"user.settings.advance.markdown_preview": "Voir l'option d'aperçu du markdown dans la zone de saisie de message",
- "user.settings.advance.off": "Non",
- "user.settings.advance.on": "Oui",
+ "user.settings.advance.off": "Désactivé",
+ "user.settings.advance.on": "Activé",
"user.settings.advance.preReleaseDesc": "Évaluez les fonctionnalités en avant-première. Vous devrez peut-être rafraîchir la page pour que ce paramètre soit activé.",
"user.settings.advance.preReleaseTitle": "Prévisualiser les fonctionnalités en avant-première",
"user.settings.advance.sendDesc": "Si activé, 'Entrée' insère une nouvelle ligne et 'Ctrl + Entrée' envoie le message.",
@@ -1125,13 +1264,21 @@
"user.settings.developer.register": "Enregistrer une nouvelle application",
"user.settings.developer.thirdParty": "Ouvrez pour enregistrer une nouvelle application tierce",
"user.settings.developer.title": "Paramètres de développement",
+ "user.settings.display.channelDisplayTitle": "Mode d’affichage du canal",
+ "user.settings.display.channeldisplaymode": "Sélectionner la largeur du canal central.",
"user.settings.display.clockDisplay": "Affichage de l'horloge",
+ "user.settings.display.fixedWidthCentered": "Largeur fixe, centrée",
"user.settings.display.fontDesc": "Choisissez la police de caractères utilisée pour l'interface de Mattermost.",
"user.settings.display.fontTitle": "Police d'affichage",
+ "user.settings.display.fullScreen": "Largeur pleine",
"user.settings.display.language": "Langue",
- "user.settings.display.militaryClock": "Horloge 24-heures (exemple : 16:00)",
+ "user.settings.display.messageDisplayClean": "nettoyer",
+ "user.settings.display.messageDisplayCompact": "Compact",
+ "user.settings.display.messageDisplayDescription": "Sélectionner comment les messages dans le canal doivent être affichés",
+ "user.settings.display.messageDisplayTitle": "Affichage des messages",
+ "user.settings.display.militaryClock": "Horloge 24 heures (exemple : 16:00)",
"user.settings.display.nameOptsDesc": "Choisissez comment afficher les autres utilisateurs dans les messages et les listes de messages privés.",
- "user.settings.display.normalClock": "Horloge 12-heures (exemple : 4:00 PM)",
+ "user.settings.display.normalClock": "Horloge 12 heures (exemple : 4:00 PM)",
"user.settings.display.preferTime": "Choisissez l'affichage des heures dans l'application.",
"user.settings.display.showFullname": "Afficher prénom et nom",
"user.settings.display.showNickname": "Afficher le pseudo s'il existe, sinon afficher prénom et nom",
@@ -1140,6 +1287,7 @@
"user.settings.display.theme.customTheme": "Thème personnalisé",
"user.settings.display.theme.describe": "Ouvrez pour gérer votre thème",
"user.settings.display.theme.import": "Importer des couleurs de thème depuis Slack",
+ "user.settings.display.theme.otherThemes": "Voir d’autre thèmes",
"user.settings.display.theme.themeColors": "Couleurs de thème",
"user.settings.display.theme.title": "Thème",
"user.settings.display.title": "Paramètres d'affichage",
@@ -1148,17 +1296,22 @@
"user.settings.general.close": "Quitter",
"user.settings.general.confirmEmail": "Courriel de confirmation",
"user.settings.general.email": "Adresse électronique",
+ "user.settings.general.emailGitlabCantUpdate": "La connexion se produit par Gitlab. L'addresse Electronique ne peut pas être mis à jour . Adresse e-mail utilisée pour les notifications est {email} .",
"user.settings.general.emailHelp1": "L'adresse électronique est utilisé pour la connexion, les notifications et la réinitialisation du mot de passe. Votre adresse électronique doit être validé si vous le changez.",
"user.settings.general.emailHelp2": "Les courriels sont désactivés par votre administrateur système. Aucune notification ne peut être envoyée.",
"user.settings.general.emailHelp3": "L'adresse électronique est utilisée pour la connexion, les notifications et la réinitialisation du mot de passe.",
"user.settings.general.emailHelp4": "Un courriel de vérification a été envoyé à {email}.",
+ "user.settings.general.emailLdapCantUpdate": "Connexion via LDAP . Le courriel ne peut pas être mis à jour . Adresse e-mail utilisée pour les notifications est {email} .",
"user.settings.general.emailMatch": "Les adresses électroniques que vous avez saisies ne correspondent pas.",
+ "user.settings.general.emptyName": "cliquez sur ‘Modifier’ pour ajouter votre nom complet",
+ "user.settings.general.emptyNickname": "Cliquez sur ‘Modifier’ pour ajouter un surnom",
"user.settings.general.firstName": "Prénom",
"user.settings.general.fullName": "Nom complet",
"user.settings.general.imageTooLarge": "Impossible de mettre à jour votre photo de profil. Le fichier est trop grand.",
"user.settings.general.imageUpdated": "Image mise à jour le {date}",
"user.settings.general.lastName": "Nom",
"user.settings.general.loginGitlab": "Connexion avec GitLab ({email})",
+ "user.settings.general.loginLdap": "Connexion avec LDAP ({email})",
"user.settings.general.newAddress": "Nouvelle adresse : {email}<br />Vérifiez votre messagerie pour valider votre adresse électronique.",
"user.settings.general.nickname": "Pseudo",
"user.settings.general.nicknameExtra": "Vous pouvez utiliser un pseudo à la place de vos prénom, nom et nom d'utilisateur. Ceci est pratique lorsque deux personnes de votre équipe ont des noms proches.",
@@ -1167,9 +1320,11 @@
"user.settings.general.primaryEmail": "Adresse de courrier électronique principale",
"user.settings.general.profilePicture": "Photo du profil",
"user.settings.general.title": "Configuration générale",
- "user.settings.general.uploadImage": "Cliquez sur \"Modifier\" pour télécharger une image.",
+ "user.settings.general.uploadImage": "Cliquez sur ‘Modifier’ pour télécharger une image",
"user.settings.general.username": "Nom d'utilisateur",
+ "user.settings.general.usernameInfo": "Choisissez quelque chose de facile à se souvenir pour vos collègues.",
"user.settings.general.usernameReserved": "Ce nom est réservé, veuillez en choisir un autre.",
+ "user.settings.general.usernameRestrictions": "Les noms d'utilisateurs doivent commencer par une lettre et contenir entre {min} et {max} caractères composés de chiffres, lettres minuscules et des symboles '.', '-' et '_'.",
"user.settings.general.validEmail": "Veuillez entrer une adresse électronique valide",
"user.settings.general.validImage": "Seules les images JPG ou PNG sont autorisées pour les photos de profil",
"user.settings.import_theme.cancel": "Annuler",
@@ -1178,10 +1333,17 @@
"user.settings.import_theme.submit": "Envoyer",
"user.settings.import_theme.submitError": "Format invalide, veuillez réessayer de copier-coller.",
"user.settings.languages.change": "Changer la langue de l'interface",
+ "user.settings.mfa.add": "Ajouter une identification à multi-facteur à votre compte",
+ "user.settings.mfa.addHelp": "Vous pouvez demander un token provenant d'un smartphone, en plus de votre mot de passe, pour vous connecter à Mattermost.<br/><br/>Pour l'activer, télécharger Google Authenticator depuis <a target='_blank' href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8'>iTunes</a> ou <a target='_blank' href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en'>Google Play</a> pour votre téléphone, puis<br/><br/>1. Cliquez sur le bouton <strong>Ajouter une identification à multi-facteur à votre compte</strong> ci-dessus.<br/>2 . Utilisez Google Authenticator pour scanner le code QR qui apparaît.<br/>3. Tapez le jeton généré par Google Authenticator et cliquez sur <strong >Enregistrer</strong>.<br/><br/> Lors de la connexion, vous serez invité à entrer un jeton de Google Authenticator en plus de vos informations d'identification régulières.",
+ "user.settings.mfa.addHelpQr": "Scanner le code-barres avec l'application Google Authenticator sur votre smartphone et remplir le jeton avec celui fourni par l'application.",
+ "user.settings.mfa.enterToken": "Token",
+ "user.settings.mfa.qrCode": "Code barre",
+ "user.settings.mfa.remove": "Retirer l’authentification à facteurs multiples",
+ "user.settings.mfa.removeHelp": "Retirer l’authentification multi-facteur signifie que vous n’aurez plus besoin d'un code d'accès basé sur un téléphone pour vous connecter à votre compte.",
"user.settings.modal.advanced": "Options avancées",
"user.settings.modal.confirmBtns": "Oui, abandonner",
"user.settings.modal.confirmMsg": "Certaines modifications ne sont pas sauvegardées, voulez-vous vraiment abandonner ?",
- "user.settings.modal.confirmTitle": "Abandonner les modifications ?",
+ "user.settings.modal.confirmTitle": "Abandonner les modifications ?",
"user.settings.modal.developer": "Développeur",
"user.settings.modal.display": "Affichage",
"user.settings.modal.general": "Général",
@@ -1189,6 +1351,7 @@
"user.settings.modal.security": "Sécurité",
"user.settings.modal.title": "Paramètres du compte",
"user.settings.notification.allActivity": "Pour toute l'activité",
+ "user.settings.notification.push": "Notifications push mobile",
"user.settings.notification.soundConfig": "Configurez les sons des notifications dans les préférences de votre navigateur",
"user.settings.notifications.channelWide": "Mentions globales \"@channel\"",
"user.settings.notifications.close": "Quitter",
@@ -1200,8 +1363,8 @@
"user.settings.notifications.info": "Les notifications sur le bureau sont disponibles avec Firefox, Safari, Chrome, Internet Explorer et Edge.",
"user.settings.notifications.never": "Jamais",
"user.settings.notifications.noWords": "Aucun mot configuré",
- "user.settings.notifications.off": "Non",
- "user.settings.notifications.on": "Oui",
+ "user.settings.notifications.off": "Désactivé",
+ "user.settings.notifications.on": "Activé",
"user.settings.notifications.onlyMentions": "Seulement pour les mentions et messages privés",
"user.settings.notifications.sensitiveName": "Votre prénom (respectant la casse) \"{first_name}\"",
"user.settings.notifications.sensitiveUsername": "Votre nom d'utilisateur (insensible à la casse) \"{username}\"",
@@ -1211,29 +1374,38 @@
"user.settings.notifications.title": "Paramètres de notification",
"user.settings.notifications.usernameMention": "Votre nom d'utilisateur mentionné \"@{username}\"",
"user.settings.notifications.wordsTrigger": "Mots qui déclenchent des mentions",
+ "user.settings.push_notification.allActivity": "Pour toute l'activit",
+ "user.settings.push_notification.info": "Les alertes de notification sont envoyées à votre appareil mobile quand il y a de l’activité dans Mattermost .",
+ "user.settings.push_notification.off": "Désactivé",
+ "user.settings.push_notification.onlyMentions": "Seulement pour les mentions et messages privés",
"user.settings.security.close": "Quitter",
"user.settings.security.currentPassword": "Mot de passe actuel",
"user.settings.security.currentPasswordError": "Veuillez saisir votre mot de passe actuel",
- "user.settings.security.emailPwd": "Courrier électronique et mot de passe",
+ "user.settings.security.emailPwd": "Adresse électronique et mot de passe",
"user.settings.security.gitlab": "GitLab SSO",
"user.settings.security.lastUpdated": "Dernière mise à jour le {date} à {time}",
+ "user.settings.security.ldap": "LDAP",
+ "user.settings.security.loginGitlab": "Connexion avec Gitlab",
+ "user.settings.security.loginLdap": "Connexion avec LDAP",
"user.settings.security.logoutActiveSessions": "Consulter et déconnecter les sessions actives",
"user.settings.security.method": "Méthode de connexion",
"user.settings.security.newPassword": "Nouveau mot de passe",
"user.settings.security.oneSignin": "Vous ne pouvez avoir qu'une seule méthode de connexion à la fois. Changer la méthode de connexion provoquera l'envoi d'un courriel si le changement est réussi.",
"user.settings.security.password": "Mot de passe",
+ "user.settings.security.passwordGitlabCantUpdate": "La connexion se produit à travers GitLab. Le mot de passe ne peut pas être mis à jour.",
+ "user.settings.security.passwordLdapCantUpdate": "La connexion se produit via LDAP . Le mot de passe ne peut pas être mis à jour.",
"user.settings.security.passwordLengthError": "Les nouveaux mots de passe doivent faire au moins {chars} caractères",
"user.settings.security.passwordMatchError": "Les adresses électroniques que vous avez saisies ne correspondent pas.",
"user.settings.security.retypePassword": "Saisissez le nouveau mot de passe",
"user.settings.security.switchEmail": "Utilisation de l'adresse électronique et du mot de passe",
"user.settings.security.switchGitlab": "Utiliser GitLab SSO",
"user.settings.security.switchGoogle": "Utiliser Google SSO",
- "user.settings.security.switchLda": "Basculez votre compte vers LDAP",
+ "user.settings.security.switchLdap": "Changer pour utiliser LDAP",
"user.settings.security.title": "Paramètres de sécurité",
"user.settings.security.viewHistory": "Voir l'historique des accès",
- "user_list.notFound": "Aucun utilisateur trouvé, snif :(",
+ "user_list.notFound": "Aucun utilisateur trouvé.",
"user_profile.notShared": "L'adresse électronique n'est pas partagée",
- "view_image.loading": "Chargement",
+ "view_image.loading": "Chargement ",
"view_image_popover.download": "Télécharger",
"view_image_popover.file": "Fichier {count} sur {total}",
"view_image_popover.publicLink": "Obtenir le lien public",
@@ -1242,5 +1414,6 @@
"web.footer.privacy": "Confidentialité",
"web.footer.terms": "Termes",
"web.header.back": "Précédent",
- "web.root.singup_info": "Toute la communication de votre équipe à un endroit, accessible de partout"
+ "web.root.singup_info": "Toute la communication de votre équipe à un endroit, accessible de partout",
+ "youtube_video.notFound": "Vidéo non trouvé"
}
diff --git a/webapp/i18n/ja.json b/webapp/i18n/ja.json
index 807d0a7de..449e0a748 100644
--- a/webapp/i18n/ja.json
+++ b/webapp/i18n/ja.json
@@ -6,6 +6,7 @@
"about.enterpriseEditionSt": "ファイアウォールの内側で、現代的なエンタープライズコミュニケーションを実現します。",
"about.enterpriseEditione1": "Enterprise Edition",
"about.hash": "ビルドハッシュ値:",
+ "about.hashee": "EEビルドハッシュ値:",
"about.licensed": "ライセンス供給元:",
"about.number": "ビルド番号:",
"about.teamEditionLearn": "Mattermostコミュニティーに参加する: ",
@@ -46,9 +47,12 @@
"add_command.method.help": "リクエストURLに発行するコマンドリクエストの種類です。",
"add_command.method.post": "POST",
"add_command.trigger": "コマンドトリガーワード",
- "add_command.trigger.help1": "例: /patient, /client, /employee",
- "add_command.trigger.help2": "予約語: /echo, /join, /logout, /me, /shrug",
- "add_command.trigger.placeholder": "コマンドトリガー 例: スラッシュコマンドに含まれていない\"hello\"",
+ "add_command.trigger.help1": "例: patient, client, employee",
+ "add_command.trigger.help2": "予約語: echo, join, logout, me, shrug",
+ "add_command.trigger.placeholder": "コマンドトリガー 例: \"hello\"",
+ "add_command.triggerInvalidLength": "トリガーワードは{min}文字以上{max}文字以下にしてください。",
+ "add_command.triggerInvalidSlash": "トリガーワードは/で始めることはできません",
+ "add_command.triggerInvalidSpace": "トリガーワードはスペースを含んではいけません",
"add_command.triggerRequired": "トリガーワードが必要です。",
"add_command.url": "リクエストURL",
"add_command.url.help": "スラッシュコマンドを実行した時に、HTTP POSTまたはGETイベントリクエストを受信するコールバックURLです。",
@@ -85,7 +89,7 @@
"admin.compliance.enableDesc": "有効な場合、Mattermostはコンプライアンスリポートの出力を許可します",
"admin.compliance.enableTitle": "コンプライアンスを有効にする",
"admin.compliance.false": "無効",
- "admin.compliance.noLicense": "<h4 class=\"banner__heading\">注意:</h4><p>コンプライアンスはエンタープライズ版のみの機能です。現在のライセンスはコンプライアンスをサポートしていません。詳しくは<a href=\"http://mattermost.com\"target=\"_blank\">エンタープライズ版についての情報と価格</a>をご覧ください。</p>",
+ "admin.compliance.noLicense": "<h4 class=\"banner__heading\">注意:</h4><p>コンプライアンスはエンタープライズ版のみの機能です。現在のライセンスはコンプライアンスをサポートしていません。詳しくは<a href=\"http://mattermost.com\" target=\"_blank\">エンタープライズ版についての情報と価格</a>をご覧ください。</p>",
"admin.compliance.save": "保存する",
"admin.compliance.saving": "設定を保存しています…",
"admin.compliance.title": "コンプライアンス設定",
@@ -119,21 +123,25 @@
"admin.connectionSecurityTitle": "接続のセキュリティー:",
"admin.connectionSecurityTls": "TLS",
"admin.connectionSecurityTlsDescription": "Mattermostとあなたのサーバー間のコミュニケーションを暗号化します。",
+ "admin.email.agreeHPNS": "私はMattermostのホストするプッシュ通知サービスの<a href=\"https://about.mattermost.com/hpns-terms/\" target=\"_blank\">使用条件</a>と<a href=\"https://about.mattermost.com/hpns-privacy/\" target=\"_blank\">プライバシーポリシー</a>を理解し、承諾します。",
"admin.email.allowEmailSignInDescription": "有効な場合、Mattermostはユーザーが電子メールアドレスとパスワードを使ってサインインをできるようにします。",
"admin.email.allowEmailSignInTitle": "電子メールアドレスでサインインを許可する: ",
"admin.email.allowSignupDescription": "有効な場合、Mattermostはチームの作成と電子メールアドレスとパスワードによる利用登録を許可します。この値は利用登録をOAuthまたはLDAPによるシングルサインオンに制限したい時のみ無効にしてください。",
"admin.email.allowSignupTitle": "電子メールアドレスでの利用登録を許可する: ",
"admin.email.allowUsernameSignInDescription": "有効な場合、Mattermostはユーザーがユーザー名とパスワードでサインインすることを許可します。この設定は電子メール確認が無効にされている場合にのみ使います。",
"admin.email.allowUsernameSignInTitle": "ユーザー名でのサインインを許可する: ",
+ "admin.email.easHelp": "あなた自身のためのモバイルアプリをコンパイルし展開する方法を<a href=\"http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas\" target=\"_blank\">エンタープライズアプリストア</a>から学びます。",
"admin.email.emailFail": "接続できません: {error}",
- "admin.email.emailSettings": "電子メールの設定",
"admin.email.emailSuccess": "電子メールを送信するに当たりエラーはありませんでした。受信ボックスを確認してください。",
- "admin.email.false": "無効",
"admin.email.fullPushNotification": "メッセージ全文を送信する",
"admin.email.genericPushNotification": "ユーザーとチャンネル名を付けて一般的な説明を送信する",
"admin.email.inviteSaltDescription": "招待の電子メールの署名に32文字のソルトを付与します。これはインストールするたびにランダムに生成されます。新しいソルトを生成するには「再生成する」をクリックしてください。",
"admin.email.inviteSaltExample": "例: \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
"admin.email.inviteSaltTitle": "招待用ソルト",
+ "admin.email.mhpns": "iOSとAndroidアプリで、暗号化され高品質なHPNS接続を使用する",
+ "admin.email.mhpnsHelp": "iTunesから<a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target=\"_blank\">Mattermost iOSアプリ</a>をダウンロードし、Google Playから<a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target=\"_blank\">Mattermost Androidアプリ</a>をダウンロードしてください。<a href=\"http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns\" target=\"_blank\">HPNS</a>についても参照してください。",
+ "admin.email.mtpns": "TPNSが有効化されたiOSまたはAndroidアプリをiTunesまたはGoogle Playから入手する",
+ "admin.email.mtpnsHelp": "iTunesから<a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target=\"_blank\">Mattermost iOSアプリ</a>をダウンロードしてください。Google Playから<a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target=\"_blank\">Mattermost Androidアプリ</a>をダウンロードしてください。<a href=\"http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns\" target=\"_blank\">TPNS</a>についても参照してください。",
"admin.email.notificationDisplayDescription": "Mattermostから電子メールによる通知を送信する際に、電子メールアカウントの表示名を使用します。",
"admin.email.notificationDisplayExample": "例: \"Mattermost Notification\", \"System\", \"No-Reply\"",
"admin.email.notificationDisplayTitle": "通知電子メールでの表示名:",
@@ -148,15 +156,15 @@
"admin.email.pushContentDesc": "「ユーザーとチャンネル名を付けて一般的な説明を送信する」を選択することで、一般的なメッセージを付けてプッシュ通知をします。これにはユーザーの名前、チャンネル名は含まれますが、メッセージテキストの詳細は含まれません。<br/><br/>「メッセージ全文を送信する」を選択すると、通知のトリガーとなったメッセージを引用します。これには、秘密にしなくてはいけない内容が含まれる可能性があります。ファイアウォールの外側に対してプッシュ通知を送る場合には、「https」プロトコルを使い暗号化した接続を使用する場合のみこの設定を有効にすることを強くお勧めします。",
"admin.email.pushContentTitle": "プッシュ通知の内容:",
"admin.email.pushDesc": "本番環境では有効に設定してください。有効な場合、Mattermostはプッシュ通知サーバーを通じてiOSとAndroidにプッシュ通知を送信します。",
+ "admin.email.pushOff": "プッシュ通知を送らない",
+ "admin.email.pushOffHelp": "セットアップオプションを設定する前に、<a href=\"http://docs.mattermost.com/deployment/push.html#push-notifications-and-mobile-devices\" target=\"_blank\">プッシュ通知についての説明</a>を参照してください。",
"admin.email.pushServerDesc": "Mattermost プッシュ通知サービスをファイアウォールの内側に設置するには、https://github.com/mattermost/push-proxy を使ってください。テストするためには、http://push-test.mattermost.com を使うことができます。これには、Apple AppStoreにあるサンプルのMattermost iOSアプリで接続できます。本番環境でテストサービスを使わないでください。",
"admin.email.pushServerEx": "例: \"http://push-test.mattermost.com\"",
"admin.email.pushServerTitle": "プッシュ通知サーバー:",
"admin.email.pushTitle": "プッシュ通知を送る: ",
- "admin.email.regenerate": "再生成する",
"admin.email.requireVerificationDescription": "本番環境では有効に設定してください。有効な場合、Mattermostは利用登録をして最初にログインする前に電子メールアドレスの確認を必須にします。開発者はこれを無効に設定することで、電子メールアドレスの確認電子メールの送信を沼沢し、開発をより早く進められるようにできます。",
"admin.email.requireVerificationTitle": "電子メールアドレスの確認が必要: ",
- "admin.email.save": "保存する",
- "admin.email.saving": "設定を保存しています…",
+ "admin.email.selfPush": "プッシュ通知サービスの場所を手入力する",
"admin.email.smtpPasswordDescription": "電子メールサーバーを設定するために管理者から認証情報を入手してください。",
"admin.email.smtpPasswordExample": "例: \"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.email.smtpPasswordTitle": "SMTPパスワード:",
@@ -170,7 +178,7 @@
"admin.email.smtpUsernameExample": "例: \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
"admin.email.smtpUsernameTitle": "SMTPユーザー名:",
"admin.email.testing": "テストしています…",
- "admin.email.true": "有効",
+ "admin.false": "false",
"admin.gitab.clientSecretDescription": "この値はGitLabにログインする際の説明で提供されています。",
"admin.gitlab.EnableHtmlDesc": "<ol><li>GitLabのアカウントでログインし、Profile Settings - Applicationsを選択してください。</li><li>Redirect URIsに \"<your-mattermost-url>/login/gitlab/complete\" (example: http://localhost:8065/login/gitlab/complete)と\"<your-mattermost-utl>/signup/gitlab/complete\"を指定してください。</li><li>次にGitLabの\"Secret\"と\"Id\"欄を以下に入力してください。</li><li>以下のエンドポイントURLを入力してください。</li></ol>",
"admin.gitlab.authDescription": "https://<your-gitlab-url>/oauth/authorize (例: https://example.com:3000/oauth/authorize)を入力してください。サーバーの設定によりHTTPかHTTPSかは異なりますので注意してください。",
@@ -183,14 +191,10 @@
"admin.gitlab.clientSecretTitle": "秘密情報:",
"admin.gitlab.enableDescription": "有効な場合、Mattermostのチームの作成と利用登録をGitLabのOAuthを使って実行できます。",
"admin.gitlab.enableTitle": "GitLabでの利用登録を有効にする: ",
- "admin.gitlab.false": "無効",
- "admin.gitlab.save": "保存する",
- "admin.gitlab.saving": "設定を保存しています…",
"admin.gitlab.settingsTitle": "GitLabの設定",
"admin.gitlab.tokenDescription": "https://<your-gitlab-url>/oauth/tokenを入力してください。サーバーの設定によりHTTPかHTTPSかは異なりますので注意してください。",
"admin.gitlab.tokenExample": "例: \"\"",
"admin.gitlab.tokenTitle": "トークンエンドポイント:",
- "admin.gitlab.true": "有効",
"admin.gitlab.userDescription": "https://<your-gitlab-url>/api/v3/user を入力してください。サーバーの設定によりHTTPかHTTPSかは異なりますので注意してください。",
"admin.gitlab.userExample": "例: \"\"",
"admin.gitlab.userTitle": "ユーザーAPIエンドポイント:",
@@ -206,8 +210,6 @@
"admin.image.amazonS3SecretDescription": "Amazon EC2の管理者から認証情報を入手してください。",
"admin.image.amazonS3SecretExample": "例: \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.image.amazonS3SecretTitle": "Amazon S3秘密アクセスキー:",
- "admin.image.false": "無効",
- "admin.image.fileSettings": "ファイル設定",
"admin.image.localDescription": "画像ファイルを書き込むディレクトリー。空欄の場合には./data/に設定されます。",
"admin.image.localExample": "例: \"./data/\"",
"admin.image.localTitle": "ローカルディレクトリーの場所:",
@@ -226,9 +228,6 @@
"admin.image.publicLinkDescription": "公開画像リンクの署名に32文字のソルトを付与します。これはインストールするたびにランダムに生成されます。新しいソルトを生成するには「再生成する」をクリックしてください。",
"admin.image.publicLinkExample": "例: Ex \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
"admin.image.publicLinkTitle": "公開リンクソルト:",
- "admin.image.regenerate": "再生成する",
- "admin.image.save": "保存する",
- "admin.image.saving": "設定を保存しています…",
"admin.image.shareDescription": "ファイルと画像に公開リンクを使い共有することをユーザーに許可します。",
"admin.image.shareTitle": "公開ファイルリンクを共有する: ",
"admin.image.storeAmazonS3": "Amazon S3",
@@ -240,7 +239,6 @@
"admin.image.thumbWidthDescription": "アップロードされた画像から生成されるサムネイルの幅を指定します。この値を変更することで、設定以降のサムネイルサイズが変更されます。しかし設定以前の画像は変更されません。",
"admin.image.thumbWidthExample": "例: \"120\"",
"admin.image.thumbWidthTitle": "サムネイルの幅:",
- "admin.image.true": "有効",
"admin.ldap.bannerDesc": "LDAPサーバーの情報が変更された場合、次回Mattermostにログインし直した際に、その情報を参照します。これにはユーザーをLDAPサーバーで無効化したり削除したりした場合も含みます。LDAPサーバーとの同期機能は将来のリリースで実装予定です。",
"admin.ldap.bannerHeading": "注意:",
"admin.ldap.baseDesc": "ベースDNはMattermostがLDAPツリーでユーザー検索を始める場所の識別名です。",
@@ -255,7 +253,6 @@
"admin.ldap.emailAttrTitle": "電子メール属性値:",
"admin.ldap.enableDesc": "有効な場合、MattermostはログインにLDAPの使用を許可します",
"admin.ldap.enableTitle": "LDAPでのログインを有効化する:",
- "admin.ldap.false": "無効",
"admin.ldap.firstnameAttrDesc": "Mattermostのユーザーの名前(ファーストネーム)を設定するために使用されるLDAPサーバーの属性値です。",
"admin.ldap.firstnameAttrEx": "例: \"givenName\"",
"admin.ldap.firstnameAttrTitle": "名前(ファーストネーム)の属性値:",
@@ -269,23 +266,20 @@
"admin.ldap.loginNameEx": "例: \"LDAP Username\"",
"admin.ldap.loginNameTitle": "ログインフィールド名:",
"admin.ldap.nicknameAttrDesc": "(オプション)Mattermostのユーザーの苗字(ラストネーム)を設定するために使用されるLDAPサーバーの属性値です。",
- "admin.ldap.nicknameAttrTitle": "ユーザー名の属性値:",
- "admin.ldap.noLicense": "<h4 class=\"banner__heading\">注意:</h4><p>LDAPはエンタープライズ版のみの機能です。現在のライセンスはLDAPをサポートしていません。詳しくは<a href=\"http://mattermost.com\"target=\"_blank\">エンタープライズ版についての情報と価格</a>をご覧ください。</p>",
+ "admin.ldap.nicknameAttrEx": "例: \"nickname'",
+ "admin.ldap.nicknameAttrTitle": "ニックネームの属性値:",
+ "admin.ldap.noLicense": "<h4 class=\"banner__heading\">注意:</h4><p>LDAPはエンタープライズ版のみの機能です。現在のライセンスはLDAPをサポートしていません。詳しくは<a href=\"http://mattermost.com\" target=\"_blank\">エンタープライズ版についての情報と価格</a>をご覧ください。</p>",
"admin.ldap.portDesc": "MattermostがLDAPサーバーに接続するポート番号です。デフォルトでは389です。",
"admin.ldap.portEx": "例: \"389\"",
"admin.ldap.portTitle": "LDAPポート:",
"admin.ldap.queryDesc": "LDAPサーバーに問い合わせをする際のタイムアウト秒数です。遅いLDAPサーバーとの接続でタイムアウトエラーが発生する場合には数値を増やしてください。",
"admin.ldap.queryEx": "例: \"60\"",
"admin.ldap.queryTitle": "問い合わせタイムアウト(秒):",
- "admin.ldap.save": "保存する",
- "admin.ldap.saving": "設定を保存しています…",
"admin.ldap.serverDesc": "LDAPサーバーのドメイン名またはIPアドレスです。",
"admin.ldap.serverEx": "例: \"10.0.0.23\"",
"admin.ldap.serverTitle": "LDAPサーバー:",
"admin.ldap.skipCertificateVerification": "証明書の検証をしない",
"admin.ldap.skipCertificateVerificationDesc": "TLSまたはSTARTTLSの証明書の検証ステップをスキップします。TLSが必要な本番環境では設定することは推奨されません。テスト用の設定です。",
- "admin.ldap.title": "LDAPの設定",
- "admin.ldap.true": "有効",
"admin.ldap.uernameAttrDesc": "Mattermostのユーザー名を設定するために使用されるLDAPサーバーの属性値です。ID属性値と同じである場合があります。",
"admin.ldap.userFilterDisc": "ユーザーオブジェクトを検索する再のLDAPフィルターを入力できます。任意項目です。これを適用したクエリーで選ばれるユーザーのみがMattermostにアクセスできます。Active Directoryでは、無効化されているユーザーを排除するには、(&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))を設定してください。",
"admin.ldap.userFilterEx": "例: \"(objectClass=user)\"",
@@ -306,7 +300,6 @@
"admin.license.uploading": "ライセンスをアップロードしています…",
"admin.log.consoleDescription": "本番環境では無効に設定してください。開発者はこれを有効に設定することで、コンソールレベルオプションに応じてログメッセージを出力させることができます。有効な場合、サーバー標準出力(stdout)にメッセージを出力します。",
"admin.log.consoleTitle": "コンソールにログを出力する: ",
- "admin.log.false": "無効",
"admin.log.fileDescription": "本番環境では有効に設定してください。有効な場合、ログファイルは以下のファイルの場所欄で指定されたファイルに出力されます。",
"admin.log.fileLevelDescription": "この設定はどのログイベントをログファイルに出力するか決定します。ERROR: エラーメッセージのみを出力する。INFO: エラーと起動と初期化の情報を出力する。DEBUG: 問題をデバッグする開発者向けの詳細を出力する。",
"admin.log.fileLevelTitle": "ファイルログレベル:",
@@ -326,27 +319,18 @@
"admin.log.locationPlaceholder": "ファイルの場所を入力してください",
"admin.log.locationTitle": "ファイルの場所:",
"admin.log.logSettings": "LDAPの設定",
- "admin.log.save": "保存する",
- "admin.log.saving": "設定を保存しています…",
- "admin.log.true": "有効",
"admin.logs.reload": "再読み込み",
"admin.logs.title": "サーバーログ",
"admin.nav.help": "ヘルプ",
"admin.nav.logout": "ログアウト",
"admin.nav.report": "問題を報告する",
- "admin.nav.switch": "{display_name}に切り替える",
- "admin.privacy.false": "無効",
- "admin.privacy.save": "保存する",
- "admin.privacy.saving": "設定を保存しています…",
+ "admin.nav.switch": "チームを切り替える",
"admin.privacy.showEmailDescription": "無効に設定した場合には、チームオーナーやチーム管理者を含んだユーザーは、他のユーザーの電子メールアドレスをユーザーインターフェイス上で参照できません。ユーザーの連絡先を秘密にした上でチームを管理するようにセットアップしたい場合に有効にしてください。",
"admin.privacy.showEmailTitle": "電子メールアドレスを表示する: ",
"admin.privacy.showFullNameDescription": "無効に設定した場合には、チームオーナーやチーム管理者を含んだユーザーは、他のユーザーのフルネームをユーザーインターフェイス上で参照できません。ユーザー名がフルネームが使用される場所で使用されます。",
"admin.privacy.showFullNameTitle": "フルネームを表示する: ",
- "admin.privacy.title": "プライバシーの設定",
- "admin.privacy.true": "有効",
"admin.rate.enableLimiterDescription": "有効な場合、APIは以下で指定した頻度に制限されます。",
"admin.rate.enableLimiterTitle": "投稿頻度制限を有効にする: ",
- "admin.rate.false": "無効",
"admin.rate.httpHeaderDescription": "記入した場合には、指定されたHTTPヘッダーフィールド(例えば、NGINXで設定する場合には\"X-Real-IP\"、AmazonELBの場合には\"X-Forwarded-For\")で投稿頻度を制限を変更できます。",
"admin.rate.httpHeaderExample": "例: \"X-Real-IP\", \"X-Forwarded-For\"",
"admin.rate.httpHeaderTitle": "HTTPヘッダーで変更する:",
@@ -360,10 +344,13 @@
"admin.rate.queriesTitle": "1秒間の問い合わせ数:",
"admin.rate.remoteDescription": "有効な場合、IPアドレスでAPIアクセスを投稿頻度を制限します。",
"admin.rate.remoteTitle": "リモートアドレスごとに設定する:",
- "admin.rate.save": "保存する",
- "admin.rate.saving": "設定を保存しています…",
- "admin.rate.title": "投稿頻度の設定",
- "admin.rate.true": "有効",
+ "admin.recycle.button": "Recycle Database Connections",
+ "admin.recycle.loading": " Recycling...",
+ "admin.recycle.reloadFail": "Recycling unsuccessful: {error}",
+ "admin.regenerate": "Re-Generate",
+ "admin.reload.button": "Reload Configuration From Disk",
+ "admin.reload.loading": " Loading...",
+ "admin.reload.reloadFail": "Reloading unsuccessful: {error}",
"admin.reset_password.close": "閉じる",
"admin.reset_password.newPassword": "新しいパスワード",
"admin.reset_password.select": "選択する",
@@ -383,7 +370,6 @@
"admin.service.corsTitle": "クロスオリジンリクエストを許可する:",
"admin.service.developerDesc": "(開発者オプション) 有効な場合、エラーに関連する追加の情報をUIに表示します。",
"admin.service.developerTitle": "開発者モードを有効にする: ",
- "admin.service.false": "無効",
"admin.service.googleDescription": "このキーを設定することで、投稿かコメントでYouTubeビデオへのハイパーリンクを含めるとビデオを埋め込むことができます。キーの取得方法は、<a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">https://www.youtube.com/watch?v=Im69kzhpR3I</a>を参照してください。空欄にしておくと、YouTubeビデオのプレビューをリンクから自動作成する機能は無効になります。",
"admin.service.googleExample": "例: \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"",
"admin.service.googleTitle": "Google開発者キー:",
@@ -404,8 +390,6 @@
"admin.service.outWebhooksTitle": "外向きのウェブフックを有効化する: ",
"admin.service.overrideDescription": "有効な場合、ウェブフックとスラッシュコマンドによって投稿に紐付くユーザー名を変更できるようにします。注意: アイコンの上書きと組み合わせることによって、フィッシング攻撃の危険性が生じる可能性があります。",
"admin.service.overrideTitle": "ウェブフックまたはスラッシュコマンドでのユーザー名を上書きする: ",
- "admin.service.save": "保存する",
- "admin.service.saving": "設定を保存しています…",
"admin.service.securityDesc": "有効な場合、システム管理者は関係のあるセキュリティー修正が発生した場合、12時間以内に電子メールでお知らせを受信できあす。電子メールが有効になっている必要があります。",
"admin.service.securityTitle": "セキュリティー通知を有効にする: ",
"admin.service.segmentDescription": "SaaSサービスを運用しているユーザーは、Segment.comのキーで入力することで、測定ができます。",
@@ -418,41 +402,55 @@
"admin.service.ssoSessionDaysDesc": "シングルサインオンのセッションは、ここで設定された日数で失効し、ユーザーはログインし直すことになります。",
"admin.service.testingDescription": "[開発者用オプション] 有効な場合、テストアカウントとテストデータによる /loadtest スラッシュコマンドが使用可能になります。この設定を変更した場合、サーバーを再起動するまで設定は反映されません。",
"admin.service.testingTitle": "テストを有効にする: ",
- "admin.service.title": "サービスの設定",
- "admin.service.true": "有効",
"admin.service.webSessionDays": "ウェブのセッション維持期間(日):",
"admin.service.webSessionDaysDesc": "ウェブのセッションは、ここで設定された日数で失効し、ユーザーはログインし直すことになります。",
"admin.service.webhooksDescription": "有効な場合、内向きのウェブフックが使用可能になります。フィッシング攻撃を防ぐため、全てのウェブフックによる投稿にはBOTタグが付けられます。",
"admin.service.webhooksTitle": "外向きのウェブフックを有効化する: ",
"admin.sidebar.addTeamSidebar": "サイドバーメニューからチームを追加する",
"admin.sidebar.audits": "コンプライアンスと監査",
- "admin.sidebar.compliance": "コンプライアンスの設定",
- "admin.sidebar.email": "電子メールの設定",
- "admin.sidebar.file": "ファイルの設定",
- "admin.sidebar.gitlab": "GitLabの設定",
- "admin.sidebar.ldap": "LDAPの設定",
+ "admin.sidebar.authentication": "認証",
+ "admin.sidebar.compliance": "コンプライアンス",
+ "admin.sidebar.configuration": "設定",
+ "admin.sidebar.connections": "接続",
+ "admin.sidebar.customBrand": "独自ブランド設定",
+ "admin.sidebar.customization": "カスタマイズ",
+ "admin.sidebar.database": "データベース",
+ "admin.sidebar.developer": "開発者",
+ "admin.sidebar.email": "電子メールアドレス",
+ "admin.sidebar.external": "外部のサービス",
+ "admin.sidebar.files": "ファイル",
+ "admin.sidebar.general": "全般",
+ "admin.sidebar.gitlab": "GitLab",
+ "admin.sidebar.images": "画像",
+ "admin.sidebar.integrations": "統合機能",
+ "admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Editionとライセンス",
- "admin.sidebar.loading": "読み込み中です",
- "admin.sidebar.log": "ログの設定",
+ "admin.sidebar.logging": "ログ",
+ "admin.sidebar.login": "ログイン",
"admin.sidebar.logs": "ログ",
+ "admin.sidebar.notifications": "通知",
"admin.sidebar.other": "その他",
- "admin.sidebar.privacy": "プライバシーの設定",
- "admin.sidebar.rate_limit": "投稿頻度制限の設定",
+ "admin.sidebar.privacy": "プライバシー",
+ "admin.sidebar.publicLinks": "公開リンク",
+ "admin.sidebar.push": "モバイルプッシュ",
+ "admin.sidebar.rateLimiting": "投稿頻度制限",
"admin.sidebar.reports": "サイトリポート",
"admin.sidebar.rmTeamSidebar": "サイドバーメニューからチームを削除する",
- "admin.sidebar.service": "サービスの設定",
+ "admin.sidebar.security": "セキュリティー",
+ "admin.sidebar.sessions": "セッション",
"admin.sidebar.settings": "設定",
- "admin.sidebar.sql": "SQLの設定",
- "admin.sidebar.statistics": "- 統計",
- "admin.sidebar.support": "法的事項とサポートの設定",
- "admin.sidebar.team": "チームの設定",
- "admin.sidebar.teams": "チーム ({count})",
- "admin.sidebar.users": "- ユーザー",
+ "admin.sidebar.sign_up": "利用登録",
+ "admin.sidebar.statistics": "統計",
+ "admin.sidebar.storage": "ストレージ",
+ "admin.sidebar.support": "法的事項とサポート",
+ "admin.sidebar.teams": "チーム ({count, number})",
+ "admin.sidebar.users": "ユーザー",
+ "admin.sidebar.usersAndTeams": "ユーザーとチーム",
"admin.sidebar.view_statistics": "統計を表示する",
+ "admin.sidebar.webhooks": "ウェブフックとコマンド",
"admin.sidebarHeader.systemConsole": "システムコンソール",
"admin.sql.dataSource": "データソース:",
"admin.sql.driverName": "ドライバー名:",
- "admin.sql.false": "無効",
"admin.sql.keyDescription": "データベースの守るべき情報の列を暗号化/復号するために32文字のソルトを使用します。",
"admin.sql.keyExample": "例: \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
"admin.sql.keyTitle": "アーカイブされたデータの暗号化キー:",
@@ -464,14 +462,9 @@
"admin.sql.maxOpenTitle": "最大稼働接続数:",
"admin.sql.noteDescription": "このセッションでの設定値の変更を有効にするにはサーバーを再起動させる必要があります。",
"admin.sql.noteTitle": "注意:",
- "admin.sql.regenerate": "再生成する",
"admin.sql.replicas": "データソースレプリカ:",
- "admin.sql.save": "保存する",
- "admin.sql.saving": "設定を保存しています…",
- "admin.sql.title": "SQLの設定",
"admin.sql.traceDescription": "(開発モード) 有効な場合、実行されるSQL文がログに出力されます。",
"admin.sql.traceTitle": "トレース: ",
- "admin.sql.true": "有効",
"admin.sql.warning": "警告: このソルトを再生成することで、データベース内のいくつかの項目は空白になります。",
"admin.support.aboutDesc": "このMattermostについての、例えばあなたの所属する組織の目的や参加者についての、より詳しい情報へのリンクです。デフォルトではMattermostの情報ページになっています。",
"admin.support.aboutTitle": "このMattermostについてのリンク:",
@@ -485,11 +478,8 @@
"admin.support.privacyTitle": "プライバシーポリシーのリンク:",
"admin.support.problemDesc": "チームサイトのメインメニューからリンクされたヘルプ文書へのリンクです。デフォルトでは、これは検索のできる相互補助的な問題解決のためのフォーラムへのリンクになっています。技術的な問題への手助けになります。",
"admin.support.problemTitle": "問題報告のリンク:",
- "admin.support.save": "保存する",
- "admin.support.saving": "設定を保存しています…",
"admin.support.termsDesc": "デスクトップ版またはモバイル版で参照できる使用条件へのリンクです。この注意書きを表示しない場合には空欄にしてください。",
"admin.support.termsTitle": "使用条件のリンク:",
- "admin.support.title": "法的事項とサポートの設定",
"admin.system_analytics.activeUsers": "投稿実績のあるアクティブユーザー",
"admin.system_analytics.title": "システム",
"admin.system_analytics.totalPosts": "総投稿数",
@@ -501,11 +491,12 @@
"admin.team.chooseImage": "新しい画像を選択する",
"admin.team.dirDesc": "有効な場合、チーム一覧に表示されるチームが、新しいチーム作成の代わりに、メインページに表示されます。",
"admin.team.dirTitle": "チーム一覧を有効にする: ",
- "admin.team.false": "無効",
"admin.team.maxUsersDescription": "チーム毎のユーザー数合計の最大値です。有効なユーザーと無効なユーザーの両方が数えられます。",
"admin.team.maxUsersExample": "例: \"25\"",
"admin.team.maxUsersTitle": "チーム毎の最大ユーザー数:",
"admin.team.noBrandImage": "ブランド画像がアップロードされていません",
+ "admin.team.openServerDescription": "有効にした場合、招待されなくても誰でもこのサーバーにユーザーアカウントを作成できます。",
+ "admin.team.openServerTitle": "オープンサーバーを有効にする: ",
"admin.team.restrictDescription": "特定のドメインからだけチームとユーザーアカウントの作成を可能にします。単数(例: \"mattermost.org\")でもカンマ区切りで複数(例: \"corp.mattermost.com, mattermost.org\")でも指定できます。",
"admin.team.restrictDirectMessage": "ダイレクトメッセージの対象範囲:",
"admin.team.restrictDirectMessageDesc": "'Mattermostの全てのユーザー'はチームに属していないユーザーへのダイレクトメッセージのチャンネルを利用することが出来ます。'このチームのメンバー'では同じチームに属しているユーザーに制限されます。",
@@ -515,15 +506,11 @@
"admin.team.restrictTitle": "ドメインでの作成制限:",
"admin.team.restrict_direct_message_any": "Mattermostの全てのユーザー",
"admin.team.restrict_direct_message_team": "チームのメンバーのみ",
- "admin.team.save": "保存する",
- "admin.team.saving": "設定を保存しています…",
"admin.team.siteNameDescription": "ログイン画面とユーザーインターフェースで表示されるサービス名です。",
"admin.team.siteNameExample": "例: \"Mattermost\"",
"admin.team.siteNameTitle": "サイト名:",
"admin.team.teamCreationDescription": "無効な場合、チーム作成機能は無効になります。チーム作成ボタンを押すとエラーが発生します。",
"admin.team.teamCreationTitle": "チーム作成を有効にする: ",
- "admin.team.title": "チームの設定",
- "admin.team.true": "有効",
"admin.team.upload": "アップロードする",
"admin.team.uploadDesc": "ログイン画面に独自画像を追加することで、ユーザー体験をカスタマイズできます。例は<a href='http://docs.mattermost.com/administration/config-settings.html#custom-branding' target='_blank'>docs.mattermost.com/administration/config-settings.html#custom-branding</a>をご覧ください。",
"admin.team.uploaded": "アップロードしました!",
@@ -532,6 +519,7 @@
"admin.team.userCreationTitle": "ユーザー作成を有効にする: ",
"admin.team_analytics.activeUsers": "投稿実績のあるアクティブユーザー",
"admin.team_analytics.totalPosts": "総投稿数",
+ "admin.true": "true",
"admin.userList.title": "{team}のユーザー",
"admin.userList.title2": "{team}のユーザー({count})",
"admin.user_item.authServiceEmail": ", <strong>サインイン方法:</strong> Email",
@@ -548,7 +536,11 @@
"admin.user_item.makeSysAdmin": "システム管理者になる",
"admin.user_item.makeTeamAdmin": "チーム管理者にする",
"admin.user_item.member": "メンバー",
+ "admin.user_item.mfaNo": ", <strong>MFA</strong>: いいえ",
+ "admin.user_item.mfaYes": ", <strong>MFA</strong>: はい",
+ "admin.user_item.resetMfa": "MFAを削除する",
"admin.user_item.resetPwd": "パスワードを初期化する",
+ "admin.user_item.switchToEmail": "電子メールアドレス/パスワードに切り替える",
"admin.user_item.sysAdmin": "システム管理者",
"admin.user_item.teamAdmin": "チーム管理者",
"analytics.chart.loading": "読み込み中です…",
@@ -777,7 +769,7 @@
"create_post.post": "投稿",
"create_post.tutorialTip": "<h4>メッセージを送信しています</h4><p>ここのメッセージを書き、<strong>Enter</strong>を押すことで投稿します。</p><p><strong>添付</strong>ボタンを押すことで画像やファイルをアップロードします。</p>",
"create_post.write": "メッセージを書き込んでいます…",
- "create_team.agreement": "{siteName}にアカウントを作成し利用する前に<a href='/static/help/terms.html'>使用条件</a>と<a href='/static/help/privacy.html'>プライバシーポリシー</a>に同意してください。同意できない場合は{siteName}は使用できません。",
+ "create_team.agreement": "アカウントを作成し{siteName}を利用する前に<a href='/static/help/terms.html'>使用条件</a>と<a href='/static/help/privacy.html'>プライバシーポリシー</a>に同意してください。同意できない場合は{siteName}は使用できません。",
"create_team.display_name.back": "前のステップに戻る",
"create_team.display_name.charLength": "名前は4文字以上の15文字以下にしてください",
"create_team.display_name.nameHelp": "チーム名はどんな言語でも使うことができます。チーム名はメニューと画面上部に表示されます。",
@@ -793,7 +785,7 @@
"create_team.team_url.taken": "URLが取得済みか、予約された単語を含んでいます",
"create_team.team_url.teamUrl": "チームURL",
"create_team.team_url.unavailable": "このURLは使用できません。他のものを試してください。",
- "create_team.team_url.webAddress": "あなたの新しいチームのウェブアドレスを選択してください。",
+ "create_team.team_url.webAddress": "あなたの新しいチームのウェブアドレスを選択してください:",
"delete_channel.cancel": "キャンセル",
"delete_channel.channel": "チャンネル",
"delete_channel.confirm": "チャンネルの削除を確認する",
@@ -849,7 +841,7 @@
"filtered_user_list.countTotal": "{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} 合計",
"filtered_user_list.member": "メンバー",
"filtered_user_list.search": "メンバーを検索する",
- "filtered_user_list.show": "表示",
+ "filtered_user_list.show": "フィルター:",
"filtered_user_list.team_only": "このチームのメンバー",
"find_team.email": "電子メールアドレス",
"find_team.findDescription": "あなたがメンバーになっているチームへのリンクが含まれる電子メールが送信されました。",
@@ -860,7 +852,7 @@
"find_team.submitError": "有効な電子メールアドレスを入力してください",
"general_tab.chooseName": "あなたのチームの新しい名称を選択してください",
"general_tab.codeDesc": "招待コードを再生成するには「編集」をクリックしてください。",
- "general_tab.codeLongDesc": "招待コードは、**チーム招待リンクを入手する**で作成されたチーム招待リンクのURLの一部として使われます。再生成することで新しいチーム招待リンクが作成され、古いリンクは無効化されます。",
+ "general_tab.codeLongDesc": "招待コードは、<strong>チーム招待リンクを入手する</strong>で作成されたチーム招待リンクのURLの一部として使われます。再生成することで新しいチーム招待リンクが作成され、古いリンクは無効化されます。",
"general_tab.codeTitle": "招待コード",
"general_tab.dirContact": "システムのホームページでのチーム一覧を有効にするようにシステム管理者に連絡してください。",
"general_tab.dirDisabled": "チーム一覧が無効になっています。システム管理者にシステムコンソールのチームの設定でチーム一覧を有効化するように依頼してください。",
@@ -868,6 +860,8 @@
"general_tab.includeDirDesc": "ホームページのチーム一覧にチーム名が表示され、サインインページへのリンクが提供されます。",
"general_tab.includeDirTitle": "チーム一覧に追加する",
"general_tab.no": "いいえ",
+ "general_tab.openInviteDesc": "許可された場合、このチームへのリンクは、このチームに誰もが参加できるようにするランディングページを含みます。",
+ "general_tab.openInviteTitle": "このチームに誰でも参加できるよう許可する",
"general_tab.regenerate": "再生成する",
"general_tab.required": "この項目は必須です",
"general_tab.teamName": "チーム名",
@@ -942,24 +936,24 @@
"ldap_signup.team_error": "チーム名を入力してください",
"loading_screen.loading": "読み込み中です",
"login.changed": "サインイン方法が変更されました",
- "login.create": "ただいま作成しています",
+ "login.create": "アカウントを作成する",
"login.createTeam": "新しいチームを作成する",
"login.email": "電子メールアドレス",
"login.find": "参加している他のチームを探す",
"login.forgot": "パスワードを忘れました",
"login.gitlab": "GitLabでログインする",
"login.google": "Google Appsでログインする",
+ "login.invalidPassword": "パスワードが正しくありません。",
"login.ldapUsername": "LDAPユーザー名",
- "login.loginIdRequired": "{type} は必須です。",
"login.noAccount": "アカウントを持っていますか? ",
"login.on": "{siteName}にて",
"login.or": "または",
"login.password": "パスワード",
- "login.passwordRequired": "パスワードは必須です",
+ "login.passwordChanged": " パスワードは正常に更新されました",
"login.session_expired": " あなたのセッションは有効期限が切れました。再度ログインしてください。",
"login.signIn": "サインイン",
- "login.signTo": "サインイン先: ",
- "login.userNotFound": "このチームにはあなたのユーザー名に合致するアカウントはありません。",
+ "login.signInWith": "サインイン方法:",
+ "login.userNotFound": "あなたのログイン情報に合致するアカウントはありません。",
"login.username": "ユーザー名",
"login.verified": " 電子メールアドレスが確認されました",
"login_mfa.enterToken": "サインインを完了するには、スマートフォンのauthenticatorからのトークンを入力してください。",
@@ -1007,6 +1001,7 @@
"navbar_dropdown.manageMembers": "メンバーを管理する",
"navbar_dropdown.report": "問題を報告する",
"navbar_dropdown.switchTeam": "{team}に切り替える",
+ "navbar_dropdown.switchTo": "切り替え先: ",
"navbar_dropdown.teamLink": "チーム招待リンクを入手する",
"navbar_dropdown.teamSettings": "チームの設定",
"password_form.change": "パスワードを変更する",
@@ -1023,13 +1018,15 @@
"password_send.link": "<p>パスワード初期化リンクが<b>{email}</b>に送信されました</p>",
"password_send.reset": "自分のパスワードを初期化する",
"password_send.title": "パスワードの初期化",
+ "pending_post_actions.cancel": "キャンセル",
+ "pending_post_actions.retry": "再試行",
+ "permalink.error.access": "アクセスできないチャンネルのパーマリンクです",
"post_attachment.collapse": "▲テキストをたたむ",
"post_attachment.more": "▼もっと読む",
"post_body.commentedOn": "{name}{apostrophe}のメッセージにコメントしました: ",
"post_body.deleted": "(メッセージは削除されています)",
"post_body.plusMore": " {count}の他のファイルを追加する",
"post_body.plusOne": " 1つの他のファイルを追加する",
- "post_body.retry": "再試行する",
"post_delete.notPosted": "コメントが投稿できませんでした",
"post_delete.okay": "削除する",
"post_delete.someone": "あなたがコメントしようとしたメッセージは削除されています。",
@@ -1079,7 +1076,6 @@
"rhs_comment.del": "削除する",
"rhs_comment.edit": "編集する",
"rhs_comment.permalink": "パーマリンク",
- "rhs_comment.retry": "再試行する",
"rhs_header.details": "メッセージの詳細",
"rhs_root.del": "削除する",
"rhs_root.direct": "ダイレクトメッセージ",
@@ -1111,6 +1107,7 @@
"sidebar.direct": "ダイレクトメッセージ",
"sidebar.more": "もっと",
"sidebar.moreElips": "もっと…",
+ "sidebar.otherMembers": "このチームの外側",
"sidebar.pg": "非公開グループ",
"sidebar.removeList": "一覧から削除する",
"sidebar.tutorialScreen1": "<h4>チャンネル</h4><p><strong>チャンネル</strong>は様々な話題についての会話を扱います。チャンネルはあなたのチームの全員が読み書き可能です。個人的なコミュニケーションには特定の一人との場合には<strong>ダイレクトメッセージ</strong>を、複数の人との場合には<strong>プライベートグループ</strong>を使ってください。</p>",
@@ -1126,11 +1123,15 @@
"sidebar_right_menu.logout": "ログアウト",
"sidebar_right_menu.manageMembers": "メンバーを管理する",
"sidebar_right_menu.report": "問題を報告する",
+ "sidebar_right_menu.switch_team": "チームを切り替える",
"sidebar_right_menu.teamLink": "チーム招待リンクを入手する",
"sidebar_right_menu.teamSettings": "チームの設定",
+ "signup_team.choose": "あなたがメンバーになっているチーム: ",
"signup_team.createTeam": "またはチームを作成する",
"signup_team.disabled": "チーム作成機能は無効化されています。使用するにはシステム管理者に連絡してください。",
+ "signup_team.join_open": "参加可能なオープンチーム: ",
"signup_team.noTeams": "チーム一覧に含まれるチームはありません。また、チーム作成機能は無効化されています。",
+ "signup_team.no_teams": "あなたはどのチームのメンバーでもありません。管理者に招待してもらうか、オープンチームが存在すればそれに参加するか、新しいチームを作成してください。",
"signup_team.none": "チーム作成方法が有効になっていません。使用するにはシステム管理者に連絡してください。",
"signup_team_complete.completed": "この招待について、既に手続きを完了しているか、有効期限が切れています。",
"signup_team_confirm.checkEmail": "あなたの電子メールアドレス<strong>{email}</strong>を確認してください。<br />電子メールにチームを設置するためのリンクがあります。",
@@ -1143,7 +1144,9 @@
"signup_user_completed.expired": "この招待について、既に手続きを完了しているか、有効期限が切れています。",
"signup_user_completed.gitlab": "GitLabでログインする",
"signup_user_completed.google": "Googleでログインする",
+ "signup_user_completed.invalid_invite": "招待リンクが不正です。管理者に連絡してください。",
"signup_user_completed.lets": "アカウントを作成しましょう",
+ "signup_user_completed.no_open_server": "このサーバーは誰でも利用登録できるように設定されていません。管理者に招待してもらってください。",
"signup_user_completed.none": "ユーザー作成方法が有効になっていません。使用するにはシステム管理者に連絡してください。",
"signup_user_completed.onSite": "{siteName}にて",
"signup_user_completed.or": "または",
@@ -1262,13 +1265,17 @@
"user.settings.developer.thirdParty": "新しいサードパーティーのアプリケーションを登録するために開く",
"user.settings.developer.title": "開発者の設定",
"user.settings.display.channelDisplayTitle": "チャンネル表示",
- "user.settings.display.channeldisplaymode": "チャンネルのテキストの表示スタイルを選択してください。",
+ "user.settings.display.channeldisplaymode": "中央のチャンネルの幅を選択してください。",
"user.settings.display.clockDisplay": "時計表示",
"user.settings.display.fixedWidthCentered": "固定幅、中央寄せ",
"user.settings.display.fontDesc": "Mattermostユーザーインターフェイスで使うフォントを選択してください。",
"user.settings.display.fontTitle": "表示フォント",
"user.settings.display.fullScreen": "全幅",
"user.settings.display.language": "言語",
+ "user.settings.display.messageDisplayClean": "クリーン",
+ "user.settings.display.messageDisplayCompact": "コンパクト",
+ "user.settings.display.messageDisplayDescription": "チャンネルでメッセージがどのように表示されるか選択してください。",
+ "user.settings.display.messageDisplayTitle": "メッセージの表示",
"user.settings.display.militaryClock": "時計の24時間表示(例: 16:00)",
"user.settings.display.nameOptsDesc": "投稿やダイレクトメッセージ中の他のユーザーの名前の表示方法を設定します。",
"user.settings.display.normalClock": "時計の12時間表示(例: 4:00 PM)",
@@ -1315,7 +1322,9 @@
"user.settings.general.title": "全般の設定",
"user.settings.general.uploadImage": "画像をアップロードするには「編集する」をクリックしてください",
"user.settings.general.username": "ユーザー名",
+ "user.settings.general.usernameInfo": "チームメイトが理解しやすく、覚えやすいものを選んでください。",
"user.settings.general.usernameReserved": "このユーザー名は予約されています。他のユーザー名を使ってください。",
+ "user.settings.general.usernameRestrictions": "ユーザー名は英小文字で始めてください。また{min}から{max} 文字の英数字と'.'、'-'、'_'の記号だけで構成してください。",
"user.settings.general.validEmail": "有効な電子メールアドレスを入力してください",
"user.settings.general.validImage": "JPGまたはPNG画像だけがプロフィール画像として使用できます",
"user.settings.import_theme.cancel": "キャンセル",
@@ -1325,9 +1334,12 @@
"user.settings.import_theme.submitError": "不正な形式です。もう一度コピーアンドペーストしてください。",
"user.settings.languages.change": "インターフェイスの言語を変更する",
"user.settings.mfa.add": "MFAをあなたのアカウントに追加する",
- "user.settings.mfa.addHelpQr": "QRコードをスマートフォン上のGoogle Authenticatorでスキャンしてください。その上で表示されたトークンをここに入力してください。",
+ "user.settings.mfa.addHelp": "Mattermostにサインインするのに、パスワードに加え、スマートフォンを使ったトークンの入力が必要です。<br/><br/>有効にするには、<a target='_blank' href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8'>iTunes</a>または<a target='_blank' href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en'>Google Play</a>から、Google Authenicatorをダウンロードしてください。<br/><br/>1. 上の<strong>MFAをアカウントに追加する</strong>ボタンを押してください。<br/>2. 表示されるQRコードをGoogle Authenticatorでスキャンしてください。<br/>3. Google Authenticatorの生成したトークンを入力し、<strong>保存</strong>ボタンを押してください。<br/><br/>ログインする際には、通常の認証情報に加え、Google Authenticatorの生成するトークンが求められます。",
+ "user.settings.mfa.addHelpQr": "QRコードをスマートフォン上のGoogle Authenticatorアプリでスキャンしてください。その上で表示されたトークンをここに入力してください。",
+ "user.settings.mfa.enterToken": "トークン(メンバーのみ)",
"user.settings.mfa.qrCode": "QRコード",
"user.settings.mfa.remove": "あなたのアカウントからMFAを削除する",
+ "user.settings.mfa.removeHelp": "多要素認証を削除すると、サインインする際にスマートフォンを使ったパスコードの入力は不要になります。",
"user.settings.modal.advanced": "詳細",
"user.settings.modal.confirmBtns": "破棄します",
"user.settings.modal.confirmMsg": "保存されていない変更があります。変更を破棄しますか?",
@@ -1339,6 +1351,7 @@
"user.settings.modal.security": "セキュリティー",
"user.settings.modal.title": "アカウントの設定",
"user.settings.notification.allActivity": "全てのアクティビティーについて",
+ "user.settings.notification.push": "モバイルプッシュ通知",
"user.settings.notification.soundConfig": "ブラウザーの設定画面で、通知音について設定してください",
"user.settings.notifications.channelWide": "チャンネル全体についての「@channel」",
"user.settings.notifications.close": "閉じる",
@@ -1361,6 +1374,10 @@
"user.settings.notifications.title": "通知の設定",
"user.settings.notifications.usernameMention": "あなたのユーザー名についての「@{username}」",
"user.settings.notifications.wordsTrigger": "誰かについての投稿となる単語",
+ "user.settings.push_notification.allActivity": "全てのアクティビティーについて",
+ "user.settings.push_notification.info": "Mattermostにアクティビティーがあると、あなたのモバイル端末に通知がプッシュされます。",
+ "user.settings.push_notification.off": "オフ",
+ "user.settings.push_notification.onlyMentions": "あなたについての投稿とダイレクトメッセージに関して",
"user.settings.security.close": "閉じる",
"user.settings.security.currentPassword": "現在のパスワード",
"user.settings.security.currentPasswordError": "現在のパスワードを入力してください",
@@ -1370,7 +1387,7 @@
"user.settings.security.ldap": "LDAP",
"user.settings.security.loginGitlab": "GitLabでログインする",
"user.settings.security.loginLdap": "LDAPでログインする",
- "user.settings.security.logoutActiveSessions": "アクティブなセッションを見るてログアウトする",
+ "user.settings.security.logoutActiveSessions": "アクティブなセッションを見てログアウトする",
"user.settings.security.method": "サインイン方法",
"user.settings.security.newPassword": "新しいパスワード",
"user.settings.security.oneSignin": "一度に一つのサインイン方法のみが選択可能です。サインイン方法を変更すると、変更完了時に電子メールで通知が送られます。",
@@ -1386,7 +1403,7 @@
"user.settings.security.switchLdap": "LDAPシングルサインオンに切り替える",
"user.settings.security.title": "セキュリティーの設定",
"user.settings.security.viewHistory": "アクセス履歴を見る",
- "user_list.notFound": "ユーザーが見付かりません :(",
+ "user_list.notFound": "ユーザーが見付かりません",
"user_profile.notShared": "電子メールは共有されません",
"view_image.loading": "読み込み中です ",
"view_image_popover.download": "ダウンロードする",
@@ -1397,5 +1414,6 @@
"web.footer.privacy": "プライバシー",
"web.footer.terms": "使用条件",
"web.header.back": "戻る",
- "web.root.singup_info": "あなたのチームの全てのコミュニケーションを一箇所で、すぐに検索可能で、どこからでもアクセスできます。"
+ "web.root.singup_info": "あなたのチームの全てのコミュニケーションを一箇所で、すぐに検索可能で、どこからでもアクセスできます。",
+ "youtube_video.notFound": "ビデオが見つかりません"
}
diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json
index 88e2a59c3..c7ef183af 100644
--- a/webapp/i18n/pt.json
+++ b/webapp/i18n/pt.json
@@ -6,8 +6,9 @@
"about.enterpriseEditionSt": "Moderna comunicação empresarial atrás do seu firewall.",
"about.enterpriseEditione1": "Enterprise Edition",
"about.hash": "Hash de Compilação:",
- "about.licensed": "Licenciado pela:",
- "about.number": "O Número De Compilação:",
+ "about.hashee": "Hash de Compilação EE:",
+ "about.licensed": "Licenciado por:",
+ "about.number": "O Número de Compilação:",
"about.teamEditionLearn": "Junte-se a comunidade Mattermost em ",
"about.teamEditionSt": "Toda comunicação da sua equipe em um só lugar, instantaneamente pesquisável e acessível em qualquer lugar.",
"about.teamEditiont0": "Team Edition",
@@ -17,7 +18,7 @@
"access_history.title": "Histórico de Acesso",
"activity_log.activeSessions": "Sessões ativas",
"activity_log.browser": "Browser: {browser}",
- "activity_log.firstTime": "Primeira vez atico: {date}, {time}",
+ "activity_log.firstTime": "Primeira vez ativo: {date}, {time}",
"activity_log.lastActivity": "Última atividade: {date}, {time}",
"activity_log.logout": "Logout",
"activity_log.moreInfo": "Mais informações",
@@ -25,7 +26,7 @@
"activity_log.sessionId": "ID da Sessão: {id}",
"activity_log.sessionsDescription": "Sessões são criadas quando você efetuar login em um novo navegador em um dispositivo. Sessões permitem que você use Mattermost sem ter que logar novamente por um período de tempo especificado pelo administrador do sistema. Se você deseja sair mais cedo, use o botão 'Logout' abaixo para terminar uma sessão.",
"activity_log_modal.android": "Android",
- "activity_log_modal.androidNativeApp": "App Nativo Android",
+ "activity_log_modal.androidNativeApp": "App Nativo para Android",
"activity_log_modal.iphoneNativeApp": "App Nativo para iPhone",
"add_command.autocomplete": "Autocompletar",
"add_command.autocomplete.help": " Mostrar este comando na lista de preenchimento automático.",
@@ -36,21 +37,24 @@
"add_command.autocompleteHint.help": "Sugestão opcional na lista autocompletada sobre os parâmetros necessários para o comando.",
"add_command.autocompleteHint.placeholder": "Exemplo: [Nome Do Paciente]",
"add_command.description": "Descrição",
- "add_command.displayName": "Nome De Exibição",
+ "add_command.displayName": "Nome de Exibição",
"add_command.header": "Adicionar",
"add_command.iconUrl": "Ícone de Resposta",
"add_command.iconUrl.help": "Escolha uma imagem do perfil para substituir as respostas dos posts deste comando slash. Digite a URL de um arquivo .png ou .jpg com pelo menos 128 pixels por 128 pixels.",
"add_command.iconUrl.placeholder": "https://www.example.com/myicon.png",
- "add_command.method": "Método de Requisição",
+ "add_command.method": "Método da Requisição",
"add_command.method.get": "GET",
"add_command.method.help": "O tipo de solicitação do comando emitido para a URL requisitada.",
"add_command.method.post": "POST",
"add_command.trigger": "Comando Palavra Gatilho",
- "add_command.trigger.help1": "Exemplos: /paciente, /cliente, /funcionario",
- "add_command.trigger.help2": "Reservados: /echo, /join, /logout, /me, /shrug",
- "add_command.trigger.placeholder": "Comando de gatilho ex. \"hello\", não incluí a barra",
+ "add_command.trigger.help1": "Exemplos: paciente, cliente, funcionario",
+ "add_command.trigger.help2": "Reservados: echo, join, logout, me, shrug",
+ "add_command.trigger.placeholder": "Comando gatilho ex. \"ola\"",
+ "add_command.triggerInvalidLength": "Uma palavra gatilho precisa conter entre {min} e {max} caracteres",
+ "add_command.triggerInvalidSlash": "Uma palavra gatilho não pode começar com /",
+ "add_command.triggerInvalidSpace": "Uma palavra gatilho não pode conter espaços",
"add_command.triggerRequired": "Uma palavra gatilho é necessária",
- "add_command.url": "URL de solicitação",
+ "add_command.url": "URL da solicitação",
"add_command.url.help": "A URL callback para receber o evento HTTP POST ou GET quando o comando slash for executado.",
"add_command.url.placeholder": "Deve começar com http:// ou https://",
"add_command.urlRequired": "Uma URL de requisição é necessária",
@@ -77,7 +81,7 @@
"add_outgoing_webhook.triggerWordsOrChannelRequired": "Um canal válido ou uma lista de palavras gatilho é necessário",
"admin.audits.reload": "Recarregar",
"admin.audits.title": "Atividade de Usuário",
- "admin.compliance.directoryDescription": "Diretório o qual os relatórios compliance são gravados, Se estiver em branco, será usado ./data/.",
+ "admin.compliance.directoryDescription": "Diretório o qual os relatórios compliance são gravados. Se estiver em branco, será usado ./data/.",
"admin.compliance.directoryExample": "Ex \"./data/\"",
"admin.compliance.directoryTitle": "Localização do Diretório de Compliance:",
"admin.compliance.enableDailyDesc": "Quando verdadeiro, Mattermost irá gerar um relatório diário de compliance.",
@@ -91,18 +95,18 @@
"admin.compliance.title": "Configurações Compliance",
"admin.compliance.true": "verdadeiro",
"admin.compliance_reports.desc": "Nome da Tarefa:",
- "admin.compliance_reports.desc_placeholder": "Ex \"Audit 445 for HR\"",
+ "admin.compliance_reports.desc_placeholder": "Ex: \"Audit 445 for HR\"",
"admin.compliance_reports.emails": "Emails:",
- "admin.compliance_reports.emails_placeholder": "Ex \"bill@example.com, bob@example.com\"",
+ "admin.compliance_reports.emails_placeholder": "Ex: \"bill@example.com, bob@example.com\"",
"admin.compliance_reports.from": "De:",
- "admin.compliance_reports.from_placeholder": "Ex \"2016-03-11\"",
+ "admin.compliance_reports.from_placeholder": "Ex: \"2016-03-11\"",
"admin.compliance_reports.keywords": "Palavras-chave:",
- "admin.compliance_reports.keywords_placeholder": "Ex \"diminuir estoque\"",
+ "admin.compliance_reports.keywords_placeholder": "Ex: \"diminuir estoque\"",
"admin.compliance_reports.reload": "Recarregar",
"admin.compliance_reports.run": "Executar",
"admin.compliance_reports.title": "Relatórios Compliance",
"admin.compliance_reports.to": "Para:",
- "admin.compliance_reports.to_placeholder": "Ex \"2016-03-15\"",
+ "admin.compliance_reports.to_placeholder": "Ex: \"2016-03-15\"",
"admin.compliance_table.desc": "Descrição",
"admin.compliance_table.download": "Download",
"admin.compliance_table.params": "Parâmetros",
@@ -128,9 +132,7 @@
"admin.email.allowUsernameSignInTitle": "Permitir Login Com Usuário: ",
"admin.email.easHelp": "Leia mais sobre compilar e publicar seu próprio aplicativo móvel em <a href=\"http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas\" target=\"_blank\">Enterprise App Store</a>.",
"admin.email.emailFail": "Conexão falhou: {error}",
- "admin.email.emailSettings": "Configuração do e-mail",
"admin.email.emailSuccess": "Nenhum erro foram relatados durante o envio de um e-mail. Por favor verifique a sua caixa de entrada para se certificar.",
- "admin.email.false": "falso",
"admin.email.fullPushNotification": "Enviar trecho de mensagem",
"admin.email.genericPushNotification": "Enviar descrição genérica com nomes do usuário e canal",
"admin.email.inviteSaltDescription": "32-caracteres salt adicionados a assinatura de convites por e-mail. Aleatoriamente gerados na instalação. Click \"Re-Gerar\" para criar um novo salt.",
@@ -160,11 +162,8 @@
"admin.email.pushServerEx": "Ex: \"http://push-test.mattermost.com\"",
"admin.email.pushServerTitle": "Servidor de Notificação Push:",
"admin.email.pushTitle": "Enviar Notificações Push: ",
- "admin.email.regenerate": "Re-Gerar",
"admin.email.requireVerificationDescription": "Normalmente definido como verdadeiro em produção. Quando verdadeiro, Mattermost requer a verificação de e-mail após a criação da conta antes de permitir login. Os desenvolvedores podem definir este campo como falso para ignorar o envio de e-mails de verificação para o desenvolvimento mais rápido.",
"admin.email.requireVerificationTitle": "Requer Verificação de E-mail: ",
- "admin.email.save": "Salvar",
- "admin.email.saving": "Salvando Config...",
"admin.email.selfPush": "Manualmente entre a localização do Serviço de Notificação Push",
"admin.email.smtpPasswordDescription": " Obter essa credencial do administrador das configurações do servidor de email.",
"admin.email.smtpPasswordExample": "Ex: \"suasenha\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
@@ -179,9 +178,9 @@
"admin.email.smtpUsernameExample": "Ex: \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
"admin.email.smtpUsernameTitle": "Usuário SMTP:",
"admin.email.testing": "Testando...",
- "admin.email.true": "verdadeiro",
+ "admin.false": "false",
"admin.gitab.clientSecretDescription": "Obter este valor de acordo com as instruções acima para logar no GitLab.",
- "admin.gitlab.EnableHtmlDesc": "<ol><li>Faça login na sua conta do GitLab e vá para Configurações do Perfil -> Aplicativos.</li><li>Digite redirecionamento URIs \"<your-mattermost-url>/login/gitlab/complete\" (exemplo: http://localhost:8065/login/gitlab/complete) e \"<your-mattermost-url>/signup/gitlab/complete\".</li><li>Em seguida, use os campos \"Secret\" e \"Id\" do Gitlab para completar as opções abaixo.</li><li>Complete o Endpoint com as URLs abaixo. </li></ol>",
+ "admin.gitlab.EnableHtmlDesc": "<ol><li>Faça login na sua conta do GitLab e vá para Configurações do Perfil -> Aplicativos.</li><li>Digite redirecionamento URIs \"<your-mattermost-url>/login/gitlab/complete\" (exemplo: http://localhost:8065/login/gitlab/complete) e \"<your-mattermost-url>/signup/gitlab/complete\".</li><li>Em seguida use os campos \"Secret\" e \"Id\" do Gitlab para completar as opções abaixo.</li><li>Complete o Endpoint com as URLs abaixo. </li></ol>",
"admin.gitlab.authDescription": "Entre https://<your-gitlab-url>/oauth/authorize (exemplo https://example.com:3000/oauth/authorize). Tenha certeza de usar HTTP ou HTTPS na sua URL dependendo da configuração do seu servidor.",
"admin.gitlab.authExample": "Ex \"\"",
"admin.gitlab.authTitle": "Autenticação Endpoint:",
@@ -192,14 +191,10 @@
"admin.gitlab.clientSecretTitle": "Segredo:",
"admin.gitlab.enableDescription": "Quando verdadeiro, Mattermost permite a criação de equipes e inscrições de conta usando GitLab OAuth.",
"admin.gitlab.enableTitle": "Permitir Inscrição Com GitLab: ",
- "admin.gitlab.false": "falso",
- "admin.gitlab.save": "Salvar",
- "admin.gitlab.saving": "Salvando Config...",
"admin.gitlab.settingsTitle": "Configurações GitLab",
"admin.gitlab.tokenDescription": "Digite https://<your-gitlab-url>/oauth/token. Certifique-se de usar HTTP ou HTTPS na sua URL dependendo da configuração de seu servidor.",
"admin.gitlab.tokenExample": "Ex \"\"",
"admin.gitlab.tokenTitle": "Token Endpoint:",
- "admin.gitlab.true": "verdadeiro",
"admin.gitlab.userDescription": "Digite https://<your-gitlab-url>/api/v3/user. Certifique-se de usar HTTP ou HTTPS na sua URL dependendo da configuração de seu servidor.",
"admin.gitlab.userExample": "Ex \"\"",
"admin.gitlab.userTitle": "API Usuário Endpoint:",
@@ -215,9 +210,7 @@
"admin.image.amazonS3SecretDescription": "Obter essa credencial do seu administrador Amazon EC2.",
"admin.image.amazonS3SecretExample": "Ex \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.image.amazonS3SecretTitle": "Amazon S3 Secret Access Key:",
- "admin.image.false": "falso",
- "admin.image.fileSettings": "Configurações do arquivo",
- "admin.image.localDescription": "Diretório o qual os arquivos de imagens são gravados, Se estiver em branco, será usado ./data/.",
+ "admin.image.localDescription": "Diretório o qual os arquivos de imagens são gravados. Se estiver em branco, será usado ./data/.",
"admin.image.localExample": "Ex \"./data/\"",
"admin.image.localTitle": "Localização do Diretório Local:",
"admin.image.previewHeightDescription": "Altura máxima da imagem de pré-visualização (\"0\": Define como tamanho automático). Atualizando este valor muda como as imagens de pré-visualização serão exibidas no futuro, mas não altera as imagens já criadas.",
@@ -235,9 +228,6 @@
"admin.image.publicLinkDescription": "32-caracteres salt adicionados a assinatura de links de imagens públicas. Aleatoriamente gerados na instalação. Click \"Re-Gerar\" para criar um novo salt.",
"admin.image.publicLinkExample": "Ex \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
"admin.image.publicLinkTitle": "Link Público Salt:",
- "admin.image.regenerate": "Re-Gerar",
- "admin.image.save": "Salvar",
- "admin.image.saving": "Salvando Configurações...",
"admin.image.shareDescription": "Permitir aos usuários compartilhar links públicos para arquivos e imagens.",
"admin.image.shareTitle": "Compartilhar Link Arquivo: ",
"admin.image.storeAmazonS3": "Amazon S3",
@@ -249,7 +239,6 @@
"admin.image.thumbWidthDescription": "Largura dos thumbnails gerados das imagens enviadas. Atualizando este valor muda como o thumbnail das imagens serão geradas no futuro, mas não muda as imagens já criadas no passado.",
"admin.image.thumbWidthExample": "Ex \"120\"",
"admin.image.thumbWidthTitle": "Largura do Thumbnail:",
- "admin.image.true": "verdadeiro",
"admin.ldap.bannerDesc": "Se um atributo de usuário de mudar no servidor LDAP, ele será atualizado na próxima vez que o usuário inserir suas credenciais para iniciar sessão no Mattermost. Isso inclui se um usuário estiver inativo ou removido de um servidor LDAP. Sincronização com servidores LDAP está prevista para um lançamento futuro.",
"admin.ldap.bannerHeading": "Nota:",
"admin.ldap.baseDesc": "Base DN é o nome distinto do local onde Mattermost deve começar sua busca para os usuários na árvore LDAP.",
@@ -264,7 +253,6 @@
"admin.ldap.emailAttrTitle": "Atributo de E-mail:",
"admin.ldap.enableDesc": "Quando verdadeiro, Mattermost permite login utilizando LDAP",
"admin.ldap.enableTitle": "Ativar Login With LDAP:",
- "admin.ldap.false": "falso",
"admin.ldap.firstnameAttrDesc": "O atributo no servidor LDAP será usado para preencher o primeiro nome dos usuários no Mattermost.",
"admin.ldap.firstnameAttrEx": "Ex \"givenName\"",
"admin.ldap.firstnameAttrTitle": "Primeiro Nome do Atributo",
@@ -287,15 +275,11 @@
"admin.ldap.queryDesc": "O valor de tempo limite para consultas para o servidor LDAP. Aumentar se você está recebendo erros de tempo limite causados por um servidor LDAP lento.",
"admin.ldap.queryEx": "Ex \"60\"",
"admin.ldap.queryTitle": "Tempo limite de Consulta (segundos):",
- "admin.ldap.save": "Salvar",
- "admin.ldap.saving": "Salvando Config...",
"admin.ldap.serverDesc": "O domínio ou o endereço IP do servidor LDAP.",
"admin.ldap.serverEx": "Ex \"10.0.0.23\"",
"admin.ldap.serverTitle": "Servidor LDAP:",
"admin.ldap.skipCertificateVerification": "Pular a Verificação do Certificado",
"admin.ldap.skipCertificateVerificationDesc": "Pula a etapa de verificação do certificado para conexões TLS ou STARTTLS. Não recomentado para ambientes de produção onde TLS é necessário. Apenas para teste.",
- "admin.ldap.title": "Configurações LDAP",
- "admin.ldap.true": "verdadeiro",
"admin.ldap.uernameAttrDesc": "O atributo no servidor LDAP que será usado para preencher o campo nome de usuário no Mattermost. Este pode ser o mesmo que o Atributo ID.",
"admin.ldap.userFilterDisc": "Opcionalmente, insira um filtro LDAP para usar ao procurar por objetos de usuário. Somente os usuários selecionados pela consulta serão capazes de acessar o Mattermost. Para o Active Directory, a consulta para filtrar os usuários desativados é (&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).",
"admin.ldap.userFilterEx": "Ex. \"(objectClass=user)\"",
@@ -316,7 +300,6 @@
"admin.license.uploading": "Enviando Licença...",
"admin.log.consoleDescription": "Normalmente definido como falso em produção. Os desenvolvedores podem definir este campo como verdadeiro para mensagens de log de saída no console baseado na opção de nível de console. Se verdadeiro, o servidor escreve mensagens para o fluxo de saída padrão (stdout).",
"admin.log.consoleTitle": "Log Para o Console: ",
- "admin.log.false": "falso",
"admin.log.fileDescription": "Normalmente definido como verdadeiro em produção. Quando verdadeiro, arquivos de log são gravados no arquivo de log especificado no campo de localização abaixo.",
"admin.log.fileLevelDescription": "Esta configuração determina o nível de detalhe que são gravados no log de eventos no console. ERROR: Saídas somente mensagens de erro. INFO: Saídas de mensagens de erro e informações em torno de inicialização. DEBUG: Impressões de alto detalhe para desenvolvedores que trabalham na depuração de problemas.",
"admin.log.fileLevelTitle": "Nível do Arquivo de Log:",
@@ -336,27 +319,18 @@
"admin.log.locationPlaceholder": "Entre a localização do seu arquivo",
"admin.log.locationTitle": "Local do Arquivo:",
"admin.log.logSettings": "Configurações de Log",
- "admin.log.save": "Salvar",
- "admin.log.saving": "Salvando Config...",
- "admin.log.true": "verdadeiro",
"admin.logs.reload": "Recarregar",
"admin.logs.title": "Log do Servidor",
"admin.nav.help": "Ajuda",
"admin.nav.logout": "Sair",
"admin.nav.report": "Relatar um Problema",
- "admin.nav.switch": "Mudar para {display_name}",
- "admin.privacy.false": "falso",
- "admin.privacy.save": "Salvar",
- "admin.privacy.saving": "Salvando Config...",
+ "admin.nav.switch": "Trocar Equipe",
"admin.privacy.showEmailDescription": "Quando falso, esconde endereço de e-mail dos outros usuários na interface do usuário, incluindo donos da equipe e administradores da equipe. Usado quando o sistema está configurado para gerenciar equipes onde alguns usuários optam por manter suas informações de contato privado.",
"admin.privacy.showEmailTitle": "Mostrar Endereços de Email: ",
"admin.privacy.showFullNameDescription": "Quando falso, oculta o nome completo dos usuários dos outros usuários, incluindo donos de equipe e administradores de equipe. Nome de usuário é mostrado no lugar do nome completo.",
"admin.privacy.showFullNameTitle": "Mostrar Nome Completo: ",
- "admin.privacy.title": "Configurações De Privacidade",
- "admin.privacy.true": "verdadeiro",
"admin.rate.enableLimiterDescription": "Quando verdadeiro, as APIs são estranguladas para as taxas especificadas abaixo.",
"admin.rate.enableLimiterTitle": "Ativar Rate Limiter: ",
- "admin.rate.false": "falso",
"admin.rate.httpHeaderDescription": "Quando preenchido, variam limitação de taxa pelo compo cabeçalho HTTP especificado (ex. quando configurado NGINX ajustado para \"X-Real-IP\", quando configurado AmazonELB ajustado para \"X-Forwarded-For\").",
"admin.rate.httpHeaderExample": "Ex \"X-Real-IP\", \"X-Forwarded-For\"",
"admin.rate.httpHeaderTitle": "Varia Pelo Cabeçalho HTTP:",
@@ -370,10 +344,13 @@
"admin.rate.queriesTitle": "Número De Consultas Por Segundo:",
"admin.rate.remoteDescription": "Quando verdadeiro, taxa limite de acesso de API por endereço de IP.",
"admin.rate.remoteTitle": "Varia Por Endereço Remoto: ",
- "admin.rate.save": "Salvar",
- "admin.rate.saving": "Salvando Config...",
- "admin.rate.title": "Configurações Rate Limit",
- "admin.rate.true": "verdadeiro",
+ "admin.recycle.button": "Recycle Database Connections",
+ "admin.recycle.loading": " Recycling...",
+ "admin.recycle.reloadFail": "Recycling unsuccessful: {error}",
+ "admin.regenerate": "Re-Generate",
+ "admin.reload.button": "Reload Configuration From Disk",
+ "admin.reload.loading": " Loading...",
+ "admin.reload.reloadFail": "Reloading unsuccessful: {error}",
"admin.reset_password.close": "Fechar",
"admin.reset_password.newPassword": "Nova Senha",
"admin.reset_password.select": "Selecionar",
@@ -393,7 +370,6 @@
"admin.service.corsTitle": "Permitir Requisição Cross-origin de:",
"admin.service.developerDesc": "(Opção dos desenvolvedores) Quando verdadeira, a informação extra em torno dos erros será exibida na UI.",
"admin.service.developerTitle": "Ativar o Modo Desenvolvedor: ",
- "admin.service.false": "falso",
"admin.service.googleDescription": "Defina esta chave para permitir a incorporação de pré-visualizações de vídeo do YouTube com base em hiperlinks que aparecem nas mensagens ou comentários. Instruções para obter uma chave disponível em <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Deixando o campo em branco desabilita a geração automática de pré-visualizações de vídeo do YouTube a partir de links.",
"admin.service.googleExample": "Ex \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"",
"admin.service.googleTitle": "Chave do Google Developer:",
@@ -414,8 +390,6 @@
"admin.service.outWebhooksTitle": "Ativar Webhooks Saída: ",
"admin.service.overrideDescription": "Quando verdadeiro, será permitido webhooks e comandos slash mudar o icone com que eles postam. Note, combinado com permitir substituição de usuário, isto pode permitir que o usuário faça ataque phishing.",
"admin.service.overrideTitle": "Ativar Sobrescrever os Nomes de usuário a partir de Webhooks e Comandos Slash: ",
- "admin.service.save": "Salvar",
- "admin.service.saving": "Salvando Config...",
"admin.service.securityDesc": "Quando verdadeiro, os Administradores de Sistema são notificados por e-mail se uma relevante correção de segurança foi anunciado nos últimos 12 horas. Requer o e-mail para ser ativado.",
"admin.service.securityTitle": "Ativar Alertas de Segurança: ",
"admin.service.segmentDescription": "Para usuários usando um serviço SaaS, inscrever-se para uma chave no Segment.com para acompanhar métricas.",
@@ -428,41 +402,55 @@
"admin.service.ssoSessionDaysDesc": "A sessão SSO irá expirar depois do número de dias especificado e será requerido um usuário para logar novamente.",
"admin.service.testingDescription": "(Opção Desenvolvedor) Quando verdadeiro, comando slash /loadtest está habilitado para carregar contas e testar e dados. Mudar isso exigirá um reinício do servidor antes que tenha efeito.",
"admin.service.testingTitle": "Ativar Teste: ",
- "admin.service.title": "Configurações do Serviço",
- "admin.service.true": "verdadeiro",
"admin.service.webSessionDays": "Duração da Sessão em Dias para Web:",
"admin.service.webSessionDaysDesc": "A sessão web irá expirar depois do número de dias especificado e será requerido um usuário para logar novamente.",
"admin.service.webhooksDescription": "Quando verdadeiro, será permitido webhooks entrada. Para ajudar a combater os ataques de phishing, todos os posts de webhooks serão marcados por uma etiqueta BOT.",
"admin.service.webhooksTitle": "Ativar Webhooks Entrada: ",
"admin.sidebar.addTeamSidebar": "Adicionar equipe do menu lateral",
"admin.sidebar.audits": "Conformidade e Auditoria",
- "admin.sidebar.compliance": "Configurações Compliance",
- "admin.sidebar.email": "Configuração do e-mail",
- "admin.sidebar.file": "Configurações do arquivo",
- "admin.sidebar.gitlab": "Configurações GitLab",
- "admin.sidebar.ldap": "Configurações LDAP",
+ "admin.sidebar.authentication": "Autenticação",
+ "admin.sidebar.compliance": "Compliance",
+ "admin.sidebar.configuration": "Configuração",
+ "admin.sidebar.connections": "Conexões",
+ "admin.sidebar.customBrand": "Marca Personalizada",
+ "admin.sidebar.customization": "Customização",
+ "admin.sidebar.database": "Banco de dados",
+ "admin.sidebar.developer": "Desenvolvedor",
+ "admin.sidebar.email": "E-mail",
+ "admin.sidebar.external": "Serviços Externos",
+ "admin.sidebar.files": "Arquivos",
+ "admin.sidebar.general": "Geral",
+ "admin.sidebar.gitlab": "GitLab",
+ "admin.sidebar.images": "Imagens",
+ "admin.sidebar.integrations": "Integrações",
+ "admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Edição e Licença",
- "admin.sidebar.loading": "Carregando",
- "admin.sidebar.log": "Configurações de Log",
+ "admin.sidebar.logging": "Acessando",
+ "admin.sidebar.login": "Login",
"admin.sidebar.logs": "Logs",
+ "admin.sidebar.notifications": "Notificações",
"admin.sidebar.other": "OUTROS",
- "admin.sidebar.privacy": "Configurações De Privacidade",
- "admin.sidebar.rate_limit": "Configurações Rate Limit",
+ "admin.sidebar.privacy": "Privacidade",
+ "admin.sidebar.publicLinks": "Links Públicos",
+ "admin.sidebar.push": "Notificação Móvel",
+ "admin.sidebar.rateLimiting": "Rate Limiting",
"admin.sidebar.reports": "RELATÓRIOS DO SITE",
"admin.sidebar.rmTeamSidebar": "Remover equipe do menu lateral",
- "admin.sidebar.service": "Configurações do Serviço",
+ "admin.sidebar.security": "Segurança",
+ "admin.sidebar.sessions": "Sessões",
"admin.sidebar.settings": "CONFIGURAÇÕES",
- "admin.sidebar.sql": "Configurações SQL",
- "admin.sidebar.statistics": "- Estátisticas",
- "admin.sidebar.support": "Configurações jurídico e apoio",
- "admin.sidebar.team": "Configurações De Equipe",
- "admin.sidebar.teams": "EQUIPES ({count})",
- "admin.sidebar.users": "- Usuários",
+ "admin.sidebar.sign_up": "Inscrever",
+ "admin.sidebar.statistics": "Estátisticas",
+ "admin.sidebar.storage": "Armazenamento",
+ "admin.sidebar.support": "Legal e Suporte",
+ "admin.sidebar.teams": "EQUIPES ({count, number})",
+ "admin.sidebar.users": "Usuários",
+ "admin.sidebar.usersAndTeams": "Usuários e Equipes",
"admin.sidebar.view_statistics": "Ver Estatísticas",
+ "admin.sidebar.webhooks": "Webhooks e Comandos",
"admin.sidebarHeader.systemConsole": "Console do Sistema",
"admin.sql.dataSource": "Fonte de Dados:",
"admin.sql.driverName": "Nome do Driver:",
- "admin.sql.false": "falso",
"admin.sql.keyDescription": "32-caracteres de salt disponível para encriptar e desencriptar campos no banco de dados.",
"admin.sql.keyExample": "Ex \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
"admin.sql.keyTitle": "At Rest Encrypt Key:",
@@ -474,14 +462,9 @@
"admin.sql.maxOpenTitle": "Máximo de Conexões Abertas:",
"admin.sql.noteDescription": "Alterando as propriedades nesta seção irá exigir que o servidor seja reiniciado para que tenha efeito.",
"admin.sql.noteTitle": "Nota:",
- "admin.sql.regenerate": "Re-Gerar",
"admin.sql.replicas": "Replicas Fonte de Dados:",
- "admin.sql.save": "Salvar",
- "admin.sql.saving": "Salvando Configurações...",
- "admin.sql.title": "Configurações do SQL",
"admin.sql.traceDescription": "(Modo Desenvolvedor) Quando verdadeiro, execução de instruções SQL será gravado no log.",
"admin.sql.traceTitle": "Rastreamento: ",
- "admin.sql.true": "verdadeiro",
"admin.sql.warning": "Aviso: re-gerar este salt pode causar algumas colunas no banco de dados para retornar resultados vazios.",
"admin.support.aboutDesc": "Link para página Sobre para mais informações sobre a implantação do Mattermost, por exemplo a sua finalidade e público dentro de sua organização. Padrão página de informação Mattermost.",
"admin.support.aboutTitle": "Sobre link:",
@@ -495,11 +478,8 @@
"admin.support.privacyTitle": "Link da Política de Privacidade:",
"admin.support.problemDesc": "Link para a documentação de ajuda do site no menu principal. Por padrão este aponta para um fórum peer-to-peer de solução de problemas onde os usuários podem pesquisar, encontrar e pedir ajuda com problemas técnicos.",
"admin.support.problemTitle": "Link para Reportar um Problema:",
- "admin.support.save": "Salvar",
- "admin.support.saving": "Salvando Config...",
"admin.support.termsDesc": "Link para os Termos de Serviço para os usuários no desktop ou móvel. Deixando este espaço em branco irá esconder a opção de exibir um aviso.",
"admin.support.termsTitle": "Link Termos do Serviço:",
- "admin.support.title": "Configurações jurídico e apoio",
"admin.system_analytics.activeUsers": "Usuários Ativos com Postagens",
"admin.system_analytics.title": "o Sistema",
"admin.system_analytics.totalPosts": "Total Posts",
@@ -511,7 +491,6 @@
"admin.team.chooseImage": "Escolha Nova Imagem",
"admin.team.dirDesc": "Quando verdadeiro, as equipes que estão configuradas para mostrar o diretório de equipe irá mostrar na página principal, em lugar de criar uma nova equipe.",
"admin.team.dirTitle": "Ativar Diretório de Equipe: ",
- "admin.team.false": "falso",
"admin.team.maxUsersDescription": "Número máximo total de usuários por equipe, incluindo ambos usuários ativos e inativos.",
"admin.team.maxUsersExample": "Ex \"25\"",
"admin.team.maxUsersTitle": "Máximo Usuários Por Equipe:",
@@ -527,15 +506,11 @@
"admin.team.restrictTitle": "Restringir Criação Para os Domínios:",
"admin.team.restrict_direct_message_any": "Qualquer usuário no Servidor Mattermost",
"admin.team.restrict_direct_message_team": "Qualquer membro da equipe",
- "admin.team.save": "Salvar",
- "admin.team.saving": "Salvando Config...",
"admin.team.siteNameDescription": "Nome do serviço mostrado na tela de início da sessão e na UI.",
"admin.team.siteNameExample": "Ex \"Mattermost\"",
"admin.team.siteNameTitle": "Nome do Site:",
"admin.team.teamCreationDescription": "Quando falso, a capacidade de criar equipes é desativada. O botão criar equipe apresenta erro quando pressionado.",
"admin.team.teamCreationTitle": "Habilitar a Criação de Equipes: ",
- "admin.team.title": "Configurações da Equipe",
- "admin.team.true": "verdadeiro",
"admin.team.upload": "Enviar",
"admin.team.uploadDesc": "Personalizar sua experiência como usuário, adicionando uma imagem personalizada na tela de login. Veja exemplos em <a href='http://docs.mattermost.com/administration/config-settings.html#custom-branding' target='_blank'>docs.mattermost.com/administration/config-settings.html#custom-branding</a>.",
"admin.team.uploaded": "Enviado!",
@@ -544,6 +519,7 @@
"admin.team.userCreationTitle": "Permitir A Criação De Usuário: ",
"admin.team_analytics.activeUsers": "Usuários Ativos Com Postagens",
"admin.team_analytics.totalPosts": "Total Posts",
+ "admin.true": "true",
"admin.userList.title": "Usuários para {team}",
"admin.userList.title2": "Usuários para {team} ({count})",
"admin.user_item.authServiceEmail": ", <strong>Método de Login:</strong> Email",
@@ -876,7 +852,7 @@
"find_team.submitError": "Por favor entre um endereço de e-mail válido",
"general_tab.chooseName": "Por favor escolha um novo nome para sua equipe",
"general_tab.codeDesc": "Clique 'Edit' para re-gerar o Código de Convite.",
- "general_tab.codeLongDesc": "O Código de convite é usado como parte da URL no link de convite da equipe criado por **Obter Link de Convite de Equipe** no menu principal. Re-gerar cria um novo link de convite de equipe e invalida os link anteriores.",
+ "general_tab.codeLongDesc": "O Código de convite é usado como parte da URL no link de convite da equipe criado por <strong>Obter Link de Convite de Equipe</strong> no menu principal. Re-gerar cria um novo link de convite de equipe e invalida os link anteriores.",
"general_tab.codeTitle": "Código de Convite",
"general_tab.dirContact": "Contate o seu administrador do sistema para ativar o diretório de equipe na página inicial do sistema.",
"general_tab.dirDisabled": "Diretório de equipe foi desativado. Por favor peça a um Administrador de Sistema para ativar o Diretório de Equipe nas configurações do Console do Sistema.",
@@ -1025,6 +1001,7 @@
"navbar_dropdown.manageMembers": "Gerenciar Membros",
"navbar_dropdown.report": "Relatar um Problema",
"navbar_dropdown.switchTeam": "Mudar para {team}",
+ "navbar_dropdown.switchTo": "Mudar para ",
"navbar_dropdown.teamLink": "Obter Link para Convite de Equipe",
"navbar_dropdown.teamSettings": "Configurações da Equipe",
"password_form.change": "Alterar minha senha",
@@ -1041,14 +1018,15 @@
"password_send.link": "<p>Um link para resetar senha foi enviado para <b>{email}</b></p>",
"password_send.reset": "Resetar minha senha",
"password_send.title": "Resetar Senha",
+ "pending_post_actions.cancel": "Cancel",
+ "pending_post_actions.retry": "Retry",
"permalink.error.access": "Permalink pertence a um canal que você não tem acesso",
"post_attachment.collapse": "▲ recolher texto",
"post_attachment.more": "▼ leia mais",
- "post_body.commentedOn": "Comentado de {name}{apostrophe} mensagem: ",
+ "post_body.commentedOn": "Comentado da mensagem {name}{apostrophe}: ",
"post_body.deleted": "(mensagem deletada)",
"post_body.plusMore": " mais {count} outros arquivos",
"post_body.plusOne": " mais 1 outro arquivo",
- "post_body.retry": "Tentar novamente",
"post_delete.notPosted": "Comentário não pode ser postado",
"post_delete.okay": "Ok",
"post_delete.someone": "Alguém deletou a mensagem no qual você tentou postar um comentário.",
@@ -1098,7 +1076,6 @@
"rhs_comment.del": "Deletar",
"rhs_comment.edit": "Editar",
"rhs_comment.permalink": "Permalink",
- "rhs_comment.retry": "Tentar novamente",
"rhs_header.details": "Detalhes da Mensagem",
"rhs_root.del": "Deletar",
"rhs_root.direct": "Mensagem Direta",
@@ -1130,6 +1107,7 @@
"sidebar.direct": "Mensagens Diretas",
"sidebar.more": "Mais",
"sidebar.moreElips": "Mais...",
+ "sidebar.otherMembers": "Fora desta equipe",
"sidebar.pg": "Grupos Privados",
"sidebar.removeList": "Remover da lista",
"sidebar.tutorialScreen1": "<h4>Canais</h4><p><strong>Canais</strong> organizar conversas em diferentes tópicos. Eles estão abertos a todos em sua equipe. Para enviar comunicações privadas utilize <strong>Mensagens Diretas</strong> para uma única pessoa ou <strong>Grupos Privados</strong> para várias pessoas.</p>",
@@ -1287,13 +1265,17 @@
"user.settings.developer.thirdParty": "Abrir para registrar um novo aplicativo de terceiros",
"user.settings.developer.title": "Configurações de Desenvolvedor",
"user.settings.display.channelDisplayTitle": "Modo de Exibição do Canal",
- "user.settings.display.channeldisplaymode": "Selecione como o texto em um canal é mostrado.",
+ "user.settings.display.channeldisplaymode": "Selecione a largura do centro do canal.",
"user.settings.display.clockDisplay": "Exibição do Relógio",
"user.settings.display.fixedWidthCentered": "Largura fixa, centralizada",
"user.settings.display.fontDesc": "Selecione a fonte mostrada na interface do usuário no Mattermost.",
"user.settings.display.fontTitle": "Fonte Exibição",
"user.settings.display.fullScreen": "Largura inteira",
"user.settings.display.language": "Idioma",
+ "user.settings.display.messageDisplayClean": "Limpar",
+ "user.settings.display.messageDisplayCompact": "Compacto",
+ "user.settings.display.messageDisplayDescription": "Selecione como as mensagens no canal podem ser mostradas.",
+ "user.settings.display.messageDisplayTitle": "Mensagem de Exibição",
"user.settings.display.militaryClock": "Relógio de 24 horas (exemplo: 16:00)",
"user.settings.display.nameOptsDesc": "Ajustar como mostrar outros nomes de usuários nas postagens e lista de Mensagens Diretas.",
"user.settings.display.normalClock": "Relógio de 12 horas (exemplo: 4:00 PM)",
@@ -1340,7 +1322,9 @@
"user.settings.general.title": "Definições Gerais",
"user.settings.general.uploadImage": "Clique em 'Editar' para enviar uma imagem.",
"user.settings.general.username": "Usuário",
+ "user.settings.general.usernameInfo": "Coloque alguma coisa fácil para sua equipe reconhecer e relembrar.",
"user.settings.general.usernameReserved": "Este nome de usuário é reservado, por favor escolha um novo.",
+ "user.settings.general.usernameRestrictions": "O nome de usuário precisa começar com uma letra, e conter entre {min} e {max} caracteres minúsculos contendo números, letras, e os símbolos '.', '-' e '_'.",
"user.settings.general.validEmail": "Por favor entre um endereço de e-mail válido",
"user.settings.general.validImage": "Somente imagens em JPG ou PNG podem ser usadas como imagem do perfil",
"user.settings.import_theme.cancel": "Cancelar",
@@ -1367,6 +1351,7 @@
"user.settings.modal.security": "Segurança",
"user.settings.modal.title": "Definições de Conta",
"user.settings.notification.allActivity": "Para todas atividades",
+ "user.settings.notification.push": "Notificações push móvel",
"user.settings.notification.soundConfig": "Por favor configurar sons de notificações nas configurações do seu navegador",
"user.settings.notifications.channelWide": "Menção para todo canal \"@channel\"",
"user.settings.notifications.close": "Fechar",
@@ -1389,6 +1374,10 @@
"user.settings.notifications.title": "Configurações de Notificação",
"user.settings.notifications.usernameMention": "Seu usuário mencionado \"@{username}\"",
"user.settings.notifications.wordsTrigger": "Palavras que desencadeiam menções",
+ "user.settings.push_notification.allActivity": "Para todas as atividades",
+ "user.settings.push_notification.info": "Alertas de notificação são enviados para o seu dispositivo móvel quando há atividade no Mattermost.",
+ "user.settings.push_notification.off": "Desligado",
+ "user.settings.push_notification.onlyMentions": "Somente para menções e mensagens diretas",
"user.settings.security.close": "Fechar",
"user.settings.security.currentPassword": "Senha Atual",
"user.settings.security.currentPasswordError": "Por favor entre sua senha atual",
@@ -1425,5 +1414,6 @@
"web.footer.privacy": "Privacidade",
"web.footer.terms": "Termos",
"web.header.back": "Voltar",
- "web.root.singup_info": "Toda comunicação em um só lugar, pesquisável e acessível em qualquer lugar"
+ "web.root.singup_info": "Toda comunicação em um só lugar, pesquisável e acessível em qualquer lugar",
+ "youtube_video.notFound": "Vídeo não encontrado"
}
diff --git a/webapp/package.json b/webapp/package.json
index 9c0377cdd..deecbc1a8 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -1,56 +1,58 @@
{
- "name": "mattermost",
+ "name": "mattermost-webapp",
"version": "0.0.1",
"private": true,
"dependencies": {
"autolinker": "mattermost/Autolinker.js#9689831109e104d7b545318e54199e6de8fd9b87",
"bootstrap": "3.3.6",
- "bootstrap-colorpicker": "2.3.0",
- "chart.js": "1.0.2",
+ "bootstrap-colorpicker": "2.3.3",
+ "chart.js": "2.1.2",
"compass-mixins": "0.12.7",
"fastclick": "1.0.6",
"flux": "2.1.1",
- "font-awesome": "4.5.0",
- "highlight.js": "9.2.0",
+ "font-awesome": "4.6.1",
+ "highlight.js": "9.3.0",
"intl": "1.1.0",
"jasny-bootstrap": "3.1.3",
- "jquery": "2.2.1",
+ "jquery": "2.2.3",
"keymirror": "0.1.1",
- "marked": "mattermost/marked#cb85e5cc81bc7937dbb73c3c53d9532b1b97e3ca",
+ "marked": "mattermost/marked#a16c38a3c7cdaaefc33922812b42bdf8bcfaa127",
"match-at": "0.1.0",
- "object-assign": "4.0.1",
- "perfect-scrollbar": "0.6.10",
- "react": "0.14.7",
- "react-addons-pure-render-mixin": "0.14.7",
- "react-bootstrap": "0.28.3",
+ "mattermost": "mattermost/mattermost-javascript#master",
+ "match-at": "0.1.0",
+ "object-assign": "4.1.0",
+ "perfect-scrollbar": "0.6.11",
+ "react": "15.0.2",
+ "react-addons-pure-render-mixin": "15.0.2",
+ "react-bootstrap": "0.29.3",
"react-custom-scrollbars": "4.0.0-beta.1",
- "react-dom": "0.14.7",
+ "react-dom": "15.0.2",
"react-intl": "2.0.0-rc-1",
- "react-router": "2.0.1",
- "react-textarea-autosize": "3.3.0",
+ "react-router": "2.4.0",
+ "react-textarea-autosize": "4.0.1",
"superagent": "1.8.3",
"twemoji": "2.0.5",
"velocity-animate": "1.2.3"
},
"devDependencies": {
- "babel-eslint": "5.0.0",
+ "babel-eslint": "6.0.4",
"babel-loader": "6.2.4",
- "babel-plugin-transform-runtime": "6.6.0",
- "babel-polyfill": "6.7.2",
- "babel-preset-es2015-webpack": "6.4.0",
+ "babel-plugin-transform-runtime": "6.8.0",
+ "babel-polyfill": "6.8.0",
+ "babel-preset-es2015-webpack": "6.4.1",
"babel-preset-react": "6.5.0",
"babel-preset-stage-0": "6.5.0",
- "copy-webpack-plugin": "1.1.1",
+ "copy-webpack-plugin": "2.1.3",
"css-loader": "0.23.1",
- "eslint": "2.2.0",
- "eslint-plugin-react": "4.0.0",
+ "eslint": "2.9.0",
+ "eslint-plugin-react": "5.1.1",
"exports-loader": "0.6.3",
"extract-text-webpack-plugin": "1.0.1",
"file-loader": "0.8.5",
"html-loader": "0.4.3",
"imports-loader": "0.6.5",
"jquery-deferred": "0.3.0",
- "jsdom": "8.5.0",
+ "jsdom": "9.0.0",
"jsdom-global": "1.7.0",
"json-loader": "0.5.4",
"mocha": "2.4.5",
@@ -58,11 +60,11 @@
"mocha-webpack": "0.3.0",
"node-sass": "3.4.2",
"raw-loader": "0.5.1",
- "react-addons-test-utils": "0.14.7",
+ "react-addons-test-utils": "15.0.2",
"sass-loader": "3.2.0",
- "style-loader": "0.13.0",
+ "style-loader": "0.13.1",
"url-loader": "0.5.7",
- "webpack": "2.1.0-beta.5",
+ "webpack": "2.1.0-beta.7",
"webpack-node-externals": "1.2.0"
},
"scripts": {
@@ -70,6 +72,6 @@
"build": "NODE_ENV=production webpack",
"run": "NODE_ENV=production webpack --progress --watch",
"run-fullmap": "webpack --progress --watch",
- "test": "mocha-webpack --webpack-config webpack.config-test.js \"**/*.test.jsx\""
+ "test": "mocha-webpack --webpack-config webpack.config.js \"**/*.test.jsx\""
}
}
diff --git a/webapp/root.jsx b/webapp/root.jsx
index 2b54c2174..dc2df64ac 100644
--- a/webapp/root.jsx
+++ b/webapp/root.jsx
@@ -24,16 +24,16 @@ import * as AsyncClient from 'utils/async_client.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import ErrorStore from 'stores/error_store.jsx';
import TeamStore from 'stores/team_store.jsx';
+import BrowserStore from 'stores/browser_store.jsx';
import * as Utils from 'utils/utils.jsx';
import Client from 'utils/web_client.jsx';
-import * as Websockets from 'action_creators/websocket_actions.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as Websockets from 'actions/websocket_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import SignupUserComplete from 'components/signup_user_complete.jsx';
import ShouldVerifyEmail from 'components/should_verify_email.jsx';
import DoVerifyEmail from 'components/do_verify_email.jsx';
-import AdminConsole from 'components/admin_console/admin_controller.jsx';
import TutorialView from 'components/tutorial/tutorial_view.jsx';
import BackstageNavbar from 'components/backstage/backstage_navbar.jsx';
import BackstageSidebar from 'components/backstage/backstage_sidebar.jsx';
@@ -50,15 +50,47 @@ import AppDispatcher from './dispatcher/app_dispatcher.jsx';
import Constants from './utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import Claim from 'components/claim/claim.jsx';
+import AdminConsole from 'components/admin_console/admin_console.jsx';
+import SystemAnalytics from 'components/analytics/system_analytics.jsx';
+import ConfigurationSettings from 'components/admin_console/configuration_settings.jsx';
+import UsersAndTeamsSettings from 'components/admin_console/users_and_teams_settings.jsx';
+import PrivacySettings from 'components/admin_console/privacy_settings.jsx';
+import LogSettings from 'components/admin_console/log_settings.jsx';
+import EmailAuthenticationSettings from 'components/admin_console/email_authentication_settings.jsx';
+import GitLabSettings from 'components/admin_console/gitlab_settings.jsx';
+import LdapSettings from 'components/admin_console/ldap_settings.jsx';
+import SignupSettings from 'components/admin_console/signup_settings.jsx';
+import LoginSettings from 'components/admin_console/login_settings.jsx';
+import PublicLinkSettings from 'components/admin_console/public_link_settings.jsx';
+import SessionSettings from 'components/admin_console/session_settings.jsx';
+import ConnectionSettings from 'components/admin_console/connection_settings.jsx';
+import EmailSettings from 'components/admin_console/email_settings.jsx';
+import PushSettings from 'components/admin_console/push_settings.jsx';
+import WebhookSettings from 'components/admin_console/webhook_settings.jsx';
+import ExternalServiceSettings from 'components/admin_console/external_service_settings.jsx';
+import DatabaseSettings from 'components/admin_console/database_settings.jsx';
+import StorageSettings from 'components/admin_console/storage_settings.jsx';
+import ImageSettings from 'components/admin_console/image_settings.jsx';
+import CustomBrandSettings from 'components/admin_console/custom_brand_settings.jsx';
+import LegalAndSupportSettings from 'components/admin_console/legal_and_support_settings.jsx';
+import ComplianceSettings from 'components/admin_console/compliance_settings.jsx';
+import RateSettings from 'components/admin_console/rate_settings.jsx';
+import DeveloperSettings from 'components/admin_console/developer_settings.jsx';
+import TeamUsers from 'components/admin_console/team_users.jsx';
+import TeamAnalytics from 'components/analytics/team_analytics.jsx';
+import LicenseSettings from 'components/admin_console/license_settings.jsx';
+import Audits from 'components/admin_console/audits.jsx';
+import Logs from 'components/admin_console/logs.jsx';
+
+import ClaimController from 'components/claim/claim_controller.jsx';
import EmailToOAuth from 'components/claim/components/email_to_oauth.jsx';
import OAuthToEmail from 'components/claim/components/oauth_to_email.jsx';
import LDAPToEmail from 'components/claim/components/ldap_to_email.jsx';
import EmailToLDAP from 'components/claim/components/email_to_ldap.jsx';
-import Login from 'components/login/login.jsx';
+import LoginController from 'components/login/login_controller.jsx';
import SelectTeam from 'components/select_team/select_team.jsx';
-import CreateTeam from 'components/create_team/create_team.jsx';
+import CreateTeamController from 'components/create_team/create_team_controller.jsx';
import CreateTeamDisplayName from 'components/create_team/components/display_name.jsx';
import CreateTeamTeamUrl from 'components/create_team/components/team_url.jsx';
@@ -80,7 +112,7 @@ function preRenderSetup(callwhendone) {
l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url;
$.ajax({
- url: '/api/v3/admin/log_client',
+ url: '/api/v3/general/log_client',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
@@ -101,9 +133,10 @@ function preRenderSetup(callwhendone) {
}
);
- // Make sure the websockets close
+ // Make sure the websockets close and reset version
$(window).on('beforeunload',
() => {
+ BrowserStore.setLastServerVersion('');
Websockets.close();
}
);
@@ -246,7 +279,7 @@ function renderRootComponent() {
<Route component={HeaderFooterTemplate}>
<Route
path='login'
- component={Login}
+ component={LoginController}
/>
<Route
path='reset_password'
@@ -258,7 +291,7 @@ function renderRootComponent() {
/>
<Route
path='claim'
- component={Claim}
+ component={ClaimController}
>
<Route
path='oauth_to_email'
@@ -301,7 +334,7 @@ function renderRootComponent() {
/>
<Route
path='create_team'
- component={CreateTeam}
+ component={CreateTeamController}
>
<IndexRoute component={CreateTeamDisplayName}/>
<Route
@@ -317,7 +350,161 @@ function renderRootComponent() {
<Route
path='admin_console'
component={AdminConsole}
- />
+ >
+ <IndexRedirect to='system_analytics'/>
+ <Route
+ path='system_analytics'
+ component={SystemAnalytics}
+ />
+ <Route path='general'>
+ <IndexRedirect to='configuration'/>
+ <Route
+ path='configuration'
+ component={ConfigurationSettings}
+ />
+ <Route
+ path='users_and_teams'
+ component={UsersAndTeamsSettings}
+ />
+ <Route
+ path='privacy'
+ component={PrivacySettings}
+ />
+ <Route
+ path='logging'
+ component={LogSettings}
+ />
+ </Route>
+ <Route path='authentication'>
+ <IndexRedirect to='email'/>
+ <Route
+ path='email'
+ component={EmailAuthenticationSettings}
+ />
+ <Route
+ path='gitlab'
+ component={GitLabSettings}
+ />
+ <Route
+ path='ldap'
+ component={LdapSettings}
+ />
+ </Route>
+ <Route path='security'>
+ <IndexRedirect to='sign_up'/>
+ <Route
+ path='sign_up'
+ component={SignupSettings}
+ />
+ <Route
+ path='login'
+ component={LoginSettings}
+ />
+ <Route
+ path='public_links'
+ component={PublicLinkSettings}
+ />
+ <Route
+ path='sessions'
+ component={SessionSettings}
+ />
+ <Route
+ path='connections'
+ component={ConnectionSettings}
+ />
+ </Route>
+ <Route path='notifications'>
+ <IndexRedirect to='email'/>
+ <Route
+ path='email'
+ component={EmailSettings}
+ />
+ <Route
+ path='push'
+ component={PushSettings}
+ />
+ </Route>
+ <Route path='integrations'>
+ <IndexRedirect to='webhooks'/>
+ <Route
+ path='webhooks'
+ component={WebhookSettings}
+ />
+ <Route
+ path='external'
+ component={ExternalServiceSettings}
+ />
+ </Route>
+ <Route
+ path='database'
+ component={DatabaseSettings}
+ />
+ <Route path='files'>
+ <IndexRedirect to='storage'/>
+ <Route
+ path='storage'
+ component={StorageSettings}
+ />
+ <Route
+ path='images'
+ component={ImageSettings}
+ />
+ </Route>
+ <Route path='customization'>
+ <IndexRedirect to='custom_brand'/>
+ <Route
+ path='custom_brand'
+ component={CustomBrandSettings}
+ />
+ <Route
+ path='legal_and_support'
+ component={LegalAndSupportSettings}
+ />
+ </Route>
+ <Route
+ path='compliance'
+ component={ComplianceSettings}
+ />
+ <Route
+ path='rate'
+ component={RateSettings}
+ />
+ <Route
+ path='developer'
+ component={DeveloperSettings}
+ />
+ <Route path='team'>
+ <Redirect
+ from=':team'
+ to=':team/users'
+ />
+ <Route
+ path=':team/users'
+ component={TeamUsers}
+ />
+ <Route
+ path=':team/analytics'
+ component={TeamAnalytics}
+ />
+ <Redirect
+ from='*'
+ to='/error'
+ query={notFoundParams}
+ />
+ </Route>
+ <Route
+ path='license'
+ component={LicenseSettings}
+ />
+ <Route
+ path='audits'
+ component={Audits}
+ />
+ <Route
+ path='logs'
+ component={Logs}
+ />
+ </Route>
<Route
path=':team'
component={NeedsTeam}
diff --git a/webapp/sass/components/_dropdown.scss b/webapp/sass/components/_dropdown.scss
index 1168c9b27..5ed0bca61 100644
--- a/webapp/sass/components/_dropdown.scss
+++ b/webapp/sass/components/_dropdown.scss
@@ -6,6 +6,11 @@
z-index: 2500;
}
+ .fa {
+ @include opacity(.6);
+ margin-right: 5px;
+ }
+
.divider {
@include opacity(.15);
}
diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss
index d53be29dc..3d3a11de0 100644
--- a/webapp/sass/components/_modal.scss
+++ b/webapp/sass/components/_modal.scss
@@ -448,7 +448,7 @@
@include opacity(.8);
float: right;
margin-right: 3px;
- margin-top: 12px;
+ margin-top: 8px;
}
.member-select__container {
@@ -459,7 +459,7 @@
select {
@include opacity(.8);
float: right;
- margin: 5px 5px 0 2px;
+ margin: 1px 5px 0 2px;
width: auto;
}
}
diff --git a/webapp/sass/components/_module.scss b/webapp/sass/components/_module.scss
index 24488df96..e74404d9c 100644
--- a/webapp/sass/components/_module.scss
+++ b/webapp/sass/components/_module.scss
@@ -11,6 +11,7 @@
@import 'modal';
@import 'oauth';
@import 'popover';
+@import 'save-button';
@import 'scrollbar';
@import 'search';
@import 'suggestion-list';
diff --git a/webapp/sass/components/_save-button.scss b/webapp/sass/components/_save-button.scss
new file mode 100644
index 000000000..12f793aa1
--- /dev/null
+++ b/webapp/sass/components/_save-button.scss
@@ -0,0 +1,7 @@
+@charset 'UTF-8';
+
+.save-button {
+ .glyphicon {
+ margin-right: 10px;
+ }
+} \ No newline at end of file
diff --git a/webapp/sass/components/_scrollbar.scss b/webapp/sass/components/_scrollbar.scss
index b6ec4f22f..b868c0bf0 100644
--- a/webapp/sass/components/_scrollbar.scss
+++ b/webapp/sass/components/_scrollbar.scss
@@ -29,3 +29,11 @@ body {
@include border-radius(2px);
@include alpha-property(background-color, $black, .5);
}
+
+.scrollbar--view {
+ -ms-overflow-style: none;
+
+ .browser--ie & {
+ margin: 0 !important;
+ }
+} \ No newline at end of file
diff --git a/webapp/sass/components/_videos.scss b/webapp/sass/components/_videos.scss
index e009e6538..b2230f71d 100644
--- a/webapp/sass/components/_videos.scss
+++ b/webapp/sass/components/_videos.scss
@@ -10,6 +10,22 @@
max-width: 100%;
}
+ .video-thumbnail__error {
+ height: 100%;
+ line-height: 2;
+ padding: 110px 0;
+ text-align: center;
+ width: 100%;
+
+ .fa {
+ @include opacity(.5);
+ }
+
+ div {
+ font-size: 1.2em;
+ }
+ }
+
.block {
background-color: alpha-color($black, .5);
border-radius: 10px;
@@ -67,3 +83,7 @@
height: 500px;
}
}
+
+.video-loading {
+ height: 360px;
+} \ No newline at end of file
diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss
index 35fef8e08..7db561438 100644
--- a/webapp/sass/layout/_headers.scss
+++ b/webapp/sass/layout/_headers.scss
@@ -67,8 +67,9 @@
.channel-intro {
border-bottom: 1px solid $light-gray;
- margin: 0 18px 35px;
- padding: 0 0 5px;
+ margin: 0 auto 15px;
+ max-width: 990px;
+ padding: 0 15px;
.intro-links {
display: inline-block;
@@ -118,7 +119,8 @@
// Team Header in Sidebar
.sidebar--left,
-.sidebar--menu {
+.sidebar--menu,
+.admin-sidebar {
.team__header {
@include legacy-pie-clearfix;
padding: 9px 10px;
diff --git a/webapp/sass/layout/_markdown.scss b/webapp/sass/layout/_markdown.scss
index cb29aa20e..9bac332d6 100644
--- a/webapp/sass/layout/_markdown.scss
+++ b/webapp/sass/layout/_markdown.scss
@@ -55,18 +55,14 @@
.post-code__language {
@include opacity(0);
- @include translate3d(0, 0, 0);
+ @include transition(opacity, .6s);
background: #21586d;
- border-radius: 0 .25em;
+ border-radius: 0 0 0 2px;
color: $white;
padding: 4px 10px 5px;
position: absolute;
right: 0;
top: 0;
- -webkit-transition: opacity 0.6s;
- -moz-transition: opacity 0.6s;
- -o-transition: opacity 0.6s;
- transition: opacity 0.6s;
z-index: 5;
}
diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss
index a99c6d57c..e3818ea94 100644
--- a/webapp/sass/layout/_post.scss
+++ b/webapp/sass/layout/_post.scss
@@ -417,7 +417,8 @@ body.ios {
.post-create-footer {
@include clearfix;
font-size: 13px;
- padding: 3px 0 0 0;
+ padding: 3px 0 0;
+
.control-label {
font-weight: normal;
margin-bottom: 0;
@@ -482,6 +483,80 @@ body.ios {
background-color: beige;
}
+ &.post--compact {
+
+ .markdown__heading {
+ font-size: 1em;
+ margin: 0 0 7px;
+ }
+
+ .post__body {
+ background: transparent !important;
+ margin-top: -1px;
+ padding: 3px 0;
+ }
+
+ .post-image__columns {
+ clear: both;
+ }
+
+ .post-image__column {
+ @include border-radius(2px);
+ font-size: .9em;
+ height: 26px;
+ line-height: 25px;
+ padding: 0 7px;
+ width: auto;
+
+ .post-image__thumbnail {
+ display: none;
+ }
+
+ .post-image__details {
+ background: transparent;
+ border: none;
+ padding: 0;
+ width: 100%;
+
+ > div {
+ display: none;
+ }
+ }
+
+ .post-image__name {
+ @include clearfix;
+ display: block;
+ margin: 0;
+ padding-right: 10px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ i {
+ font-size: .9em;
+ margin-right: 5px;
+ opacity: .5;
+ }
+ }
+
+ a {
+ &:hover {
+ text-decoration: none;
+ }
+ }
+ }
+
+ .post__img {
+ padding-top: 3px;
+ width: 28px;
+
+ img,
+ svg {
+ height: 20px;
+ width: 20px;
+ }
+ }
+ }
+
p {
font-size: .97em;
line-height: 1.6em;
@@ -526,6 +601,12 @@ body.ios {
}
}
+ .post__img {
+ img {
+ display: none;
+ }
+ }
+
.post__header {
height: 0;
margin: 0;
@@ -545,9 +626,11 @@ body.ios {
display: none;
}
- .post__img {
- img {
- display: none;
+ &.same--user {
+ .post__img {
+ img {
+ display: none;
+ }
}
}
}
@@ -644,11 +727,11 @@ body.ios {
}
.post__img {
- width: 46px;
+ width: 42px;
svg {
- height: 36px;
- width: 36px;
+ height: 32px;
+ width: 32px;
}
path {
@@ -657,9 +740,9 @@ body.ios {
img {
@include border-radius(50px);
- height: 36px;
+ height: 32px;
vertical-align: inherit;
- width: 36px;
+ width: 32px;
}
}
@@ -731,6 +814,7 @@ body.ios {
padding: .2em .5em;
width: calc(100% - 75px);
word-wrap: break-word;
+ position: relative;
p {
margin: 0 0 .4em;
@@ -796,6 +880,20 @@ body.ios {
margin-right: 8px;
}
}
+
+ .pending-post-actions {
+ position: absolute;
+ right: 0;
+ top: 0;
+ padding: 5px 7px;
+ background: rgba(0, 0, 0, .7);
+ color: white;
+ font-size: .9em;
+
+ a {
+ color: white;
+ }
+ }
}
.post__link {
diff --git a/webapp/sass/layout/_sidebar-left.scss b/webapp/sass/layout/_sidebar-left.scss
index 7ac1fee75..cc8f13730 100644
--- a/webapp/sass/layout/_sidebar-left.scss
+++ b/webapp/sass/layout/_sidebar-left.scss
@@ -40,7 +40,7 @@
}
.dropdown-menu {
- max-height: 400px;
+ max-height: 80vh;
max-width: 200px;
overflow-x: hidden;
overflow-y: auto;
@@ -140,6 +140,12 @@
text-transform: uppercase;
}
+ .divider {
+ & + .divider {
+ display: none;
+ }
+ }
+
> a {
border-radius: 0;
line-height: 1.5;
diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss
index cc3d7a4b9..049e12055 100644
--- a/webapp/sass/responsive/_mobile.scss
+++ b/webapp/sass/responsive/_mobile.scss
@@ -1,6 +1,12 @@
@charset 'UTF-8';
@media screen and (max-width: 768px) {
+ .post-code__language {
+ @include opacity(.6);
+ @include transition(none);
+ display: block;
+ }
+
.backstage-filters {
display: block;
@@ -41,10 +47,15 @@
}
.channel-intro {
- margin: 0 15px 35px;
+ margin: 0 0 35px;
}
.post {
+ &.post--compact {
+
+
+ }
+
.post__dropdown {
display: inline-block;
height: 20px;
@@ -136,10 +147,6 @@
&.same--root {
&.same--user {
- .post__time {
- display: none;
- }
-
.post__header {
height: auto;
margin-top: 5px;
@@ -275,7 +282,7 @@
.signup-team__container {
font-size: .9em;
margin-bottom: 30px;
- padding: 30px 0;
+ padding: 60px 10px 0;
.signup-team__name {
font-size: 2em;
@@ -1010,12 +1017,19 @@
@media screen and (max-height: 640px) {
.signup-team__container {
- padding: 30px 0;
- margin-bottom: 30px;
font-size: .9em;
+ margin-bottom: 30px;
.signup-team__name {
font-size: 2em;
}
}
}
+
+@media screen and (max-width: 320px) {
+ .tip-overlay {
+ &.tip-overlay--sidebar {
+ min-height: 440px;
+ }
+ }
+}
diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss
index 72b4b5aad..f5e341b25 100644
--- a/webapp/sass/responsive/_tablet.scss
+++ b/webapp/sass/responsive/_tablet.scss
@@ -65,6 +65,7 @@
}
}
+// Tablet and desktop
@media screen and (min-width: 768px) {
.second-bar {
display: none;
@@ -83,6 +84,111 @@
}
.post {
+ &.post--compact {
+ padding: 5px .5em 0 80px;
+
+ .post__link {
+ margin: 4px 0 7px;
+ }
+
+ .post__time {
+ font-size: .85em;
+ left: -70px;
+ position: absolute;
+ top: 2px;
+ }
+
+ span {
+ p {
+ &:last-child {
+ margin-bottom: .3em;
+ }
+ }
+ }
+
+ .post__header {
+ float: left;
+ height: 18px;
+ padding-top: 3px;
+
+ .col__name {
+ font-weight: bold;
+ }
+
+ .col__reply {
+ top: 2px;
+ }
+ }
+
+ &.other--root {
+ .post__body {
+ > div {
+ &:first-child {
+ min-height: 21px;
+ }
+ }
+ }
+
+ .post__link + .post__body {
+ clear: both;
+ }
+
+ &.post--comment {
+ .post__header {
+ .col__reply {
+ top: 0;
+ }
+ }
+ }
+ }
+
+ .post-code {
+ clear: both;
+ }
+
+ &.same--root {
+ &.same--user {
+ padding-left: 80px;
+
+ .post__img {
+ img {
+ display: none;
+ }
+ }
+ }
+
+ &.post--comment {
+ padding-top: 1px;
+
+ .post__img {
+ img {
+ display: inline-block;
+ }
+ }
+
+ &.same--user {
+ .post__img {
+ img {
+ display: none;
+ }
+ }
+ }
+
+ .post__header {
+ margin-left: 12px;
+ }
+ }
+ }
+
+ .post__body {
+ width: 100%;
+ }
+
+ .post__content {
+ padding-right: 85px;
+ }
+ }
+
&.same--root {
&.same--user {
.post__time {
@@ -94,6 +200,14 @@
text-rendering: auto;
top: -2px;
}
+
+ &.post--compact {
+ .post__time {
+ font-size: .85em;
+ left: -70px;
+ top: -5px;
+ }
+ }
}
}
}
diff --git a/webapp/sass/routes/_about-modal.scss b/webapp/sass/routes/_about-modal.scss
index 43d04319d..4bfd04e57 100644
--- a/webapp/sass/routes/_about-modal.scss
+++ b/webapp/sass/routes/_about-modal.scss
@@ -59,6 +59,7 @@
p {
&:first-child {
float: left;
+ text-align: left;
}
}
}
@@ -79,4 +80,4 @@
}
}
-} \ No newline at end of file
+}
diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss
index 0f47e7529..c8af72472 100644
--- a/webapp/sass/routes/_admin-console.scss
+++ b/webapp/sass/routes/_admin-console.scss
@@ -1,8 +1,16 @@
@charset 'UTF-8';
-.admin-controller {
- > div {
- height: 100%;
+.admin-console {
+ color: #333;
+ height: 100%;
+ margin-left: 220px;
+ overflow: auto;
+ padding: 0 20px;
+
+ .dropdown-menu {
+ .divider {
+ @include opacity(1);
+ }
}
.inner-wrap {
@@ -17,6 +25,11 @@
padding-bottom: .5em;
}
+ h4 {
+ font-weight: 600;
+ margin-bottom: 2em;
+ }
+
.form-control {
background-color: $white;
border: 1px solid $light-gray;
@@ -34,122 +47,6 @@
}
}
- .sidebar--left {
- &.sidebar--collapsable {
- background: #333;
-
- .team__header {
- background: transparent;
- margin-bottom: 5px;
- }
-
- .nav {
- li {
- padding: 0;
- @include opacity(1);
-
- .icon {
- width: 17px;
- }
-
- &.divider {
- @include alpha-property(background, $black, .1);
- }
-
- > a {
- &:hover,
- &:focus {
- @include alpha-property(background, $black, .1);
- }
-
- &.active {
- background-color: transparent;
- }
- }
-
- > h4 {
- background: alpha-color($white, .15);
- color: $white;
- margin: 1px 0 0;
- padding: 10px;
-
- .menu-icon--right {
- right: 12px;
- top: 6px;
- }
- }
- }
-
- .menu-icon--right {
- font-size: 18px;
- font-weight: 600;
- height: 20px;
- line-height: 20px;
- position: absolute;
- right: 10px;
- text-align: center;
- top: 3px;
- width: 20px;
-
- .fa {
- color: $white;
- font-size: 13px;
- position: relative;
- right: -2px;
- }
- }
-
- &.nav__sub-menu {
- @include font-smoothing(initial);
- background: #111;
-
- &.padded {
- padding: 5px 0;
- }
-
- li {
- > a {
- background: transparent;
- color: #bbb;
- font-size: 13px;
- padding: 5px 35px 5px 15px;
-
- &:hover {
- color: lighten($primary-color, 10);
- }
-
- &.active {
- color: $white;
- font-weight: 600;
- }
- }
-
- .nav-more {
- background: transparent;
- color: #bbb;
- cursor: pointer;
- display: block;
- font-size: 13px;
- padding: 5px 15px;
-
- &:hover {
- color: lighten($primary-color, 10);
- }
- }
- }
- }
-
- &.nav__inner-menu {
- li {
- > a {
- padding-left: 20px;
- }
- }
- }
- }
- }
- }
-
.log__panel {
background-color: white;
border: 1px solid #ddd;
@@ -160,168 +57,160 @@
width: 100%;
}
- .app__content {
- color: #333;
+ &.admin {
+ background-color: #f1f1f1;
+ min-height: 600px;
+ overflow: auto;
+ padding: 0 40px 20px;
+ }
- &.admin {
- background-color: #f1f1f1;
- min-height: 600px;
- overflow: auto;
- padding: 0 40px 20px;
- }
+ .wrapper--fixed {
+ max-width: 800px;
+ }
+
+ .form-horizontal {
+ margin-top: 40px;
- .wrapper--fixed {
- max-width: 800px;
+ .control-label {
+ font-weight: 600;
+ padding-right: 0;
+ text-align: left;
}
- .form-horizontal {
- margin-top: 40px;
+ .form-group {
+ margin-bottom: 25px;
+ }
- .control-label {
- font-weight: 600;
- padding-right: 0;
- text-align: left;
+ .file__upload {
+ display: inline-block;
+ margin: 0 10px 10px 0;
+ position: relative;
+
+ input {
+ @include opacity(0);
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ z-index: 5;
}
+ }
- .form-group {
- margin-bottom: 25px;
+ .help-text {
+ color: #777;
+ margin: 10px 0 0 15px;
- &.form-group--small {
- margin-bottom: 10px;
- }
+ &.no-margin {
+ margin: 0;
}
- .file__upload {
- display: inline-block;
- margin: 0 10px 10px 0;
- position: relative;
-
- input {
- @include opacity(0);
- height: 100%;
- left: 0;
- position: absolute;
- top: 0;
- width: 100%;
- z-index: 5;
- }
+ &.no-margin--top {
+ margin-top: 0;
}
- .help-text {
- color: #777;
- margin: 10px 0 0 15px;
-
- &.no-margin {
- margin: 0;
- }
-
- &.no-margin--top {
- margin-top: 0;
- }
-
- ul,
- ol {
- padding-left: 23px;
- }
-
- .help-link {
- margin-right: 5px;
- }
-
- .btn {
- font-size: 13px;
- }
+ ul,
+ ol {
+ padding-left: 23px;
}
- .alert {
- display: inline-block;
- margin: 1em 0 0;
- padding: 5px 7px;
- position: relative;
- top: 1px;
+ .help-link {
+ margin-right: 5px;
+ }
- .fa {
- margin-right: 5px;
- }
+ .btn {
+ font-size: 13px;
}
}
- .banner {
- background: $white;
- border: 1px solid #ddd;
- font-size: .95em;
- margin: 2em 0;
- padding: .7em 1.5em;
+ .alert {
+ display: inline-block;
+ margin: 1em 0 0;
+ padding: 5px 7px;
+ position: relative;
+ top: 1px;
- .banner__heading {
- font-size: 1.5em;
+ .fa {
+ margin-right: 5px;
}
+ }
+ }
- .banner__content {
- width: 80%;
- }
+ .banner {
+ background: $white;
+ border: 1px solid #ddd;
+ font-size: .95em;
+ margin: 2em 0;
+ padding: .7em 1.5em;
- &.warning {
- background: #e60000;
- }
+ .banner__heading {
+ font-size: 1.5em;
}
- .popover {
- border-radius: 3px;
- font-size: .95em;
- width: 100%;
+ .banner__content {
+ width: 80%;
}
- .panel {
- background-color: transparent;
- border: none;
+ &.warning {
+ background: #e60000;
}
+ }
- .panel-default {
- > .panel-heading {
- background-color: transparent;
- padding: 10px 0;
- }
+ .popover {
+ border-radius: 3px;
+ font-size: .95em;
+ width: 100%;
+ }
- .panel-body {
- padding: 30px 0 10px;
- }
- }
+ .panel {
+ background-color: transparent;
+ border: none;
+ }
- .panel-group {
- margin-bottom: 50px;
+ .panel-default {
+ > .panel-heading {
+ background-color: transparent;
+ padding: 10px 0;
}
- .panel-title {
- font-size: 24px;
- line-height: 1.5;
+ .panel-body {
+ padding: 30px 0 10px;
+ }
+ }
- a {
- @include clearfix;
- display: block;
- text-decoration: none;
+ .panel-group {
+ margin-bottom: 50px;
+ }
- &.collapsed {
- .fa-minus {
- display: none;
- }
+ .panel-title {
+ font-size: 24px;
+ line-height: 1.5;
- .fa-plus {
- display: inline-block;
- }
- }
+ a {
+ @include clearfix;
+ display: block;
+ text-decoration: none;
- .fa {
- color: #aaa;
- float: right;
- font-size: 18px;
- margin-top: 8px;
+ &.collapsed {
+ .fa-minus {
+ display: none;
}
.fa-plus {
- display: none;
+ display: inline-block;
}
}
+
+ .fa {
+ color: #aaa;
+ float: right;
+ font-size: 18px;
+ margin-top: 8px;
+ }
+
+ .fa-plus {
+ display: none;
+ }
}
}
@@ -343,3 +232,146 @@
margin-bottom: 1.5em;
max-width: 150px;
}
+
+.admin-console__disabled-text {
+ color: #777;
+ margin: 10px 0 0 15px;
+}
+
+.admin-sidebar {
+ background: #333;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ width: 220px;
+ z-index: 5;
+
+ .dropdown-menu {
+ min-width: 200px;
+ }
+
+ .team__header {
+ background: transparent;
+ }
+
+ .nav-pills__container {
+ background: #111;
+ @include font-smoothing(initial);
+ height: calc(100% - 80px);
+ margin-top: 1px;
+ position: relative;
+ }
+
+ .sidebar-category {
+ .category-title {
+ background: alpha-color($white, .15);
+ color: $white;
+ line-height: 15.4px;
+ padding: 10px;
+
+ .category-icon {
+ right: 12px;
+ top: 6px;
+ width: 17px;
+ }
+ }
+
+ .sections {
+ padding: 5px 0;
+ }
+ }
+
+ .sidebar-section {
+ > .sidebar-section-title {
+ position: relative;
+ }
+ }
+
+ .sidebar-section-title {
+ padding: 5px 35px 5px 15px;
+ }
+
+ .sidebar-subsection-title {
+ padding: 5px 35px 5px 30px;
+ }
+
+ .sidebar-section-title,
+ .sidebar-subsection-title {
+ color: #bbb;
+ display: block;
+ font-size: 13px;
+ position: relative;
+
+ &:focus {
+ text-decoration: none;
+ }
+
+ &:hover {
+ color: lighten($primary-color, 10);
+ text-decoration: none;
+ }
+
+ &--active {
+ color: $white;
+ font-weight: 600;
+
+ &:after {
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ color: whitesmoke;
+ content: "\f0d9";
+ display: inline-block;
+ font: normal normal normal 26px/1 FontAwesome;
+ position: absolute;
+ right: -1px;
+ text-rendering: auto;
+ top: 3px;
+ }
+ }
+ }
+
+ .sidebar-subsection-title {
+ &--active {
+ &:after {
+ top: 2px;
+ }
+ }
+ }
+
+ .menu-icon--right {
+ font-size: 18px;
+ font-weight: 600;
+ height: 20px;
+ line-height: 20px;
+ position: absolute;
+ right: 12px;
+ text-align: center;
+ top: 8px;
+ width: 20px;
+
+ .fa {
+ color: $white;
+ font-size: 13px;
+ position: relative;
+ right: -2px;
+ }
+
+ &.menu__close {
+ cursor: pointer;
+ right: 10px;
+ top: 3px;
+ }
+ }
+}
+
+.email-connection-test {
+ margin-top: -15px;
+}
+
+.reload-config {
+ margin-bottom: 50px !important;
+}
+
+.recycle-db {
+ margin-top: 50px !important;
+} \ No newline at end of file
diff --git a/webapp/sass/routes/_settings.scss b/webapp/sass/routes/_settings.scss
index 1551e5f4d..6986959b2 100644
--- a/webapp/sass/routes/_settings.scss
+++ b/webapp/sass/routes/_settings.scss
@@ -146,10 +146,10 @@
@include appearance(none);
appearance: none;
padding-right: 25px;
+ }
- &::ms-expand {
- display: none;
- }
+ select::ms-expand {
+ display: none;
}
&:before {
diff --git a/webapp/sass/routes/_signup.scss b/webapp/sass/routes/_signup.scss
index 08bd0d12d..d3fe0363f 100644
--- a/webapp/sass/routes/_signup.scss
+++ b/webapp/sass/routes/_signup.scss
@@ -26,6 +26,10 @@
@include flex(1.3 0 0);
padding-right: 80px;
+ img {
+ max-width: 450px;
+ }
+
p {
color: lighten($black, 50%);
}
diff --git a/webapp/stores/admin_store.jsx b/webapp/stores/admin_store.jsx
index ecfbaf85f..b135d9485 100644
--- a/webapp/stores/admin_store.jsx
+++ b/webapp/stores/admin_store.jsx
@@ -22,7 +22,7 @@ class AdminStoreClass extends EventEmitter {
this.logs = null;
this.audits = null;
this.config = null;
- this.teams = null;
+ this.teams = {};
this.complianceReports = null;
}
@@ -126,6 +126,10 @@ class AdminStoreClass extends EventEmitter {
this.teams = teams;
}
+ getTeam(id) {
+ return this.teams[id];
+ }
+
getSelectedTeams() {
const result = BrowserStore.getItem('seleted_teams');
if (!result) {
diff --git a/webapp/stores/browser_store.jsx b/webapp/stores/browser_store.jsx
index 2dae78f46..11fe50928 100644
--- a/webapp/stores/browser_store.jsx
+++ b/webapp/stores/browser_store.jsx
@@ -158,6 +158,7 @@ class BrowserStoreClass {
clear() {
// don't clear the logout id so IE11 can tell which tab sent a logout request
const logoutId = sessionStorage.getItem('__logout__');
+ const serverVersion = this.getLastServerVersion();
sessionStorage.clear();
localStorage.clear();
@@ -165,6 +166,10 @@ class BrowserStoreClass {
if (logoutId) {
sessionStorage.setItem('__logout__', logoutId);
}
+
+ if (serverVersion) {
+ this.setLastServerVersion(serverVersion);
+ }
}
clearAll() {
diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx
index 32ea8441c..b34e92530 100644
--- a/webapp/stores/channel_store.jsx
+++ b/webapp/stores/channel_store.jsx
@@ -290,7 +290,7 @@ class ChannelStoreClass extends EventEmitter {
}
leaveChannel(id) {
- delete this.channelMembers[id];
+ Reflect.deleteProperty(this.channelMembers, id);
const element = this.channels.indexOf(id);
if (element > -1) {
this.channels.splice(element, 1);
diff --git a/webapp/stores/error_store.jsx b/webapp/stores/error_store.jsx
index 278e252c0..4a357472d 100644
--- a/webapp/stores/error_store.jsx
+++ b/webapp/stores/error_store.jsx
@@ -64,8 +64,10 @@ class ErrorStoreClass extends EventEmitter {
clearLastError() {
var lastError = this.getLastError();
+
+ // preview message can only be cleared by clearPreviewError
if (lastError && lastError.email_preview) {
- this.ignore_email_preview = true;
+ return;
}
BrowserStore.removeGlobalItem('last_error');
@@ -74,6 +76,12 @@ class ErrorStoreClass extends EventEmitter {
this.emitChange();
}
}
+
+ clearPreviewError() {
+ this.ignore_email_preview = true;
+ this.storeLastError('');
+ this.clearLastError();
+ }
}
var ErrorStore = new ErrorStoreClass();
diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx
index 17529acb6..7a532fa2e 100644
--- a/webapp/stores/post_store.jsx
+++ b/webapp/stores/post_store.jsx
@@ -454,10 +454,11 @@ class PostStoreClass extends EventEmitter {
for (let i = 0; i < len; i++) {
const post = postList.posts[postList.order[i]];
- // don't edit webhook posts or deleted posts
+ // don't edit webhook posts, deleted posts, or system messages
if (post.user_id !== userId ||
(post.props && post.props.from_webhook) ||
- post.state === Constants.POST_DELETED) {
+ post.state === Constants.POST_DELETED ||
+ (post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX))) {
continue;
}
diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx
index 8eebbaca5..210d5c211 100644
--- a/webapp/stores/team_store.jsx
+++ b/webapp/stores/team_store.jsx
@@ -177,6 +177,11 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => {
TeamStore.saveMyTeam(action.team);
TeamStore.emitChange();
break;
+ case ActionTypes.CREATED_TEAM:
+ TeamStore.saveTeam(action.team);
+ TeamStore.appendTeamMember(action.member);
+ TeamStore.emitChange();
+ break;
case ActionTypes.RECEIVED_ALL_TEAMS:
TeamStore.saveTeams(action.teams);
TeamStore.emitChange();
diff --git a/webapp/tests/.eslintrc.json b/webapp/tests/.eslintrc.json
new file mode 100644
index 000000000..c2d57abea
--- /dev/null
+++ b/webapp/tests/.eslintrc.json
@@ -0,0 +1,12 @@
+{
+ "rules": {
+ "no-console": 0,
+ "global-require": 0,
+ "func-names": 0,
+ "prefer-arrow-callback": 0,
+ "no-magic-numbers": 0,
+ "no-unreachable": 0,
+ "new-cap": 0,
+ "max-nested-callbacks": 0
+ }
+}
diff --git a/webapp/tests/client_channel.test.jsx b/webapp/tests/client_channel.test.jsx
deleted file mode 100644
index 9d88f3de0..000000000
--- a/webapp/tests/client_channel.test.jsx
+++ /dev/null
@@ -1,356 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
-
-import assert from 'assert';
-import TestHelper from './test_helper.jsx';
-
-describe('Client.Channels', function() {
- this.timeout(100000);
-
- it('createChannel', function(done) {
- TestHelper.initBasic(() => {
- var channel = TestHelper.fakeChannel();
- channel.team_id = TestHelper.basicTeam().id;
- TestHelper.basicClient().createChannel(
- channel,
- function(data) {
- assert.equal(data.id.length > 0, true);
- assert.equal(data.name, channel.name);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('createDirectChannel', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().createUser(
- TestHelper.fakeUser(),
- function(user2) {
- TestHelper.basicClient().addUserToTeam(
- user2.id,
- function() {
- TestHelper.basicClient().createDirectChannel(
- user2.id,
- function(data) {
- assert.equal(data.id.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updateChannel', function(done) {
- TestHelper.initBasic(() => {
- var channel = TestHelper.basicChannel();
- channel.display_name = 'changed';
- TestHelper.basicClient().updateChannel(
- channel,
- function(data) {
- assert.equal(data.id.length > 0, true);
- assert.equal(data.display_name, 'changed');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updateChannelHeader', function(done) {
- TestHelper.initBasic(() => {
- var channel = TestHelper.basicChannel();
- channel.display_name = 'changed';
- TestHelper.basicClient().updateChannelHeader(
- channel.id,
- 'new header',
- function(data) {
- assert.equal(data.id.length > 0, true);
- assert.equal(data.header, 'new header');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updateChannelPurpose', function(done) {
- TestHelper.initBasic(() => {
- var channel = TestHelper.basicChannel();
- channel.display_name = 'changed';
- TestHelper.basicClient().updateChannelPurpose(
- channel.id,
- 'new purpose',
- function(data) {
- assert.equal(data.id.length > 0, true);
- assert.equal(data.purpose, 'new purpose');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updateChannelNotifyProps', function(done) {
- TestHelper.initBasic(() => {
- var props = {};
- props.channel_id = TestHelper.basicChannel().id;
- props.user_id = TestHelper.basicUser().id;
- props.desktop = 'all';
- TestHelper.basicClient().updateChannelNotifyProps(
- props,
- function(data) {
- assert.equal(data.desktop, 'all');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('leaveChannel', function(done) {
- TestHelper.initBasic(() => {
- var channel = TestHelper.basicChannel();
- TestHelper.basicClient().leaveChannel(
- channel.id,
- function(data) {
- assert.equal(data.id, channel.id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('joinChannel', function(done) {
- TestHelper.initBasic(() => {
- var channel = TestHelper.basicChannel();
- TestHelper.basicClient().leaveChannel(
- channel.id,
- function() {
- TestHelper.basicClient().joinChannel(
- channel.id,
- function() {
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('joinChannelByName', function(done) {
- TestHelper.initBasic(() => {
- var channel = TestHelper.basicChannel();
- TestHelper.basicClient().leaveChannel(
- channel.id,
- function() {
- TestHelper.basicClient().joinChannelByName(
- channel.name,
- function() {
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('deleteChannel', function(done) {
- TestHelper.initBasic(() => {
- var channel = TestHelper.basicChannel();
- TestHelper.basicClient().deleteChannel(
- channel.id,
- function(data) {
- assert.equal(data.id, channel.id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updateLastViewedAt', function(done) {
- TestHelper.initBasic(() => {
- var channel = TestHelper.basicChannel();
- TestHelper.basicClient().updateLastViewedAt(
- channel.id,
- function(data) {
- assert.equal(data.id, channel.id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getChannels', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getChannels(
- function(data) {
- assert.equal(data.channels.length, 3);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getChannel', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getChannel(
- TestHelper.basicChannel().id,
- function(data) {
- assert.equal(TestHelper.basicChannel().id, data.channel.id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getMoreChannels', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getMoreChannels(
- function(data) {
- assert.equal(data.channels.length, 0);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getChannelCounts', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getChannelCounts(
- function(data) {
- assert.equal(data.counts[TestHelper.basicChannel().id], 1);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getChannelExtraInfo', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getChannelExtraInfo(
- TestHelper.basicChannel().id,
- 5,
- function(data) {
- assert.equal(data.member_count, 1);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('addChannelMember', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().createUser(
- TestHelper.fakeUser(),
- function(user2) {
- TestHelper.basicClient().addUserToTeam(
- user2.id,
- function() {
- TestHelper.basicClient().addChannelMember(
- TestHelper.basicChannel().id,
- user2.id,
- function(data) {
- assert.equal(data.channel_id.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('removeChannelMember', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().removeChannelMember(
- TestHelper.basicChannel().id,
- TestHelper.basicUser().id,
- function(data) {
- assert.equal(data.channel_id.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-});
-
diff --git a/webapp/tests/client_command.test.jsx b/webapp/tests/client_command.test.jsx
deleted file mode 100644
index f7f0d2b25..000000000
--- a/webapp/tests/client_command.test.jsx
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
-
-import assert from 'assert';
-import TestHelper from './test_helper.jsx';
-
-describe('Client.Commands', function() {
- this.timeout(100000);
-
- it('listCommands', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().listCommands(
- function(data) {
- assert.equal(data.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('listTeamCommands', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().listTeamCommands(
- function() {
- done(new Error('cmds not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.command.disabled.app_error');
- done();
- }
- );
- });
- });
-
- it('executeCommand', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().executeCommand(
- TestHelper.basicChannel().id,
- '/shrug',
- null,
- function(data) {
- assert.equal(data.response_type, 'in_channel');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('addCommand', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
-
- var cmd = {};
- cmd.url = 'http://www.gonowhere.com';
- cmd.trigger = '/hello';
- cmd.method = 'P';
- cmd.username = '';
- cmd.icon_url = '';
- cmd.auto_complete = false;
- cmd.auto_complete_desc = '';
- cmd.auto_complete_hint = '';
- cmd.display_name = 'Unit Test';
-
- TestHelper.basicClient().addCommand(
- cmd,
- function() {
- done(new Error('cmds not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.command.disabled.app_error');
- done();
- }
- );
- });
- });
-
- it('deleteCommand', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().deleteCommand(
- TestHelper.generateId(),
- function() {
- done(new Error('cmds not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.command.disabled.app_error');
- done();
- }
- );
- });
- });
-
- it('regenCommandToken', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().regenCommandToken(
- TestHelper.generateId(),
- function() {
- done(new Error('cmds not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.command.disabled.app_error');
- done();
- }
- );
- });
- });
-});
-
diff --git a/webapp/tests/client_general.test.jsx b/webapp/tests/client_general.test.jsx
deleted file mode 100644
index 870c11257..000000000
--- a/webapp/tests/client_general.test.jsx
+++ /dev/null
@@ -1,333 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
-
-var assert = require('assert');
-import TestHelper from './test_helper.jsx';
-
-describe('Client.General', function() {
- this.timeout(10000);
-
- it('Admin.getClientConfig', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getClientConfig(
- function(data) {
- assert.equal(data.SiteName, 'Mattermost');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('Admin.getComplianceReports', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().getComplianceReports(
- function() {
- done(new Error('should need system admin permissions'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.system_permissions.app_error');
- done();
- }
- );
- });
- });
-
- it('Admin.saveComplianceReports', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
-
- var job = {};
- job.desc = 'desc';
- job.emails = '';
- job.keywords = 'test';
- job.start_at = new Date();
- job.end_at = new Date();
-
- TestHelper.basicClient().saveComplianceReports(
- job,
- function() {
- done(new Error('should need system admin permissions'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.system_permissions.app_error');
- done();
- }
- );
- });
- });
-
- it('Admin.getLogs', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().getLogs(
- function() {
- done(new Error('should need system admin permissions'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.system_permissions.app_error');
- done();
- }
- );
- });
- });
-
- it('Admin.getServerAudits', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().getServerAudits(
- function() {
- done(new Error('should need system admin permissions'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.system_permissions.app_error');
- done();
- }
- );
- });
- });
-
- it('Admin.getConfig', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().getConfig(
- function() {
- done(new Error('should need system admin permissions'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.system_permissions.app_error');
- done();
- }
- );
- });
- });
-
- it('Admin.getAnalytics', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().getAnalytics(
- 'standard',
- null,
- function() {
- done(new Error('should need system admin permissions'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.system_permissions.app_error');
- done();
- }
- );
- });
- });
-
- it('Admin.getTeamAnalytics', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().getTeamAnalytics(
- TestHelper.basicTeam().id,
- 'standard',
- function() {
- done(new Error('should need system admin permissions'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.system_permissions.app_error');
- done();
- }
- );
- });
- });
-
- it('Admin.saveConfig', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- var config = {};
- config.site_name = 'test';
-
- TestHelper.basicClient().saveConfig(
- config,
- function() {
- done(new Error('should need system admin permissions'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.system_permissions.app_error');
- done();
- }
- );
- });
- });
-
- it('Admin.testEmail', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- var config = {};
- config.site_name = 'test';
-
- TestHelper.basicClient().testEmail(
- config,
- function() {
- done(new Error('should need system admin permissions'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.system_permissions.app_error');
- done();
- }
- );
- });
- });
-
- it('Admin.logClientError', function(done) {
- TestHelper.initBasic(() => {
- var config = {};
- config.site_name = 'test';
- TestHelper.basicClient().logClientError('this is a test');
- done();
- });
- });
-
- it('Admin.adminResetMfa', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
-
- TestHelper.basicClient().adminResetMfa(
- TestHelper.basicUser().id,
- function() {
- done(new Error('should need a license'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.permissions.app_error');
- done();
- }
- );
- });
- });
-
- it('Admin.adminResetPassword', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- var user = TestHelper.basicUser();
-
- TestHelper.basicClient().resetPassword(
- user.id,
- 'new_password',
- function() {
- throw Error('shouldnt work');
- },
- function(err) {
- // this should fail since you're not a system admin
- assert.equal(err.id, 'api.context.invalid_param.app_error');
- done();
- }
- );
- });
- });
-
- it('License.getClientLicenceConfig', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getClientLicenceConfig(
- function(data) {
- assert.equal(data.IsLicensed, 'false');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('License.removeLicenseFile', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().removeLicenseFile(
- function() {
- done(new Error('not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.permissions.app_error');
- done();
- }
- );
- });
- });
-
- /*it('License.uploadLicenseFile', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().uploadLicenseFile(
- 'form data',
- function() {
- done(new Error('not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.permissions.app_error');
- done();
- }
- );
- });
- });*/
-
- // TODO XXX FIX ME - this test depends on make dist
-
- // it('General.getTranslations', function(done) {
- // TestHelper.initBasic(() => {
- // TestHelper.basicClient().getTranslations(
- // 'http://localhost:8065/static/i18n/es.json',
- // function(data) {
- // assert.equal(data['login.or'], 'o');
- // done();
- // },
- // function(err) {
- // done(new Error(err.message));
- // }
- // );
- // });
- // });
-
- it('File.getFileInfo', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
-
- TestHelper.basicClient().getFileInfo(
- `/${TestHelper.basicChannel().id}/${TestHelper.basicUser().id}/filename.txt`,
- function(data) {
- assert.equal(data.filename, 'filename.txt');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('File.getPublicLink', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- var data = {};
- data.channel_id = TestHelper.basicChannel().id;
- data.user_id = TestHelper.basicUser().id;
- data.filename = `/${TestHelper.basicChannel().id}/${TestHelper.basicUser().id}/filename.txt`;
-
- TestHelper.basicClient().getPublicLink(
- data,
- function() {
- done(new Error('not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.file.get_public_link.disabled.app_error');
- done();
- }
- );
- });
- });
-});
-
diff --git a/webapp/tests/client_hooks.test.jsx b/webapp/tests/client_hooks.test.jsx
deleted file mode 100644
index 0cad22153..000000000
--- a/webapp/tests/client_hooks.test.jsx
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
-
-import assert from 'assert';
-import TestHelper from './test_helper.jsx';
-
-describe('Client.Hooks', function() {
- this.timeout(100000);
-
- it('addIncomingHook', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
-
- var hook = {};
- hook.channel_id = TestHelper.basicChannel().id;
- hook.description = 'desc';
- hook.display_name = 'Unit Test';
-
- TestHelper.basicClient().addIncomingHook(
- hook,
- function() {
- done(new Error('hooks not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.webhook.create_incoming.disabled.app_errror');
- done();
- }
- );
- });
- });
-
- it('deleteIncomingHook', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().deleteIncomingHook(
- TestHelper.generateId(),
- function() {
- done(new Error('hooks not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.webhook.delete_incoming.disabled.app_errror');
- done();
- }
- );
- });
- });
-
- it('listIncomingHooks', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().listIncomingHooks(
- function() {
- done(new Error('hooks not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.webhook.get_incoming.disabled.app_error');
- done();
- }
- );
- });
- });
-
- it('addOutgoingHook', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
-
- var hook = {};
- hook.channel_id = TestHelper.basicChannel().id;
- hook.description = 'desc';
- hook.display_name = 'Unit Test';
-
- TestHelper.basicClient().addOutgoingHook(
- hook,
- function() {
- done(new Error('hooks not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.webhook.create_outgoing.disabled.app_error');
- done();
- }
- );
- });
- });
-
- it('deleteOutgoingHook', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().deleteOutgoingHook(
- TestHelper.generateId(),
- function() {
- done(new Error('hooks not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.webhook.delete_outgoing.disabled.app_error');
- done();
- }
- );
- });
- });
-
- it('listOutgoingHooks', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().listOutgoingHooks(
- function() {
- done(new Error('hooks not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.webhook.get_outgoing.disabled.app_error');
- done();
- }
- );
- });
- });
-
- it('regenOutgoingHookToken', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().regenOutgoingHookToken(
- TestHelper.generateId(),
- function() {
- done(new Error('hooks not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.webhook.regen_outgoing_token.disabled.app_error');
- done();
- }
- );
- });
- });
-});
-
diff --git a/webapp/tests/client_oauth.test.jsx b/webapp/tests/client_oauth.test.jsx
deleted file mode 100644
index df2fc665b..000000000
--- a/webapp/tests/client_oauth.test.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
-
-import assert from 'assert';
-import TestHelper from './test_helper.jsx';
-
-describe('Client.OAuth', function() {
- this.timeout(100000);
-
- it('registerOAuthApp', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
-
- var app = {};
- app.name = 'test';
- app.homepage = 'homepage';
- app.description = 'desc';
- app.callback_urls = '';
-
- TestHelper.basicClient().registerOAuthApp(
- app,
- function() {
- done(new Error('not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.oauth.register_oauth_app.turn_off.app_error');
- done();
- }
- );
- });
- });
-
- it('allowOAuth2', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
-
- TestHelper.basicClient().allowOAuth2(
- 'GET',
- '123456',
- 'http://nowhere.com',
- 'state',
- 'scope',
- function() {
- done(new Error('not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'api.oauth.allow_oauth.turn_off.app_error');
- done();
- }
- );
- });
- });
-});
diff --git a/webapp/tests/client_post.test.jsx b/webapp/tests/client_post.test.jsx
deleted file mode 100644
index c8e6fad0f..000000000
--- a/webapp/tests/client_post.test.jsx
+++ /dev/null
@@ -1,208 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
-
-import assert from 'assert';
-import TestHelper from './test_helper.jsx';
-
-describe('Client.Posts', function() {
- this.timeout(100000);
-
- it('createPost', function(done) {
- TestHelper.initBasic(() => {
- var post = TestHelper.fakePost();
- post.channel_id = TestHelper.basicChannel().id;
-
- TestHelper.basicClient().createPost(
- post,
- function(data) {
- assert.equal(data.id.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getPostById', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getPostById(
- TestHelper.basicPost().id,
- function(data) {
- assert.equal(data.order[0], TestHelper.basicPost().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getPost', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getPost(
- TestHelper.basicChannel().id,
- TestHelper.basicPost().id,
- function(data) {
- assert.equal(data.order[0], TestHelper.basicPost().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updatePost', function(done) {
- TestHelper.initBasic(() => {
- var post = TestHelper.basicPost();
- post.message = 'new message';
- post.channel_id = TestHelper.basicChannel().id;
-
- TestHelper.basicClient().updatePost(
- post,
- function(data) {
- assert.equal(data.id.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('deletePost', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().deletePost(
- TestHelper.basicChannel().id,
- TestHelper.basicPost().id,
- function(data) {
- assert.equal(data.id, TestHelper.basicPost().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('searchPost', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().search(
- 'unit test',
- false,
- function(data) {
- assert.equal(data.order[0], TestHelper.basicPost().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getPostsPage', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getPostsPage(
- TestHelper.basicChannel().id,
- 0,
- 10,
- function(data) {
- assert.equal(data.order[0], TestHelper.basicPost().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getPosts', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getPosts(
- TestHelper.basicChannel().id,
- 0,
- function(data) {
- assert.equal(data.order[0], TestHelper.basicPost().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getPostsBefore', function(done) {
- TestHelper.initBasic(() => {
- var post = TestHelper.fakePost();
- post.channel_id = TestHelper.basicChannel().id;
-
- TestHelper.basicClient().createPost(
- post,
- function(rpost) {
- TestHelper.basicClient().getPostsBefore(
- TestHelper.basicChannel().id,
- rpost.id,
- 0,
- 10,
- function(data) {
- assert.equal(data.order[0], TestHelper.basicPost().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getPostsAfter', function(done) {
- TestHelper.initBasic(() => {
- var post = TestHelper.fakePost();
- post.channel_id = TestHelper.basicChannel().id;
-
- TestHelper.basicClient().createPost(
- post,
- function(rpost) {
- TestHelper.basicClient().getPostsAfter(
- TestHelper.basicChannel().id,
- TestHelper.basicPost().id,
- 0,
- 10,
- function(data) {
- assert.equal(data.order[0], rpost.id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-});
-
diff --git a/webapp/tests/client_preferences.test.jsx b/webapp/tests/client_preferences.test.jsx
deleted file mode 100644
index 987728704..000000000
--- a/webapp/tests/client_preferences.test.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
-
-import assert from 'assert';
-import TestHelper from './test_helper.jsx';
-
-describe('Client.Preferences', function() {
- this.timeout(100000);
-
- it('getAllPreferences', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getAllPreferences(
- function(data) {
- assert.equal(data[0].category, 'tutorial_step');
- assert.equal(data[0].user_id, TestHelper.basicUser().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('savePreferences', function(done) {
- TestHelper.initBasic(() => {
- var perf = {};
- perf.user_id = TestHelper.basicUser().id;
- perf.category = 'test';
- perf.name = 'name';
- perf.value = 'value';
-
- var perfs = [];
- perfs.push(perf);
-
- TestHelper.basicClient().savePreferences(
- perfs,
- function(data) {
- assert.equal(data, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getPreferenceCategory', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getPreferenceCategory(
- 'tutorial_step',
- function(data) {
- assert.equal(data[0].category, 'tutorial_step');
- assert.equal(data[0].user_id, TestHelper.basicUser().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-});
-
diff --git a/webapp/tests/client_team.test.jsx b/webapp/tests/client_team.test.jsx
deleted file mode 100644
index e8b71d2f8..000000000
--- a/webapp/tests/client_team.test.jsx
+++ /dev/null
@@ -1,235 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
-
-import assert from 'assert';
-import TestHelper from './test_helper.jsx';
-
-describe('Client.Team', function() {
- this.timeout(100000);
-
- it('findTeamByName', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().findTeamByName(
- TestHelper.basicTeam().name,
- function(data) {
- assert.equal(data, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('signupTeam', function(done) {
- var client = TestHelper.createClient();
- var email = TestHelper.fakeEmail();
-
- client.signupTeam(
- email,
- function(data) {
- assert.equal(data.email, email);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
-
- it('createTeamFromSignup', function(done) {
- var client = TestHelper.createClient();
- var email = TestHelper.fakeEmail();
-
- client.signupTeam(
- email,
- function(data) {
- var teamSignup = {};
- teamSignup.invites = [];
- teamSignup.data = decodeURIComponent(data.follow_link.split('&h=')[0].replace('/signup_team_complete/?d=', ''));
- teamSignup.hash = decodeURIComponent(data.follow_link.split('&h=')[1]);
-
- teamSignup.user = TestHelper.fakeUser();
- teamSignup.team = TestHelper.fakeTeam();
- teamSignup.team.email = teamSignup.user.email;
-
- client.createTeamFromSignup(
- teamSignup,
- function(data2) {
- assert.equal(data2.team.id.length > 0, true);
- assert.equal(data2.user.id.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
-
- it('createTeam', function(done) {
- var client = TestHelper.createClient();
- var team = TestHelper.fakeTeam();
- client.createTeam(
- team,
- function(data) {
- assert.equal(data.id.length > 0, true);
- assert.equal(data.name, team.name);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
-
- it('getAllTeams', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getAllTeams(
- function(data) {
- assert.equal(data[TestHelper.basicTeam().id].name, TestHelper.basicTeam().name);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getAllTeamListings', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getAllTeamListings(
- function(data) {
- console.log(data);
- assert.equal(data != null, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getMyTeam', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getMyTeam(
- function(data) {
- assert.equal(data.name, TestHelper.basicTeam().name);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('GetTeamMembers', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getTeamMembers(
- TestHelper.basicTeam().id,
- function(data) {
- assert.equal(data.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('inviteMembers', function(done) {
- TestHelper.initBasic(() => {
- var data = {};
- data.invites = [];
- var invite = {};
- invite.email = TestHelper.fakeEmail();
- invite.firstName = 'first';
- invite.lastName = 'last';
- data.invites.push(invite);
-
- TestHelper.basicClient().inviteMembers(
- data,
- function(dataBack) {
- assert.equal(dataBack.invites.length, 1);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updateTeam', function(done) {
- TestHelper.initBasic(() => {
- var team = TestHelper.basicTeam();
- team.display_name = 'test_updated';
-
- TestHelper.basicClient().updateTeam(
- team,
- function(data) {
- assert.equal(data.display_name, 'test_updated');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('addUserToTeam', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().createUser(
- TestHelper.fakeUser(),
- function(user2) {
- TestHelper.basicClient().addUserToTeam(
- user2.id,
- function(data) {
- assert.equal(data.user_id, user2.id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getInviteInfo', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getInviteInfo(
- TestHelper.basicTeam().invite_id,
- function(data) {
- assert.equal(data.display_name.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-});
-
diff --git a/webapp/tests/client_user.test.jsx b/webapp/tests/client_user.test.jsx
deleted file mode 100644
index 2b3b1b89a..000000000
--- a/webapp/tests/client_user.test.jsx
+++ /dev/null
@@ -1,565 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
-
-import assert from 'assert';
-import TestHelper from './test_helper.jsx';
-
-describe('Client.User', function() {
- this.timeout(100000);
-
- it('getMe', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getMe(
- function(data) {
- assert.equal(data.id, TestHelper.basicUser().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getInitialLoad', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getInitialLoad(
- function(data) {
- assert.equal(data.user.id.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('createUser', function(done) {
- var client = TestHelper.createClient();
- var user = TestHelper.fakeUser();
- client.createUser(
- user,
- function(data) {
- assert.equal(data.id.length > 0, true);
- assert.equal(data.email, user.email);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
-
- it('loginByEmail', function(done) {
- var client = TestHelper.createClient();
- var user = TestHelper.fakeUser();
- client.createUser(
- user,
- function() {
- client.login(
- user.email,
- user.password,
- null,
- function(data) {
- assert.equal(data.id.length > 0, true);
- assert.equal(data.email, user.email);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
-
- it('loginById', function(done) {
- var client = TestHelper.createClient();
- var user = TestHelper.fakeUser();
- client.createUser(
- user,
- function(newUser) {
- assert.equal(user.email, newUser.email);
- client.loginById(
- newUser.id,
- user.password,
- null,
- function(data) {
- assert.equal(data.id.length > 0, true);
- assert.equal(data.email, user.email);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
-
- it('loginByUsername', function(done) {
- var client = TestHelper.createClient();
- client.enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- var user = TestHelper.fakeUser();
- client.createUser(
- user,
- function() {
- client.login(
- user.username,
- user.password,
- null,
- function() {
- done(new Error());
- },
- function(err) {
- // should error out because logging in by username is disabled by default
- assert.equal(err.id, 'store.sql_user.get_for_login.app_error');
- done();
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
-
- it('updateUser', function(done) {
- TestHelper.initBasic(() => {
- var user = TestHelper.basicUser();
- user.nickname = 'updated';
-
- TestHelper.basicClient().updateUser(
- user,
- function(data) {
- assert.equal(data.nickname, 'updated');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updatePassword', function(done) {
- TestHelper.initBasic(() => {
- var user = TestHelper.basicUser();
-
- TestHelper.basicClient().updatePassword(
- user.id,
- user.password,
- 'update_password',
- function(data) {
- assert.equal(data.user_id, user.id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updateUserNotifyProps', function(done) {
- TestHelper.initBasic(() => {
- var user = TestHelper.basicUser();
-
- var notifyProps = {
- all: 'true',
- channel: 'true',
- desktop: 'all',
- desktop_sound: 'true',
- email: 'false',
- first_name: 'false',
- mention_keys: '',
- user_id: user.id
- };
-
- TestHelper.basicClient().updateUserNotifyProps(
- notifyProps,
- function(data) {
- assert.equal(data.notify_props.email, 'false');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updateRoles', function(done) {
- TestHelper.initBasic(() => {
- var user = TestHelper.basicUser();
- var team = TestHelper.basicTeam();
-
- TestHelper.basicClient().updateRoles(
- team.id,
- user.id,
- '',
- function(data) {
- assert.equal(data.user_id, user.id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updateActive', function(done) {
- TestHelper.initBasic(() => {
- var user = TestHelper.basicUser();
-
- TestHelper.basicClient().updateActive(
- user.id,
- false,
- function(data) {
- assert.equal(data.last_activity_at > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('sendPasswordReset', function(done) {
- TestHelper.initBasic(() => {
- var user = TestHelper.basicUser();
-
- TestHelper.basicClient().sendPasswordReset(
- user.email,
- function(data) {
- assert.equal(data.email, user.email);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('resetPassword', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
-
- TestHelper.basicClient().resetPassword(
- '',
- 'new_password',
- function() {
- throw Error('shouldnt work');
- },
- function(err) {
- // this should fail since you're not a system admin
- assert.equal(err.id, 'api.context.invalid_param.app_error');
- done();
- }
- );
- });
- });
-
- it('emailToOAuth', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- var user = TestHelper.basicUser();
-
- TestHelper.basicClient().emailToOAuth(
- user.email,
- 'new_password',
- 'gitlab',
- function() {
- throw Error('shouldnt work');
- },
- function(err) {
- // this should fail since you're not a system admin
- assert.equal(err.id, 'api.user.check_user_password.invalid.app_error');
- done();
- }
- );
- });
- });
-
- it('oauthToEmail', function(done) {
- TestHelper.initBasic(() => {
- var user = TestHelper.basicUser();
-
- TestHelper.basicClient().oauthToEmail(
- user.email,
- 'new_password',
- function(data) {
- assert.equal(data.follow_link.length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('emailToLdap', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- var user = TestHelper.basicUser();
-
- TestHelper.basicClient().emailToLdap(
- user.email,
- user.password,
- 'unknown_id',
- 'unknown_pwd',
- function() {
- throw Error('shouldnt work');
- },
- function(err) {
- assert.equal(err.id, 'ent.ldap.do_login.licence_disable.app_error');
- done();
- }
- );
- });
- });
-
- it('ldapToEmail', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- var user = TestHelper.basicUser();
-
- TestHelper.basicClient().ldapToEmail(
- user.email,
- 'new_password',
- 'new_password',
- function() {
- throw Error('shouldnt work');
- },
- function(err) {
- assert.equal(err.id, 'api.user.ldap_to_email.not_ldap_account.app_error');
- done();
- }
- );
- });
- });
-
- it('logout', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().logout(
- function(data) {
- assert.equal(data.user_id, TestHelper.basicUser().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('checkMfa', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().checkMfa(
- TestHelper.generateId(),
- function(data) {
- assert.equal(data.mfa_required, 'false');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getSessions', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getSessions(
- TestHelper.basicUser().id,
- function(data) {
- assert.equal(data[0].user_id, TestHelper.basicUser().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('revokeSession', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getSessions(
- TestHelper.basicUser().id,
- function(sessions) {
- TestHelper.basicClient().revokeSession(
- sessions[0].id,
- function(data) {
- assert.equal(data.id, sessions[0].id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getAudits', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getAudits(
- TestHelper.basicUser().id,
- function(data) {
- assert.equal(data[0].user_id, TestHelper.basicUser().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getDirectProfiles', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getDirectProfiles(
- function(data) {
- assert.equal(Object.keys(data).length === 0, true);
- done();
- },
- function(err) {
- done(new Error(err.getDirectProfiles));
- }
- );
- });
- });
-
- it('getProfiles', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getProfiles(
- function(data) {
- assert.equal(data[TestHelper.basicUser().id].id, TestHelper.basicUser().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getProfilesForTeam', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getProfilesForTeam(
- TestHelper.basicTeam().id,
- function(data) {
- assert.equal(data[TestHelper.basicUser().id].id, TestHelper.basicUser().id);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getProfilesForDirectMessageList', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().getProfilesForDirectMessageList(
- function(data) {
- assert.equal(Object.keys(data).length > 0, true);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('getStatuses', function(done) {
- TestHelper.initBasic(() => {
- var ids = [];
- ids.push(TestHelper.basicUser().id);
-
- TestHelper.basicClient().getStatuses(
- ids,
- function(data) {
- assert.equal(data[TestHelper.basicUser().id], 'online');
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('verifyEmail', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().verifyEmail(
- 'junk',
- 'junk',
- function() {
- done(new Error('should be invalid'));
- },
- function(err) {
- assert.equal(err.id, 'api.context.invalid_param.app_error');
- done();
- }
- );
- });
- });
-
- it('resendVerification', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().resendVerification(
- TestHelper.basicUser().email,
- function() {
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
- });
- });
-
- it('updateMfa', function(done) {
- TestHelper.initBasic(() => {
- TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
- TestHelper.basicClient().updateMfa(
- 'junk',
- true,
- function() {
- done(new Error('not enabled'));
- },
- function(err) {
- assert.equal(err.id, 'ent.mfa.license_disable.app_error');
- done();
- }
- );
- });
- });
-});
diff --git a/webapp/tests/emoticons.test.jsx b/webapp/tests/emoticons.test.jsx
new file mode 100644
index 000000000..bb0421651
--- /dev/null
+++ b/webapp/tests/emoticons.test.jsx
@@ -0,0 +1,44 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import assert from 'assert';
+
+import * as Emoticons from 'utils/emoticons.jsx';
+
+describe('Emoticons', function() {
+ this.timeout(100000);
+
+ it('handleEmoticons', function(done) {
+ assert.equal(
+ Emoticons.handleEmoticons(':goat: :dash:', new Map()),
+ 'MM_EMOTICON0 MM_EMOTICON1',
+ 'should replace emoticons with tokens'
+ );
+
+ assert.equal(
+ Emoticons.handleEmoticons(':goat::dash:', new Map()),
+ 'MM_EMOTICON0MM_EMOTICON1',
+ 'should replace emoticons not separated by whitespace'
+ );
+
+ assert.equal(
+ Emoticons.handleEmoticons('/:goat:..:dash:)', new Map()),
+ '/MM_EMOTICON0..MM_EMOTICON1)',
+ 'should replace emoticons separated by punctuation'
+ );
+
+ assert.equal(
+ Emoticons.handleEmoticons('asdf:goat:asdf:dash:asdf', new Map()),
+ 'asdfMM_EMOTICON0asdfMM_EMOTICON1asdf',
+ 'should replace emoticons separated by text'
+ );
+
+ assert.equal(
+ Emoticons.handleEmoticons(':asdf: :goat : : dash:', new Map()),
+ ':asdf: :goat : : dash:',
+ 'shouldn\'t replace invalid emoticons'
+ );
+
+ done();
+ });
+}); \ No newline at end of file
diff --git a/webapp/tests/spinner_button.test.jsx b/webapp/tests/spinner_button.test.jsx
index 0e282e0ee..296f4eaff 100644
--- a/webapp/tests/spinner_button.test.jsx
+++ b/webapp/tests/spinner_button.test.jsx
@@ -1,9 +1,3 @@
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
var jsdom = require('mocha-jsdom');
var assert = require('assert');
@@ -12,6 +6,7 @@ import SpinnerButton from '../components/spinner_button.jsx';
import React from 'react';
describe('SpinnerButton', function() {
+ this.timeout(10000);
jsdom();
it('check props', function() {
@@ -21,4 +16,4 @@ describe('SpinnerButton', function() {
assert.equal(spinner.props.spinning, false, 'should start in the default false state');
});
-}); \ No newline at end of file
+});
diff --git a/webapp/tests/test_helper.jsx b/webapp/tests/test_helper.jsx
deleted file mode 100644
index f19d96433..000000000
--- a/webapp/tests/test_helper.jsx
+++ /dev/null
@@ -1,182 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-/* eslint-disable no-console */
-/* eslint-disable global-require */
-/* eslint-disable func-names */
-/* eslint-disable prefer-arrow-callback */
-/* eslint-disable no-magic-numbers */
-/* eslint-disable no-unreachable */
-/* eslint-disable new-cap */
-
-import Client from '../client/client.jsx';
-import jqd from 'jquery-deferred';
-
-class TestHelperClass {
- basicClient = () => {
- return this.basicc;
- }
-
- basicTeam = () => {
- return this.basict;
- }
-
- basicUser = () => {
- return this.basicu;
- }
-
- basicChannel = () => {
- return this.basicch;
- }
-
- basicPost = () => {
- return this.basicp;
- }
-
- generateId = () => {
- // implementation taken from http://stackoverflow.com/a/2117523
- var id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
-
- id = id.replace(/[xy]/g, function replaceRandom(c) {
- var r = Math.floor(Math.random() * 16);
-
- var v;
- if (c === 'x') {
- v = r;
- } else {
- v = r & 0x3 | 0x8;
- }
-
- return v.toString(16);
- });
-
- return 'uid' + id;
- }
-
- createClient() {
- var c = new Client();
- c.setUrl('http://localhost:8065');
- c.useHeaderToken();
- c.enableLogErrorsToConsole(true);
- return c;
- }
-
- fakeEmail = () => {
- return 'success' + this.generateId() + '@simulator.amazonses.com';
- }
-
- fakeUser = () => {
- var user = {};
- user.email = this.fakeEmail();
- user.allow_marketing = true;
- user.password = 'password1';
- user.username = this.generateId();
- return user;
- }
-
- fakeTeam = () => {
- var team = {};
- team.name = this.generateId();
- team.display_name = `Unit Test ${team.name}`;
- team.type = 'O';
- team.email = this.fakeEmail();
- team.allowed_domains = '';
- return team;
- }
-
- fakeChannel = () => {
- var channel = {};
- channel.name = this.generateId();
- channel.display_name = `Unit Test ${channel.name}`;
- channel.type = 'O'; // open channel
- return channel;
- }
-
- fakePost = () => {
- var post = {};
- post.message = `Unit Test ${this.generateId()}`;
- return post;
- }
-
- initBasic = (callback) => {
- this.basicc = this.createClient();
-
- var d1 = jqd.Deferred();
- var email = this.fakeEmail();
- var outer = this; // eslint-disable-line consistent-this
-
- this.basicClient().signupTeam(
- email,
- function(rsignUp) {
- var teamSignup = {};
- teamSignup.invites = [];
- teamSignup.data = decodeURIComponent(rsignUp.follow_link.split('&h=')[0].replace('/signup_team_complete/?d=', ''));
- teamSignup.hash = decodeURIComponent(rsignUp.follow_link.split('&h=')[1]);
-
- teamSignup.user = outer.fakeUser();
- teamSignup.team = outer.fakeTeam();
- teamSignup.team.email = email;
- teamSignup.user.email = email;
- var password = teamSignup.user.password;
-
- outer.basicClient().createTeamFromSignup(
- teamSignup,
- function(rteamSignup) {
- outer.basict = rteamSignup.team;
- outer.basicu = rteamSignup.user;
- outer.basicu.password = password;
- outer.basicClient().setTeamId(outer.basict.id);
- outer.basicClient().login(
- rteamSignup.user.email,
- password,
- null,
- function() {
- outer.basicClient().useHeaderToken();
- var channel = outer.fakeChannel();
- channel.team_id = outer.basicTeam().id;
- outer.basicClient().createChannel(
- channel,
- function(rchannel) {
- outer.basicch = rchannel;
- var post = outer.fakePost();
- post.channel_id = rchannel.id;
-
- outer.basicClient().createPost(
- post,
- function(rpost) {
- outer.basicp = rpost;
- d1.resolve();
- },
- function(err) {
- throw err;
- }
- );
- },
- function(err) {
- throw err;
- }
- );
- },
- function(err) {
- throw err;
- }
- );
- },
- function(err) {
- throw err;
- }
- );
- },
- function(err) {
- throw err;
- }
- );
-
- jqd.when(d1).done(() => {
- callback();
- });
- }
-}
-
-var TestHelper = new TestHelperClass();
-export default TestHelper;
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 6535c024d..6f5f8a2cd 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -3,7 +3,7 @@
import $ from 'jquery';
import Client from './web_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
@@ -47,7 +47,21 @@ function isCallInProgress(callName) {
return true;
}
-export function getChannels(checkVersion) {
+export function checkVersion() {
+ var serverVersion = Client.getServerVersion();
+
+ if (serverVersion !== BrowserStore.getLastServerVersion()) {
+ if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') {
+ BrowserStore.setLastServerVersion(serverVersion);
+ } else {
+ BrowserStore.setLastServerVersion(serverVersion);
+ window.location.reload(true);
+ console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
+ }
+ }
+}
+
+export function getChannels(doVersionCheck) {
if (isCallInProgress('getChannels')) {
return null;
}
@@ -58,18 +72,8 @@ export function getChannels(checkVersion) {
(data) => {
callTracker.getChannels = 0;
- if (checkVersion) {
- var serverVersion = Client.getServerVersion();
-
- if (serverVersion !== BrowserStore.getLastServerVersion()) {
- if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') {
- BrowserStore.setLastServerVersion(serverVersion);
- } else {
- BrowserStore.setLastServerVersion(serverVersion);
- window.location.reload(true);
- console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
- }
- }
+ if (doVersionCheck) {
+ checkVersion();
}
AppDispatcher.handleServerAction({
diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx
index 1d18e26ba..c9dd30712 100644
--- a/webapp/utils/channel_intro_messages.jsx
+++ b/webapp/utils/channel_intro_messages.jsx
@@ -8,7 +8,7 @@ import ToggleModalButton from 'components/toggle_modal_button.jsx';
import UserProfile from 'components/user_profile.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import Constants from 'utils/constants.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Client from 'utils/web_client.jsx';
import React from 'react';
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 109291d1f..0e2ae07ea 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -88,6 +88,7 @@ export default {
RECEIVED_MSG: null,
RECEIVED_MY_TEAM: null,
+ CREATED_TEAM: null,
RECEIVED_CONFIG: null,
RECEIVED_LOGS: null,
@@ -249,7 +250,8 @@ export default {
RESERVED_USERNAMES: [
'valet',
'all',
- 'channel'
+ 'channel',
+ 'here'
],
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
MAX_DMS: 20,
@@ -304,7 +306,7 @@ export default {
sidebarTextHoverBg: '#e6f2fa',
sidebarTextActiveBorder: '#378FD2',
sidebarTextActiveColor: '#111111',
- sidebarHeaderBg: '#2389d7',
+ sidebarHeaderBg: '#3481B9',
sidebarHeaderTextColor: '#ffffff',
onlineIndicator: '#7DBE00',
awayIndicator: '#DCBD4E',
@@ -314,7 +316,7 @@ export default {
centerChannelColor: '#333333',
newMessageSeparator: '#FF8800',
linkColor: '#2389d7',
- buttonBg: '#2389d7',
+ buttonBg: '#23A2FF',
buttonColor: '#FFFFFF',
mentionHighlightBg: '#fff2bb',
mentionHighlightLink: '#2f81b7',
@@ -530,7 +532,11 @@ export default {
CHANNEL_DISPLAY_MODE: 'channel_display_mode',
CHANNEL_DISPLAY_MODE_CENTERED: 'centered',
CHANNEL_DISPLAY_MODE_FULL_SCREEN: 'full',
- CHANNEL_DISPLAY_MODE_DEFAULT: 'centered'
+ CHANNEL_DISPLAY_MODE_DEFAULT: 'centered',
+ MESSAGE_DISPLAY: 'message_display',
+ MESSAGE_DISPLAY_CLEAN: 'clean',
+ MESSAGE_DISPLAY_COMPACT: 'compact',
+ MESSAGE_DISPLAY_DEFAULT: 'clean'
},
TutorialSteps: {
INTRO_SCREENS: 0,
@@ -539,16 +545,101 @@ export default {
MENU_POPOVER: 3
},
KeyCodes: {
- UP: 38,
- DOWN: 40,
- LEFT: 37,
- RIGHT: 39,
BACKSPACE: 8,
+ TAB: 9,
ENTER: 13,
+ SHIFT: 16,
+ CTRL: 17,
+ ALT: 18,
+ CAPS_LOCK: 20,
ESCAPE: 27,
SPACE: 32,
- TAB: 9,
- U: 85
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
+ END: 35,
+ HOME: 36,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ INSERT: 45,
+ DELETE: 46,
+ ZERO: 48,
+ ONE: 49,
+ TWO: 50,
+ THREE: 51,
+ FOUR: 52,
+ FIVE: 53,
+ SIX: 54,
+ SEVEN: 55,
+ EIGHT: 56,
+ NINE: 57,
+ A: 65,
+ B: 66,
+ C: 67,
+ D: 68,
+ E: 69,
+ F: 70,
+ G: 71,
+ H: 72,
+ I: 73,
+ J: 74,
+ K: 75,
+ L: 76,
+ M: 77,
+ N: 78,
+ O: 79,
+ P: 80,
+ Q: 81,
+ R: 82,
+ S: 83,
+ T: 84,
+ U: 85,
+ V: 86,
+ W: 87,
+ X: 88,
+ Y: 89,
+ Z: 90,
+ CMD: 91,
+ MENU: 93,
+ NUMPAD_0: 96,
+ NUMPAD_1: 97,
+ NUMPAD_2: 98,
+ NUMPAD_3: 99,
+ NUMPAD_4: 100,
+ NUMPAD_5: 101,
+ NUMPAD_6: 102,
+ NUMPAD_7: 103,
+ NUMPAD_8: 104,
+ NUMPAD_9: 105,
+ MULTIPLY: 106,
+ ADD: 107,
+ SUBTRACT: 109,
+ DECIMAL: 110,
+ DIVIDE: 111,
+ F1: 112,
+ F2: 113,
+ F3: 114,
+ F4: 115,
+ F5: 116,
+ F6: 117,
+ F7: 118,
+ F8: 119,
+ F9: 120,
+ F10: 121,
+ F11: 122,
+ F12: 123,
+ NUM_LOCK: 144,
+ SEMICOLON: 186,
+ EQUAL: 187,
+ COMMA: 188,
+ DASH: 189,
+ PERIOD: 190,
+ FORWARD_SLASH: 191,
+ TILDE: 192,
+ OPEN_BRACKET: 219,
+ BACK_SLASH: 220,
+ CLOSE_BRACKET: 221
},
CODE_PREVIEW_MAX_FILE_SIZE: 500000, // 500 KB
HighlightedLanguages: {
@@ -633,14 +724,19 @@ export default {
}
},
OVERLAY_TIME_DELAY: 400,
+ MIN_TEAMNAME_LENGTH: 4,
+ MAX_TEAMNAME_LENGTH: 15,
MIN_USERNAME_LENGTH: 3,
MAX_USERNAME_LENGTH: 22,
MIN_PASSWORD_LENGTH: 5,
MAX_PASSWORD_LENGTH: 50,
+ MIN_TRIGGER_LENGTH: 1,
+ MAX_TRIGGER_LENGTH: 128,
TIME_SINCE_UPDATE_INTERVAL: 30000,
MIN_HASHTAG_LINK_LENGTH: 3,
EMOJI_PATH: '/static/emoji',
DEFAULT_WEBHOOK_LOGO: logoWebhook,
MHPNS: 'https://push.mattermost.com',
- MTPNS: 'http://push-test.mattermost.com'
+ MTPNS: 'http://push-test.mattermost.com',
+ BOT_NAME: 'BOT'
};
diff --git a/webapp/utils/emoticons.jsx b/webapp/utils/emoticons.jsx
index 505e10c19..35c7dba04 100644
--- a/webapp/utils/emoticons.jsx
+++ b/webapp/utils/emoticons.jsx
@@ -1,8 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-
import Constants from './constants.jsx';
import emojis from './emoji.json';
@@ -134,7 +132,7 @@ export function getEmoticonsByCodePoint() {
export function handleEmoticons(text, tokens) {
let output = text;
- function replaceEmoticonWithToken(fullMatch, prefix, matchText, name) {
+ function replaceEmoticonWithToken(fullMatch, matchText, name) {
if (getEmoticonsByName().has(name)) {
const index = tokens.size;
const alias = `MM_EMOTICON${index}`;
@@ -145,19 +143,23 @@ export function handleEmoticons(text, tokens) {
originalText: fullMatch
});
- return prefix + alias;
+ return alias;
}
return fullMatch;
}
- output = output.replace(/(^|\s)(:([a-zA-Z0-9_-]+):)(?=$|\s)/g, (fullMatch, prefix, matchText, name) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name));
+ // match named emoticons like :goat:
+ output = output.replace(/(:([a-zA-Z0-9_-]+):)/g, (fullMatch, matchText, name) => replaceEmoticonWithToken(fullMatch, matchText, name));
+
+ // match text smilies like :D
+ for (const name of Object.keys(emoticonPatterns)) {
+ const pattern = emoticonPatterns[name];
- $.each(emoticonPatterns, (name, pattern) => {
// this might look a bit funny, but since the name isn't contained in the actual match
// like with the named emoticons, we need to add it in manually
- output = output.replace(pattern, (fullMatch, prefix, matchText) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name));
- });
+ output = output.replace(pattern, (fullMatch, matchText) => replaceEmoticonWithToken(fullMatch, matchText, name));
+ }
return output;
}
diff --git a/webapp/utils/markdown.jsx b/webapp/utils/markdown.jsx
index 2ddd3fe11..809ecc526 100644
--- a/webapp/utils/markdown.jsx
+++ b/webapp/utils/markdown.jsx
@@ -13,40 +13,6 @@ function markdownImageLoaded(image) {
}
window.markdownImageLoaded = markdownImageLoaded;
-class MattermostInlineLexer extends marked.InlineLexer {
- constructor(links, options) {
- super(links, options);
-
- this.rules = Object.assign({}, this.rules);
-
- // modified version of the regex that allows for links starting with www and those surrounded by parentheses
- // the original is /^[\s\S]+?(?=[\\<!\[_*`~]|https?:\/\/| {2,}\n|$)/
- this.rules.text = /^[\s\S]+?(?=[\\<!\[_*`~]|https?:\/\/|www\.|\(| {2,}\n|$)/;
-
- // modified version of the regex that allows links starting with www and those surrounded by parentheses
- // the original is /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/
- this.rules.url = /^(\(?(?:https?:\/\/|www\.)[^\s<.][^\s<]*[^<.,:;"'\]\s])/;
-
- // modified version of the regex that allows <links> starting with www.
- // the original is /^<([^ >]+(@|:\/)[^ >]+)>/
- this.rules.autolink = /^<((?:[^ >]+(@|:\/)|www\.)[^ >]+)>/;
- }
-}
-
-class MattermostParser extends marked.Parser {
- parse(src) {
- this.inline = new MattermostInlineLexer(src.links, this.options, this.renderer);
- this.tokens = src.reverse();
-
- var out = '';
- while (this.next()) {
- out += this.tok();
- }
-
- return out;
- }
-}
-
class MattermostMarkdownRenderer extends marked.Renderer {
constructor(options, formattingOptions = {}) {
super(options);
@@ -109,18 +75,6 @@ class MattermostMarkdownRenderer extends marked.Renderer {
link(href, title, text) {
let outHref = href;
- let outText = text;
- let prefix = '';
- let suffix = '';
-
- // some links like https://en.wikipedia.org/wiki/Rendering_(computer_graphics) contain brackets
- // and we try our best to differentiate those from ones just wrapped in brackets when autolinking
- if (outHref.startsWith('(') && outHref.endsWith(')') && text === outHref) {
- prefix = '(';
- suffix = ')';
- outText = text.substring(1, text.length - 1);
- outHref = outHref.substring(1, outHref.length - 1);
- }
try {
const unescaped = decodeURIComponent(unescape(href)).replace(/[^\w:]/g, '').toLowerCase();
@@ -149,9 +103,9 @@ class MattermostMarkdownRenderer extends marked.Renderer {
output += ' title="' + title + '"';
}
- output += '>' + outText + '</a>';
+ output += '>' + text + '</a>';
- return prefix + output + suffix;
+ return output;
}
paragraph(text) {
@@ -166,13 +120,19 @@ class MattermostMarkdownRenderer extends marked.Renderer {
return `<div class="table-responsive"><table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table></div>`;
}
- listitem(text) {
+ listitem(text, bullet) {
const taskListReg = /^\[([ |xX])\] /;
const isTaskList = taskListReg.exec(text);
if (isTaskList) {
return `<li class="list-item--task-list">${'<input type="checkbox" disabled="disabled" ' + (isTaskList[1] === ' ' ? '' : 'checked="checked" ') + '/> '}${text.replace(taskListReg, '')}</li>`;
}
+
+ if (/^\d+.$/.test(bullet)) {
+ // this is a numbered list item so override the numbering
+ return `<li value="${parseInt(bullet, 10)}">${text}</li>`;
+ }
+
return `<li>${text}</li>`;
}
@@ -181,301 +141,6 @@ class MattermostMarkdownRenderer extends marked.Renderer {
}
}
-class MattermostLexer extends marked.Lexer {
- token(originalSrc, top, bq) {
- let src = originalSrc.replace(/^ +$/gm, '');
-
- while (src) {
- // newline
- let cap = this.rules.newline.exec(src);
- if (cap) {
- src = src.substring(cap[0].length);
- if (cap[0].length > 1) {
- this.tokens.push({
- type: 'space'
- });
- }
- }
-
- // code
- cap = this.rules.code.exec(src);
- if (cap) {
- src = src.substring(cap[0].length);
- cap = cap[0].replace(/^ {4}/gm, '');
- this.tokens.push({
- type: 'code',
- text: this.options.pedantic ? cap : cap.replace(/\n+$/, '')
- });
- continue;
- }
-
- // fences (gfm)
- cap = this.rules.fences.exec(src);
- if (cap) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'code',
- lang: cap[2],
- text: cap[3] || ''
- });
- continue;
- }
-
- // heading
- cap = this.rules.heading.exec(src);
- if (cap) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'heading',
- depth: cap[1].length,
- text: cap[2]
- });
- continue;
- }
-
- // table no leading pipe (gfm)
- cap = this.rules.nptable.exec(src);
- if (top && cap) {
- src = src.substring(cap[0].length);
-
- const item = {
- type: 'table',
- header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
- align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
- cells: cap[3].replace(/\n$/, '').split('\n')
- };
-
- for (let i = 0; i < item.align.length; i++) {
- if (/^ *-+: *$/.test(item.align[i])) {
- item.align[i] = 'right';
- } else if (/^ *:-+: *$/.test(item.align[i])) {
- item.align[i] = 'center';
- } else if (/^ *:-+ *$/.test(item.align[i])) {
- item.align[i] = 'left';
- } else {
- item.align[i] = null;
- }
- }
-
- for (let i = 0; i < item.cells.length; i++) {
- item.cells[i] = item.cells[i].split(/ *\| */);
- }
-
- this.tokens.push(item);
-
- continue;
- }
-
- // lheading
- cap = this.rules.lheading.exec(src);
- if (cap) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'heading',
- depth: cap[2] === '=' ? 1 : 2,
- text: cap[1]
- });
- continue;
- }
-
- // hr
- cap = this.rules.hr.exec(src);
- if (cap) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'hr'
- });
- continue;
- }
-
- // blockquote
- cap = this.rules.blockquote.exec(src);
- if (cap) {
- src = src.substring(cap[0].length);
-
- this.tokens.push({
- type: 'blockquote_start'
- });
-
- cap = cap[0].replace(/^ *> ?/gm, '');
-
- // Pass `top` to keep the current
- // "toplevel" state. This is exactly
- // how markdown.pl works.
- this.token(cap, top, true);
-
- this.tokens.push({
- type: 'blockquote_end'
- });
-
- continue;
- }
-
- // list
- cap = this.rules.list.exec(src);
- if (cap) {
- src = src.substring(cap[0].length);
- const bull = cap[2];
-
- this.tokens.push({
- type: 'list_start',
- ordered: bull.length > 1
- });
-
- // Get each top-level item.
- cap = cap[0].match(this.rules.item);
-
- let next = false;
- const l = cap.length;
- let i = 0;
-
- for (; i < l; i++) {
- let item = cap[i];
-
- // Remove the list item's bullet
- // so it is seen as the next token.
- let space = item.length;
- item = item.replace(/^ *([*+-]|\d+\.) +/, '');
-
- // Outdent whatever the
- // list item contains. Hacky.
- if (~item.indexOf('\n ')) {
- space -= item.length;
- item = this.options.pedantic ?
- item.replace(/^ {1,4}/gm, '') :
- item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '');
- }
-
- // Determine whether the next list item belongs here.
- // Backpedal if it does not belong in this list.
- if (this.options.smartLists && i !== l - 1) {
- const b = this.rules.bullet.exec(cap[i + 1])[0];
- if (bull !== b && !(bull.length > 1 && b.length > 1)) {
- src = cap.slice(i + 1).join('\n') + src;
- i = l - 1;
- }
- }
-
- // Determine whether item is loose or not.
- // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
- // for discount behavior.
- let loose = next || (/\n\n(?!\s*$)/).test(item);
- if (i !== l - 1) {
- next = item.charAt(item.length - 1) === '\n';
- if (!loose) {
- loose = next;
- }
- }
-
- this.tokens.push({
- type: loose ?
- 'loose_item_start' :
- 'list_item_start'
- });
-
- // Recurse.
- this.token(item, false, bq);
-
- this.tokens.push({
- type: 'list_item_end'
- });
- }
-
- this.tokens.push({
- type: 'list_end'
- });
-
- continue;
- }
-
- // html
- cap = this.rules.html.exec(src);
- if (cap) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: this.options.sanitize ? 'paragraph' : 'html',
- pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
- text: cap[0]
- });
- continue;
- }
-
- // def
- cap = this.rules.def.exec(src);
- if ((!bq && top) && cap) {
- src = src.substring(cap[0].length);
- this.tokens.links[cap[1].toLowerCase()] = {
- href: cap[2],
- title: cap[3]
- };
- continue;
- }
-
- // table (gfm)
- cap = this.rules.table.exec(src);
- if (top && cap) {
- src = src.substring(cap[0].length);
-
- const item = {
- type: 'table',
- header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
- align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
- cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
- };
-
- for (let i = 0; i < item.align.length; i++) {
- if (/^ *-+: *$/.test(item.align[i])) {
- item.align[i] = 'right';
- } else if (/^ *:-+: *$/.test(item.align[i])) {
- item.align[i] = 'center';
- } else if (/^ *:-+ *$/.test(item.align[i])) {
- item.align[i] = 'left';
- } else {
- item.align[i] = null;
- }
- }
-
- for (let i = 0; i < item.cells.length; i++) {
- item.cells[i] = item.cells[i].replace(/^ *\| *| *\| *$/g, '').split(/ *\| */);
- }
-
- this.tokens.push(item);
-
- continue;
- }
-
- // top-level paragraph
- cap = this.rules.paragraph.exec(src);
- if (top && cap) {
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'paragraph',
- text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1]
- });
- continue;
- }
-
- // text
- cap = this.rules.text.exec(src);
- if (cap) {
- // Top-level should never reach here.
- src = src.substring(cap[0].length);
- this.tokens.push({
- type: 'text',
- text: cap[0]
- });
- continue;
- }
-
- if (src) {
- throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
- }
- }
-
- return this.tokens;
- }
-}
-
export function format(text, options) {
const markdownOptions = {
renderer: new MattermostMarkdownRenderer(null, options),
@@ -484,9 +149,7 @@ export function format(text, options) {
tables: true
};
- const tokens = new MattermostLexer(markdownOptions).lex(text);
-
- return new MattermostParser(markdownOptions).parse(tokens);
+ return marked(text, markdownOptions);
}
// Marked helper functions that should probably just be exported
diff --git a/webapp/utils/text_formatting.jsx b/webapp/utils/text_formatting.jsx
index 96b51d632..623fe0660 100644
--- a/webapp/utils/text_formatting.jsx
+++ b/webapp/utils/text_formatting.jsx
@@ -318,7 +318,7 @@ function parseSearchTerms(searchTerm) {
termString = termString.substring(captured[0].length);
// break the text up into words based on how the server splits them in SqlPostStore.SearchPosts and then discard empty terms
- terms.push(...captured[0].split(/[ <>+\-\(\)\~\@]/).filter((term) => !!term));
+ terms.push(...captured[0].split(/[ <>+\-\(\)~@]/).filter((term) => !!term));
continue;
}
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index 2f728226c..978f231df 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -34,38 +34,6 @@ export function cleanUpUrlable(input) {
return cleaned;
}
-export function isTestDomain() {
- if ((/^localhost/).test(window.location.hostname)) {
- return true;
- }
-
- if ((/^dockerhost/).test(window.location.hostname)) {
- return true;
- }
-
- if ((/^test/).test(window.location.hostname)) {
- return true;
- }
-
- if ((/^127.0./).test(window.location.hostname)) {
- return true;
- }
-
- if ((/^192.168./).test(window.location.hostname)) {
- return true;
- }
-
- if ((/^10./).test(window.location.hostname)) {
- return true;
- }
-
- if ((/^176./).test(window.location.hostname)) {
- return true;
- }
-
- return false;
-}
-
export function isChrome() {
if (navigator.userAgent.indexOf('Chrome') > -1) {
return true;
@@ -457,7 +425,7 @@ export function replaceHtmlEntities(text) {
};
var newtext = text;
for (var tag in tagsToReplace) {
- if ({}.hasOwnProperty.call(tagsToReplace, tag)) {
+ if (Reflect.apply({}.hasOwnProperty, this, [tagsToReplace, tag])) {
var regex = new RegExp(tag, 'g');
newtext = newtext.replace(regex, tagsToReplace[tag]);
}
@@ -473,7 +441,7 @@ export function insertHtmlEntities(text) {
};
var newtext = text;
for (var tag in tagsToReplace) {
- if ({}.hasOwnProperty.call(tagsToReplace, tag)) {
+ if (Reflect.apply({}.hasOwnProperty, this, [tagsToReplace, tag])) {
var regex = new RegExp(tag, 'g');
newtext = newtext.replace(regex, tagsToReplace[tag]);
}
@@ -682,7 +650,7 @@ export function applyTheme(theme) {
changeCss('.app__body .post-list__arrows', 'fill:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.app__body .sidebar--left, .app__body .sidebar--right .sidebar--right__header, .app__body .suggestion-list__content .command', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.app__body .app__content, .app__body .post-create__container .post-create-body .btn-file, .app__body .post-create__container .post-create-footer .msg-typing, .app__body .suggestion-list__content .command, .app__body .modal .modal-content, .app__body .dropdown-menu, .app__body .popover, .app__body .mentions__name, .app__body .tip-overlay', 'color:' + theme.centerChannelColor, 1);
- changeCss('.app__body #archive-link-home', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
+ changeCss('.app__body #archive-link-home, .video-div .video-thumbnail__error', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('.app__body #post-create', 'color:' + theme.centerChannelColor, 2);
changeCss('.app__body .mentions--top, .app__body .suggestion-list', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
changeCss('.app__body .mentions--top, .app__body .suggestion-list', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
@@ -734,7 +702,7 @@ export function applyTheme(theme) {
changeCss('.app__body .scrollbar--horizontal, .app__body .scrollbar--vertical', 'background:' + changeOpacity(theme.centerChannelColor, 0.5), 2);
changeCss('.app__body .post-list__new-messages-below', 'background:' + changeColor(theme.centerChannelColor, 0.5), 2);
changeCss('.app__body .post.post--comment .post__body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
- changeCss('.app__body .post.post--comment.current--user .post__body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.13), 1);
+ changeCss('.app__body .post.post--comment.current--user .post__body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
}
if (theme.newMessageSeparator) {
@@ -924,7 +892,7 @@ export function isValidUsername(name) {
error = 'This field is required';
} else if (name.length < Constants.MIN_USERNAME_LENGTH || name.length > Constants.MAX_USERNAME_LENGTH) {
error = 'Must be between ' + Constants.MIN_USERNAME_LENGTH + ' and ' + Constants.MAX_USERNAME_LENGTH + ' characters';
- } else if (!(/^[a-z0-9\.\-\_]+$/).test(name)) {
+ } else if (!(/^[a-z0-9\.\-_]+$/).test(name)) {
error = "Must contain only letters, numbers, and the symbols '.', '-', and '_'.";
} else if (!(/[a-z]/).test(name.charAt(0))) { //eslint-disable-line no-negated-condition
error = 'First character must be a letter.';
@@ -977,7 +945,7 @@ Image.prototype.load = function imageLoad(url, progressCallback) {
xmlHTTP.responseType = 'arraybuffer';
xmlHTTP.onload = function onLoad() {
var h = xmlHTTP.getAllResponseHeaders();
- var m = h.match(/^Content-Type\:\s*(.*?)$/mi);
+ var m = h.match(/^Content-Type:\s*(.*?)$/mi);
var mimeType = m[1] || 'image/png';
var blob = new Blob([this.response], {type: mimeType});
@@ -1142,7 +1110,7 @@ export function generateId() {
// implementation taken from http://stackoverflow.com/a/2117523
var id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
- id = id.replace(/[xy]/g, function replaceRandom(c) {
+ id = id.replace(/[xy]/g, (c) => {
var r = Math.floor(Math.random() * 16);
var v;
@@ -1408,6 +1376,10 @@ export function localizeMessage(id, defaultMessage) {
return id;
}
+export function mod(a, b) {
+ return ((a % b) + b) % b;
+}
+
export function getProfilePicSrcForPost(post, timestamp) {
let src = Client.getUsersRoute() + '/' + post.user_id + '/image?time=' + timestamp;
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
diff --git a/webapp/utils/web_client.jsx b/webapp/utils/web_client.jsx
index 642e523b7..b974ad31f 100644
--- a/webapp/utils/web_client.jsx
+++ b/webapp/utils/web_client.jsx
@@ -1,10 +1,12 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import Client from '../client/client.jsx';
+import Client from 'mattermost/client.jsx';
import TeamStore from '../stores/team_store.jsx';
import BrowserStore from '../stores/browser_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
+
+import request from 'superagent';
const HTTP_UNAUTHORIZED = 401;
@@ -53,7 +55,7 @@ class WebClientClass extends Client {
}
},
(err) => {
- this.track('api', 'api_users_login_fail', name, 'login_id', loginId);
+ this.track('api', 'api_users_login_fail', '', 'login_id', loginId);
if (error) {
error(err);
}
@@ -75,13 +77,24 @@ class WebClientClass extends Client {
}
},
(err) => {
- this.track('api', 'api_users_login_fail', name, 'login_id', loginId);
+ this.track('api', 'api_users_login_fail', '', 'login_id', loginId);
if (error) {
error(err);
}
}
);
}
+
+ getYoutubeVideoInfo(googleKey, videoId, success, error) {
+ request.get('https://www.googleapis.com/youtube/v3/videos').
+ query({part: 'snippet', id: videoId, key: googleKey}).
+ end((err, res) => {
+ if (err) {
+ return error(err);
+ }
+ return success(res.body);
+ });
+ }
}
var WebClient = new WebClientClass();
diff --git a/webapp/webpack.config-test.js b/webapp/webpack.config-test.js
deleted file mode 100644
index aaeefeb8c..000000000
--- a/webapp/webpack.config-test.js
+++ /dev/null
@@ -1,131 +0,0 @@
-const webpack = require('webpack');
-const path = require('path');
-const ExtractTextPlugin = require('extract-text-webpack-plugin');
-const CopyWebpackPlugin = require('copy-webpack-plugin');
-const nodeExternals = require('webpack-node-externals');
-
-const htmlExtract = new ExtractTextPlugin('html', 'root.html');
-
-const NPM_TARGET = process.env.npm_lifecycle_event; //eslint-disable-line no-process-env
-
-var DEV = true;
-var FULLMAP = false;
-if (NPM_TARGET === 'run' || NPM_TARGET === 'run-fullmap') {
- DEV = true;
- if (NPM_TARGET === 'run-fullmap') {
- FULLMAP = true;
- }
-}
-
-var config = {
- target: 'node',
- externals: [nodeExternals()],
- module: {
- loaders: [
- {
- test: /\.jsx?$/,
- loader: 'babel',
- exclude: /(node_modules|non_npm_dependencies)/,
- query: {
- presets: ['react', 'es2015-webpack', 'stage-0'],
- plugins: ['transform-runtime'],
- cacheDirectory: DEV
- }
- },
- {
- test: /\.json$/,
- loader: 'json'
- },
- {
- test: /(node_modules|non_npm_dependencies)\/.+\.(js|jsx)$/,
- loader: 'imports',
- query: {
- $: 'jquery',
- jQuery: 'jquery'
- }
- },
- {
- test: /\.scss$/,
- loaders: ['style', 'css', 'sass']
- },
- {
- test: /\.css$/,
- loaders: ['style', 'css']
- },
- {
- test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3|jpg)$/,
- loader: 'file',
- query: {
- name: 'files/[hash].[ext]'
- }
- },
- {
- test: /\.html$/,
- loader: htmlExtract.extract('html?attrs=link:href')
- }
- ]
- },
- sassLoader: {
- includePaths: ['node_modules/compass-mixins/lib']
- },
- plugins: [
- new webpack.ProvidePlugin({
- 'window.jQuery': 'jquery'
- }),
- htmlExtract,
- new CopyWebpackPlugin([
- {from: 'images/emoji', to: 'emoji'}
- ]),
- new webpack.LoaderOptionsPlugin({
- minimize: !DEV,
- debug: false
- })
- ],
- resolve: {
- alias: {
- jquery: 'jquery/dist/jquery'
- },
- modules: [
- 'node_modules',
- 'non_npm_dependencies',
- path.resolve(__dirname)
- ]
- }
-};
-
-// Development mode configuration
-if (DEV) {
- if (FULLMAP) {
- config.devtool = 'source-map';
- } else {
- config.devtool = 'eval-cheap-module-source-map';
- }
-}
-
-// Production mode configuration
-if (!DEV) {
- config.devtool = 'source-map';
- config.plugins.push(
- new webpack.optimize.UglifyJsPlugin({
- 'screw-ie8': true,
- mangle: {
- toplevel: false
- },
- compress: {
- warnings: false
- },
- comments: false
- })
- );
- config.plugins.push(
- new webpack.optimize.AggressiveMergingPlugin()
- );
- config.plugins.push(
- new webpack.optimize.OccurrenceOrderPlugin(true)
- );
- config.plugins.push(
- new webpack.optimize.DedupePlugin()
- );
-}
-
-module.exports = config;
diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js
index 6471731eb..191518a07 100644
--- a/webapp/webpack.config.js
+++ b/webapp/webpack.config.js
@@ -2,6 +2,7 @@ const webpack = require('webpack');
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
+const nodeExternals = require('webpack-node-externals');
const htmlExtract = new ExtractTextPlugin('html', 'root.html');
@@ -9,6 +10,7 @@ const NPM_TARGET = process.env.npm_lifecycle_event; //eslint-disable-line no-pro
var DEV = false;
var FULLMAP = false;
+var TEST = false;
if (NPM_TARGET === 'run' || NPM_TARGET === 'run-fullmap') {
DEV = true;
if (NPM_TARGET === 'run-fullmap') {
@@ -16,6 +18,11 @@ if (NPM_TARGET === 'run' || NPM_TARGET === 'run-fullmap') {
}
}
+if (NPM_TARGET === 'test') {
+ DEV = false;
+ TEST = true;
+}
+
var config = {
entry: ['babel-polyfill', './root.jsx', 'root.html'],
output: {
@@ -36,6 +43,15 @@ var config = {
}
},
{
+ test: /node_modules\/mattermost\/client\.jsx?$/,
+ loader: 'babel',
+ query: {
+ presets: ['react', 'es2015-webpack', 'stage-0'],
+ plugins: ['transform-runtime'],
+ cacheDirectory: DEV
+ }
+ },
+ {
test: /\.json$/,
loader: 'json'
},
@@ -139,4 +155,9 @@ if (!DEV) {
);
}
+// Test mode configuration
+if (TEST) {
+ config.externals = [nodeExternals()];
+}
+
module.exports = config;