From 57fcb79d71a88dc8bd97794ec773ee923a1bb637 Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Fri, 30 Sep 2022 19:55:23 +0530 Subject: [PATCH 01/54] fix: Article slug auto saves (#5524) - Auto save article slug --- app/models/article.rb | 8 +++++++- .../20220930025317_add_unique_index_to_slug.rb | 6 ++++++ db/schema.rb | 4 ++-- spec/factories/articles.rb | 3 +-- spec/models/article_spec.rb | 14 +++++++++++++- 5 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20220930025317_add_unique_index_to_slug.rb diff --git a/app/models/article.rb b/app/models/article.rb index f7318c497..ac6506d01 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -23,7 +23,7 @@ # # index_articles_on_associated_article_id (associated_article_id) # index_articles_on_author_id (author_id) -# index_articles_on_slug (slug) +# index_articles_on_slug (slug) UNIQUE # class Article < ApplicationRecord include PgSearch::Model @@ -45,6 +45,8 @@ class Article < ApplicationRecord belongs_to :author, class_name: 'User' before_validation :ensure_account_id + before_validation :ensure_article_slug + validates :account_id, presence: true validates :category_id, presence: true validates :author_id, presence: true @@ -112,4 +114,8 @@ class Article < ApplicationRecord def ensure_account_id self.account_id = portal&.account_id end + + def ensure_article_slug + self.slug ||= "#{Time.now.utc.to_i}-#{title.underscore.parameterize(separator: '-')}" if title.present? + end end diff --git a/db/migrate/20220930025317_add_unique_index_to_slug.rb b/db/migrate/20220930025317_add_unique_index_to_slug.rb new file mode 100644 index 000000000..e35f6289c --- /dev/null +++ b/db/migrate/20220930025317_add_unique_index_to_slug.rb @@ -0,0 +1,6 @@ +class AddUniqueIndexToSlug < ActiveRecord::Migration[6.1] + def change + remove_index :articles, :slug + add_index :articles, :slug, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index ad5c8241a..0793a6587 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_09_26_164441) do +ActiveRecord::Schema.define(version: 2022_09_30_025317) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -134,7 +134,7 @@ ActiveRecord::Schema.define(version: 2022_09_26_164441) do t.string "slug", null: false t.index ["associated_article_id"], name: "index_articles_on_associated_article_id" t.index ["author_id"], name: "index_articles_on_author_id" - t.index ["slug"], name: "index_articles_on_slug" + t.index ["slug"], name: "index_articles_on_slug", unique: true end create_table "attachments", id: :serial, force: :cascade do |t| diff --git a/spec/factories/articles.rb b/spec/factories/articles.rb index f820ddd6c..30c842621 100644 --- a/spec/factories/articles.rb +++ b/spec/factories/articles.rb @@ -3,8 +3,7 @@ FactoryBot.define do account_id { 1 } category_id { 1 } author_id { 1 } - title { 'MyString' } - slug { 'MyString' } + title { Faker::Movie.title } content { 'MyText' } description { 'MyDescrption' } status { 1 } diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 3fffa63d6..2e0f957dc 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -119,11 +119,23 @@ RSpec.describe Article, type: :model do records = portal_1.articles.search(params) expect(records.count).to eq(2) end + + it 'auto saves article slug' do + article = create(:article, category_id: category_1.id, title: 'the awesome article 1', content: 'This is the content', portal_id: portal_1.id, + author_id: user.id) + expect(article.slug).to include('the-awesome-article-1') + end end context 'with pagination' do it 'returns paginated articles' do - create_list(:article, 30, category_id: category_2.id, slug: 'title-1', title: 'title 1', portal_id: portal_2.id, author_id: user.id) + build_list(:article, 30) do |record, i| + record.category_id = category_2.id + record.title = "title #{i}" + record.portal_id = portal_2.id + record.author_id = user.id + record.save! + end params = { category_slug: 'category_2' } records = portal_2.articles.search(params) expect(records.count).to eq(25) From 4f0360c7a2ac455a671a688ea897319a73e246b2 Mon Sep 17 00:00:00 2001 From: Jordan Brough Date: Fri, 30 Sep 2022 11:28:18 -0700 Subject: [PATCH 02/54] chore: Allow setting "users.display_name" in Platform API (#5532) --- .../platform/api/v1/users_controller.rb | 2 +- .../platform/api/v1/users_controller_spec.rb | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb index 12c87deb5..2c8995f81 100644 --- a/app/controllers/platform/api/v1/users_controller.rb +++ b/app/controllers/platform/api/v1/users_controller.rb @@ -51,6 +51,6 @@ class Platform::Api::V1::UsersController < PlatformController end def user_params - params.permit(:name, :email, :password, custom_attributes: {}) + params.permit(:name, :display_name, :email, :password, custom_attributes: {}) end end diff --git a/spec/controllers/platform/api/v1/users_controller_spec.rb b/spec/controllers/platform/api/v1/users_controller_spec.rb index 57fa81bd4..ca3cd3bf9 100644 --- a/spec/controllers/platform/api/v1/users_controller_spec.rb +++ b/spec/controllers/platform/api/v1/users_controller_spec.rb @@ -96,15 +96,24 @@ RSpec.describe 'Platform Users API', type: :request do it 'creates a new user and permissible for the user' do expect do - post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'Password1!', + post '/platform/api/v1/users/', params: { name: 'test', display_name: 'displaytest', + email: 'test@test.com', password: 'Password1!', custom_attributes: { test: 'test_create' } }, headers: { api_access_token: platform_app.access_token.token }, as: :json end.not_to enqueue_mail expect(response).to have_http_status(:success) data = JSON.parse(response.body) - expect(data['email']).to eq('test@test.com') - expect(data['custom_attributes']['test']).to eq('test_create') + expect(data).to match( + hash_including( + 'name' => 'test', + 'display_name' => 'displaytest', + 'email' => 'test@test.com', + 'custom_attributes' => { + 'test' => 'test_create' + } + ) + ) expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id'] end From 7b54990ae641ad8199239b76a06166d7c1e6230d Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Sat, 1 Oct 2022 00:03:00 +0530 Subject: [PATCH 03/54] fix: Updated IMAP errors add method (#5520) fixes: #5519 --- app/models/message.rb | 2 +- spec/models/message_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/models/message.rb b/app/models/message.rb index a2382ae3a..2930786f9 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -241,7 +241,7 @@ class Message < ApplicationRecord end def validate_attachments_limit(_attachment) - errors.add(attachments: 'exceeded maximum allowed') if attachments.size >= NUMBER_OF_PERMITTED_ATTACHMENTS + errors.add(:attachments, message: 'exceeded maximum allowed') if attachments.size >= NUMBER_OF_PERMITTED_ATTACHMENTS end def set_conversation_activity diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb index e74a1129d..4940d704a 100644 --- a/spec/models/message_spec.rb +++ b/spec/models/message_spec.rb @@ -106,6 +106,19 @@ RSpec.describe Message, type: :model do end end + context 'when attachments size maximum' do + let(:message) { build(:message, content_type: nil, account: create(:account)) } + + it 'add errors to message for attachment size is more than allowed limit' do + 16.times.each do + attachment = message.attachments.new(account_id: message.account_id, file_type: :image) + attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png') + end + + expect(message.errors.messages).to eq({ attachments: ['exceeded maximum allowed'] }) + end + end + context 'when email notifiable message' do let(:message) { build(:message, content_type: nil, account: create(:account)) } From 705d06ac3c0237e0b07f56bfba676ee8b7f2d83c Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Sat, 1 Oct 2022 03:33:33 +0530 Subject: [PATCH 04/54] fix: Avoid editor formatting issues when a canned response is edited (#5533) --- .../components/widgets/WootWriter/Editor.vue | 132 ++++++++++-------- .../widgets/conversation/ReplyBox.vue | 8 ++ .../dashboard/helper/localStorage.js | 1 + .../helpcenter/components/ArticleEditor.vue | 1 + 4 files changed, 85 insertions(+), 57 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index a21658a8a..92023444c 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -28,7 +28,7 @@ import { suggestionsPlugin, triggerCharacters, } from '@chatwoot/prosemirror-schema/src/mentions/plugin'; -import { EditorState } from 'prosemirror-state'; +import { EditorState, Selection } from 'prosemirror-state'; import { defaultMarkdownParser } from 'prosemirror-markdown'; import { wootWriterSetup } from '@chatwoot/prosemirror-schema'; @@ -61,23 +61,28 @@ export default { mixins: [eventListenerMixins], props: { value: { type: String, default: '' }, + editorId: { type: String, default: '' }, placeholder: { type: String, default: '' }, isPrivate: { type: Boolean, default: false }, - isFormatMode: { type: Boolean, default: false }, enableSuggestions: { type: Boolean, default: true }, }, data() { return { - lastValue: null, showUserMentions: false, showCannedMenu: false, mentionSearchKey: '', cannedSearchTerm: '', editorView: null, range: null, + state: undefined, }; }, computed: { + contentFromEditor() { + return addMentionsToMarkdownSerializer( + defaultMarkdownSerializer + ).serialize(this.editorView.state.doc); + }, plugins() { if (!this.enableSuggestions) { return []; @@ -102,7 +107,6 @@ export default { onExit: () => { this.mentionSearchKey = ''; this.showUserMentions = false; - this.editorView = null; return false; }, onKeyDown: ({ event }) => { @@ -131,7 +135,6 @@ export default { onExit: () => { this.cannedSearchTerm = ''; this.showCannedMenu = false; - this.editorView = null; return false; }, onKeyDown: ({ event }) => { @@ -149,54 +152,57 @@ export default { this.$emit('toggle-canned-menu', !this.isPrivate && updatedValue); }, value(newValue = '') { - if (newValue !== this.lastValue) { - const { tr } = this.state; - if (this.isFormatMode) { - this.state = createState( - newValue, - this.placeholder, - this.plugins, - this.isFormatMode - ); - } else { - tr.insertText(newValue, 0, tr.doc.content.size); - this.state = this.view.state.apply(tr); - } - this.view.updateState(this.state); + if (newValue !== this.contentFromEditor) { + this.reloadState(); } }, + editorId() { + this.reloadState(); + }, + isPrivate() { + this.reloadState(); + }, }, created() { this.state = createState(this.value, this.placeholder, this.plugins); }, mounted() { - this.view = new EditorView(this.$refs.editor, { - state: this.state, - dispatchTransaction: tx => { - this.state = this.state.apply(tx); - this.emitOnChange(); - }, - handleDOMEvents: { - keyup: () => { - this.onKeyup(); - }, - focus: () => { - this.onFocus(); - }, - blur: () => { - this.onBlur(); - }, - paste: (view, event) => { - const data = event.clipboardData.files; - if (data.length > 0) { - event.preventDefault(); - } - }, - }, - }); + this.createEditorView(); + this.editorView.updateState(this.state); this.focusEditorInputField(); }, methods: { + reloadState() { + this.state = createState(this.value, this.placeholder, this.plugins); + this.editorView.updateState(this.state); + this.focusEditorInputField(); + }, + createEditorView() { + this.editorView = new EditorView(this.$refs.editor, { + state: this.state, + dispatchTransaction: tx => { + this.state = this.state.apply(tx); + this.emitOnChange(); + }, + handleDOMEvents: { + keyup: () => { + this.onKeyup(); + }, + focus: () => { + this.onFocus(); + }, + blur: () => { + this.onBlur(); + }, + paste: (view, event) => { + const data = event.clipboardData.files; + if (data.length > 0) { + event.preventDefault(); + } + }, + }, + }); + }, handleKeyEvents(e) { if (hasPressedAltAndPKey(e)) { this.focusEditorInputField(); @@ -206,47 +212,59 @@ export default { } }, focusEditorInputField() { - this.$refs.editor.querySelector('div.ProseMirror-woot-style').focus(); + const { tr } = this.editorView.state; + const selection = Selection.atEnd(tr.doc); + + this.editorView.dispatch(tr.setSelection(selection)); + this.editorView.focus(); }, insertMentionNode(mentionItem) { - if (!this.view) { + if (!this.editorView) { return null; } - const node = this.view.state.schema.nodes.mention.create({ + const node = this.editorView.state.schema.nodes.mention.create({ userId: mentionItem.key, userFullName: mentionItem.label, }); - const tr = this.view.state.tr.replaceWith( + const tr = this.editorView.state.tr.replaceWith( this.range.from, this.range.to, node ); - this.state = this.view.state.apply(tr); + this.state = this.editorView.state.apply(tr); return this.emitOnChange(); }, insertCannedResponse(cannedItem) { - if (!this.view) { + if (!this.editorView) { return null; } - const tr = this.view.state.tr.insertText( + const tr = this.editorView.state.tr.insertText( cannedItem, this.range.from, this.range.to ); - this.state = this.view.state.apply(tr); - return this.emitOnChange(); + this.state = this.editorView.state.apply(tr); + this.emitOnChange(); + + // Hacky fix for #5501 + this.state = createState( + this.contentFromEditor, + this.placeholder, + this.plugins + ); + this.editorView.updateState(this.state); + return false; }, emitOnChange() { - this.view.updateState(this.state); - this.lastValue = addMentionsToMarkdownSerializer( - defaultMarkdownSerializer - ).serialize(this.state.doc); - this.$emit('input', this.lastValue); + this.editorView.updateState(this.state); + + this.$emit('input', this.contentFromEditor); }, + hideMentions() { this.showUserMentions = false; }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index d24b31d30..53a793e48 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -56,6 +56,7 @@ {}, }; }, mounted() { From 9ea43a2678664e6ab550a7877ea2964c90fb40b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Kube=C5=A1?= <46596180+KubesDavid@users.noreply.github.com> Date: Mon, 3 Oct 2022 14:13:50 +0200 Subject: [PATCH 05/54] chore: Improve Nginx settings for speed and security (#5144) * fix: Fixes #5138 * Move to helper function * Improve Nginx settings * chore: set ssl_prefer_server_ciphers to off ssl_prefer_server_ciphers should be set to `off` in a modern context. ref: https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=modern&openssl=1.1.1k&guideline=5.6 Co-authored-by: Vishnu Narayanan --- deployment/nginx_chatwoot.conf | 54 ++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/deployment/nginx_chatwoot.conf b/deployment/nginx_chatwoot.conf index 8359431c9..95a5b145a 100644 --- a/deployment/nginx_chatwoot.conf +++ b/deployment/nginx_chatwoot.conf @@ -1,3 +1,14 @@ +upstream backend { + zone upstreams 64K; + server 127.0.0.1:3000; + keepalive 32; +} + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + server { listen 80; listen [::]:80; @@ -6,12 +17,12 @@ server { access_log /var/log/nginx/chatwoot_access_80.log; error_log /var/log/nginx/chatwoot_error_80.log; - return 301 https://chatwoot.domain.com/; + return 301 https://chatwoot.domain.com$request_uri; } server { - listen 443 ssl http2; - listen [::]:443 ssl http2; + listen 443 ssl http2 reuseport; + listen [::]:443 ssl http2 reuseport; server_name chatwoot.domain.com www.chatwoot.domain.com; underscores_in_headers on; @@ -20,28 +31,33 @@ server { error_log /var/log/nginx/chatwoot_error_443.log; location / { - proxy_pass_header Authorization; - proxy_pass http://127.0.0.1:3000; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Ssl on; # Optional - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_buffering off; - client_max_body_size 0; - proxy_read_timeout 36000s; - proxy_redirect off; + proxy_pass http://backend; + proxy_redirect off; + + proxy_pass_header Authorization; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Ssl on; # Optional + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + client_max_body_size 0; + proxy_read_timeout 36000s; } ssl_certificate /etc/letsencrypt/live/chatwoot.domain.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/chatwoot.domain.com/privkey.pem; # managed by Certbot - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_dhparam /etc/ssl/dhparam; + ssl_early_data on; + ssl_buffer_size 4k; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; } From beedfc47bfde31545ab67cfa7c0c8d4007ad040f Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 4 Oct 2022 03:57:34 +0530 Subject: [PATCH 06/54] feat: Allow users to select `Cmd+Enter` as a hotkey (#4401) Co-authored-by: Pranav Raj S --- .../dashboard/components/ui/PreviewCard.vue | 113 ++++++++++++++++++ .../widgets/WootWriter/ReplyBottomPanel.vue | 32 ----- .../widgets/conversation/ReplyBox.vue | 100 ++++++++-------- .../i18n/locale/en/conversation.json | 1 - .../dashboard/i18n/locale/en/settings.json | 15 +++ .../dashboard/mixins/specs/uiSettings.spec.js | 24 ++++ app/javascript/dashboard/mixins/uiSettings.js | 19 ++- .../dashboard/settings/profile/Index.vue | 77 +++++++++++- .../settings/profile/MessageSignature.vue | 2 +- .../settings/profile/NotificationSettings.vue | 6 +- .../shared/helpers/KeyboardHelpers.js | 26 ++++ .../helpers/specs/KeyboardHelpers.spec.js | 22 +++- .../images/dashboard/editor/cmd-editor.png | Bin 0 -> 149362 bytes .../images/dashboard/editor/enter-editor.png | Bin 0 -> 147829 bytes 14 files changed, 344 insertions(+), 93 deletions(-) create mode 100644 app/javascript/dashboard/components/ui/PreviewCard.vue create mode 100644 public/assets/images/dashboard/editor/cmd-editor.png create mode 100644 public/assets/images/dashboard/editor/enter-editor.png diff --git a/app/javascript/dashboard/components/ui/PreviewCard.vue b/app/javascript/dashboard/components/ui/PreviewCard.vue new file mode 100644 index 000000000..751a070a5 --- /dev/null +++ b/app/javascript/dashboard/components/ui/PreviewCard.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index 74cd9de4d..9ace1ceb2 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -91,17 +91,6 @@
-
- - -
{ }); }); }); + +describe('isEditorHotKeyEnabled', () => { + it('returns true if hot key is not configured and enter to send flag is true', () => { + expect( + isEditorHotKeyEnabled({ enter_to_send_enabled: true }, 'enter') + ).toEqual(true); + expect( + isEditorHotKeyEnabled({ enter_to_send_enabled: true }, 'cmd_enter') + ).toEqual(false); + + expect(isEditorHotKeyEnabled({}, 'cmd_enter')).toEqual(true); + expect(isEditorHotKeyEnabled({}, 'enter')).toEqual(false); + }); + + it('returns correct value if hot key is configured', () => { + expect( + isEditorHotKeyEnabled({ editor_message_key: 'enter' }, 'enter') + ).toEqual(true); + expect( + isEditorHotKeyEnabled({ editor_message_key: 'cmd_enter' }, 'enter') + ).toEqual(false); + }); +}); diff --git a/app/javascript/dashboard/mixins/uiSettings.js b/app/javascript/dashboard/mixins/uiSettings.js index e265975e1..adc407eb3 100644 --- a/app/javascript/dashboard/mixins/uiSettings.js +++ b/app/javascript/dashboard/mixins/uiSettings.js @@ -10,11 +10,24 @@ export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = [ { name: 'contact_labels' }, { name: 'previous_conversation' }, ]; + +export const isEditorHotKeyEnabled = (uiSettings, key) => { + const { + editor_message_key: editorMessageKey, + enter_to_send_enabled: enterToSendEnabled, + } = uiSettings || {}; + if (!editorMessageKey) { + if (enterToSendEnabled) { + return key === 'enter'; + } + return key === 'cmd_enter'; + } + return editorMessageKey === key; +}; + export default { computed: { - ...mapGetters({ - uiSettings: 'getUISettings', - }), + ...mapGetters({ uiSettings: 'getUISettings' }), conversationSidebarItemsOrder() { const { conversation_sidebar_items_order: itemsOrder } = this.uiSettings; return itemsOrder || DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER; diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue index 53152f09d..23b5425e2 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue @@ -70,6 +70,31 @@
+
+
+

+ {{ $t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE') }} +

+

+ {{ $t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE') }} +

+
+
+ +
+
@@ -102,14 +127,19 @@ import alertMixin from 'shared/mixins/alertMixin'; import ChangePassword from './ChangePassword'; import MessageSignature from './MessageSignature'; import globalConfigMixin from 'shared/mixins/globalConfigMixin'; +import uiSettingsMixin, { + isEditorHotKeyEnabled, +} from 'dashboard/mixins/uiSettings'; +import PreviewCard from 'dashboard/components/ui/PreviewCard.vue'; export default { components: { NotificationSettings, ChangePassword, MessageSignature, + PreviewCard, }, - mixins: [alertMixin, globalConfigMixin], + mixins: [alertMixin, globalConfigMixin, uiSettingsMixin], data() { return { avatarFile: '', @@ -119,6 +149,28 @@ export default { email: '', isProfileUpdating: false, errorMessage: '', + keyOptions: [ + { + key: 'enter', + src: '/assets/images/dashboard/editor/enter-editor.png', + heading: this.$t( + 'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.HEADING' + ), + content: this.$t( + 'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.CONTENT' + ), + }, + { + key: 'cmd_enter', + src: '/assets/images/dashboard/editor/cmd-editor.png', + heading: this.$t( + 'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.HEADING' + ), + content: this.$t( + 'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.CONTENT' + ), + }, + ], }; }, validations: { @@ -158,6 +210,7 @@ export default { this.avatarUrl = this.currentUser.avatar_url; this.displayName = this.currentUser.display_name; }, + isEditorHotKeyEnabled, async updateUser() { this.$v.$touch(); if (this.$v.$invalid) { @@ -207,6 +260,12 @@ export default { showDeleteButton() { return this.avatarUrl && !this.avatarUrl.includes('www.gravatar.com'); }, + toggleEditorMessageKey(key) { + this.updateUISettings({ editor_message_key: key }); + this.showAlert( + this.$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.UPDATE_SUCCESS') + ); + }, }, }; @@ -216,18 +275,32 @@ export default { @import '~dashboard/assets/scss/mixins.scss'; .profile--settings { - padding: 24px; overflow: auto; + padding: 24px; } .profile--settings--row { @include border-normal-bottom; + align-items: center; + display: flex; padding: $space-normal; + .small-3 { padding: $space-normal $space-medium $space-normal 0; } + .small-9 { padding: $space-normal; } + + .card-preview { + display: flex; + flex-direction: row; + + .preview-button { + cursor: pointer; + margin-right: var(--space-normal); + } + } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue index e08e4fdee..557f239dc 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue @@ -77,7 +77,7 @@ export default { methods: { initValues() { const { message_signature: messageSignature } = this.currentUser; - this.messageSignature = messageSignature; + this.messageSignature = messageSignature || ''; }, async updateSignature() { this.$v.$touch(); diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue index bbf92b43d..60584d726 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue @@ -154,7 +154,7 @@ ) }}

-
+
Date: Wed, 5 Oct 2022 13:37:49 +0200 Subject: [PATCH 09/54] feat: Enable Docker Buildx multi-arch builds with arm64 support(#5545) Fixes #2575 multi-arch images with arm64 support CE edition images Co-authored-by: Vishnu Narayanan --- .github/workflows/publish_foss_docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml index 37f0f3e6e..aa1b15df2 100644 --- a/.github/workflows/publish_foss_docker.yml +++ b/.github/workflows/publish_foss_docker.yml @@ -58,5 +58,6 @@ jobs: with: context: . file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 push: true tags: ${{ env.DOCKER_TAG }} From 8b0e95ece8ed77e38e8e3537d8ef45214a9bcf48 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 5 Oct 2022 10:59:31 -0700 Subject: [PATCH 10/54] fix: Flakiness in CI pipeline (#5562) - Fixing the recent flakiness in CI pipelines --- .circleci/config.yml | 9 ++++++--- Gemfile | 1 + Gemfile.lock | 5 ++++- spec/factories/articles.rb | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a9e0bd450..fbb4fea90 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,6 +19,7 @@ defaults: &defaults - COVERAGE: true - LOG_LEVEL: warn parallelism: 4 + resource_class: large jobs: build: @@ -122,9 +123,11 @@ jobs: mkdir -p coverage ~/tmp/cc-test-reporter before-build TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) - bundle exec rspec --profile 10 \ - --out test-results/rspec/rspec.xml \ + bundle exec rspec --format progress \ + --format RspecJunitFormatter \ + --out ~/tmp/test-results/rspec.xml \ -- ${TESTFILES} + no_output_timeout: 30m - run: name: Code Climate Test Coverage command: | @@ -137,7 +140,7 @@ jobs: ~/tmp/cc-test-reporter before-build TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings) yarn test:coverage --profile 10 \ - --out test-results/frontend_specs/rspec.xml \ + --out ~/tmp/test-results/yarn.xml \ -- ${TESTFILES} - run: name: Code Climate Test Coverage diff --git a/Gemfile b/Gemfile index ca6c7856d..d47b5e449 100644 --- a/Gemfile +++ b/Gemfile @@ -174,6 +174,7 @@ group :development, :test do gem 'listen' gem 'mock_redis' gem 'pry-rails' + gem 'rspec_junit_formatter' gem 'rspec-rails', '~> 5.0.0' gem 'rubocop', require: false gem 'rubocop-performance', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 5cf4f020d..e8095a5e5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -536,6 +536,8 @@ GEM rspec-mocks (~> 3.10) rspec-support (~> 3.10) rspec-support (3.11.0) + rspec_junit_formatter (0.6.0) + rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.31.2) json (~> 2.3) parallel (~> 1.10) @@ -769,6 +771,7 @@ DEPENDENCIES responders rest-client rspec-rails (~> 5.0.0) + rspec_junit_formatter rubocop rubocop-performance rubocop-rails @@ -805,4 +808,4 @@ RUBY VERSION ruby 3.0.4p208 BUNDLED WITH - 2.3.17 + 2.3.18 diff --git a/spec/factories/articles.rb b/spec/factories/articles.rb index 30c842621..ec9c4cd2d 100644 --- a/spec/factories/articles.rb +++ b/spec/factories/articles.rb @@ -3,7 +3,7 @@ FactoryBot.define do account_id { 1 } category_id { 1 } author_id { 1 } - title { Faker::Movie.title } + title { "#{Faker::Movie.title} #{SecureRandom.hex}" } content { 'MyText' } description { 'MyDescrption' } status { 1 } From cd4c1ef27ed7f98774444267122d27def1d3b6cf Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Wed, 5 Oct 2022 14:18:16 -0700 Subject: [PATCH 11/54] feat: Update the design of mentions with thumbnail (#5551) --- .../components/widgets/WootWriter/Editor.vue | 7 +- .../widgets/conversation/TagAgents.vue | 161 +++++++++++++++--- .../widgets/mentions/MentionBox.vue | 45 +---- .../mentions/mentionSelectionKeyboardMixin.js | 39 +++++ .../mentionSelectionKeyboardMixin.spec.js | 64 +++++++ 5 files changed, 251 insertions(+), 65 deletions(-) create mode 100644 app/javascript/dashboard/components/widgets/mentions/mentionSelectionKeyboardMixin.js create mode 100644 app/javascript/dashboard/components/widgets/mentions/specs/mentionSelectionKeyboardMixin.spec.js diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index 92023444c..ee3337cf9 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -6,7 +6,7 @@ @click="insertMentionNode" /> @@ -223,8 +223,8 @@ export default { return null; } const node = this.editorView.state.schema.nodes.mention.create({ - userId: mentionItem.key, - userFullName: mentionItem.label, + userId: mentionItem.id, + userFullName: mentionItem.name, }); const tr = this.editorView.state.tr.replaceWith( @@ -256,6 +256,7 @@ export default { this.plugins ); this.editorView.updateState(this.state); + this.focusEditorInputField(); return false; }, diff --git a/app/javascript/dashboard/components/widgets/conversation/TagAgents.vue b/app/javascript/dashboard/components/widgets/conversation/TagAgents.vue index 73002eb5c..8cfe1033b 100644 --- a/app/javascript/dashboard/components/widgets/conversation/TagAgents.vue +++ b/app/javascript/dashboard/components/widgets/conversation/TagAgents.vue @@ -1,49 +1,160 @@ + + diff --git a/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue b/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue index e5963d0ee..4613d8357 100644 --- a/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue +++ b/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue @@ -20,7 +20,9 @@ + diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js index 04bd4104e..93dc40f28 100644 --- a/app/javascript/dashboard/helper/URLHelper.js +++ b/app/javascript/dashboard/helper/URLHelper.js @@ -6,7 +6,7 @@ export const frontendURL = (path, params) => { }; const getSSOAccountPath = ({ ssoAccountId, user }) => { - const { accounts = [] } = user || {}; + const { accounts = [], account_id = null } = user || {}; const ssoAccount = accounts.find( account => account.id === Number(ssoAccountId) ); @@ -14,7 +14,9 @@ const getSSOAccountPath = ({ ssoAccountId, user }) => { if (ssoAccount) { accountPath = `accounts/${ssoAccountId}`; } else if (accounts.length) { - accountPath = `accounts/${accounts[0].id}`; + // If the account id is not found, redirect to the first account + const accountId = account_id || accounts[0].id; + accountPath = `accounts/${accountId}`; } return accountPath; }; diff --git a/app/javascript/dashboard/store/modules/auth.js b/app/javascript/dashboard/store/modules/auth.js index 16bd0e4bb..17806959b 100644 --- a/app/javascript/dashboard/store/modules/auth.js +++ b/app/javascript/dashboard/store/modules/auth.js @@ -179,6 +179,14 @@ export const actions = { commit(types.SET_CURRENT_USER_AVAILABILITY, data[$state.currentUser.id]); } }, + + setActiveAccount: async (_, { accountId }) => { + try { + await authAPI.setActiveAccount({ accountId }); + } catch (error) { + // Ignore error + } + }, }; // mutations diff --git a/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js b/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js index f6211f324..6c1c85996 100644 --- a/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js @@ -165,4 +165,15 @@ describe('#actions', () => { expect(commit.mock.calls).toEqual([]); }); }); + + describe('#setActiveAccount', () => { + it('sends correct mutations if account id is available', async () => { + actions.setActiveAccount( + { + commit, + }, + { accountId: 1 } + ); + }); + }); }); diff --git a/config/routes.rb b/config/routes.rb index 903c5efc8..8b9b43863 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -182,6 +182,7 @@ Rails.application.routes.draw do delete :avatar, on: :collection member do post :availability + put :set_active_account end end diff --git a/spec/controllers/api/v1/profiles_controller_spec.rb b/spec/controllers/api/v1/profiles_controller_spec.rb index 0e386752f..8c331d932 100644 --- a/spec/controllers/api/v1/profiles_controller_spec.rb +++ b/spec/controllers/api/v1/profiles_controller_spec.rb @@ -195,4 +195,27 @@ RSpec.describe 'Profile API', type: :request do end end end + + describe 'PUT /api/v1/profile/set_active_account' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + put '/api/v1/profile/set_active_account' + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) } + + it 'updates the last active account id' do + put '/api/v1/profile/set_active_account', + params: { profile: { account_id: account.id } }, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + end + end + end end From 0a9ea6e272f3cfde03976aed57e9dd3af1386ee7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Oct 2022 17:32:00 -0700 Subject: [PATCH 14/54] chore(deps): bump google-protobuf from 3.21.2 to 3.21.7 (#5550) Bumps [google-protobuf](https://github.com/protocolbuffers/protobuf) from 3.21.2 to 3.21.7. - [Release notes](https://github.com/protocolbuffers/protobuf/releases) - [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/generate_changelog.py) - [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.21.2...v3.21.7) --- updated-dependencies: - dependency-name: google-protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: Sojan Jose --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e8095a5e5..5b14d5b5e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -286,9 +286,9 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - google-protobuf (3.21.2) - google-protobuf (3.21.2-x86_64-darwin) - google-protobuf (3.21.2-x86_64-linux) + google-protobuf (3.21.7) + google-protobuf (3.21.7-x86_64-darwin) + google-protobuf (3.21.7-x86_64-linux) googleapis-common-protos (1.3.12) google-protobuf (~> 3.14) googleapis-common-protos-types (~> 1.2) From 788b766179a9b30d1dd0a1b547a3d658cd54cf92 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Wed, 5 Oct 2022 22:00:15 -0700 Subject: [PATCH 15/54] feat: Quickly create canned responses (#5563) --- .../widgets/conversation/Message.vue | 9 +-- .../i18n/locale/en/conversation.json | 3 +- .../components/MessageContextMenu.vue | 69 ++++++++++++++++--- .../dashboard/settings/canned/AddCanned.vue | 9 +-- .../FluentIcon/dashboard-icons.json | 3 +- 5 files changed, 71 insertions(+), 22 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 5051a54fa..3481125ca 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -100,10 +100,11 @@ v-if="isBubble && !isMessageDeleted" :is-open="showContextMenu" :show-copy="hasText" + :show-canned-response-option="isOutgoing" :menu-position="contextMenuPosition" + :message-content="data.content" @toggle="handleContextMenuClick" @delete="handleDelete" - @copy="handleCopy" />
@@ -126,7 +127,6 @@ import alertMixin from 'shared/mixins/alertMixin'; import contentTypeMixin from 'shared/mixins/contentTypeMixin'; import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages'; import { generateBotMessageContent } from './helpers/botMessageContentHelper'; -import { copyTextToClipboard } from 'shared/helpers/clipboard'; export default { components: { @@ -408,11 +408,6 @@ export default { this.showAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE')); } }, - async handleCopy() { - await copyTextToClipboard(this.data.content); - this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL')); - this.showContextMenu = false; - }, async retrySendMessage() { await this.$store.dispatch('sendMessageWithData', this.data); }, diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index f3d5595e7..7c6fa76d5 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -150,7 +150,8 @@ }, "CONTEXT_MENU": { "COPY": "Copy", - "DELETE": "Delete" + "DELETE": "Delete", + "CREATE_A_CANNED_RESPONSE": "Add to canned responses" } }, "EMAIL_TRANSCRIPT": { diff --git a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue index eb069b163..a50e6bb0d 100644 --- a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue +++ b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue @@ -1,5 +1,15 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue index 073dc9c9a..a182bb8b2 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue @@ -85,25 +85,6 @@
-
- -
-
'test_key', 'phone_number_id' => '123456789', 'business_account_id' => '123456789', - 'webhook_verify_token': 'test_token' } + channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge({ 'api_key' => 'test_key', 'phone_number_id' => '123456789', + 'business_account_id' => '123456789' }) end end diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb index d96ffdc85..9a6620a5e 100644 --- a/spec/models/channel/whatsapp_spec.rb +++ b/spec/models/channel/whatsapp_spec.rb @@ -20,4 +20,20 @@ RSpec.describe Channel::Whatsapp do expect(channel.save).to be(true) end end + + describe 'webhook_verify_token' do + it 'generates webhook_verify_token if not present' do + channel = create(:channel_whatsapp, provider_config: { webhook_verify_token: nil }, provider: 'whatsapp_cloud', account: create(:account), + validate_provider_config: false, sync_templates: false) + + expect(channel.provider_config['webhook_verify_token']).not_to be_nil + end + + it 'does not generate webhook_verify_token if present' do + channel = create(:channel_whatsapp, provider: 'whatsapp_cloud', provider_config: { webhook_verify_token: '123' }, account: create(:account), + validate_provider_config: false, sync_templates: false) + + expect(channel.provider_config['webhook_verify_token']).to eq '123' + end + end end diff --git a/spec/models/contact_inbox_spec.rb b/spec/models/contact_inbox_spec.rb index aba062980..e38ad81e8 100644 --- a/spec/models/contact_inbox_spec.rb +++ b/spec/models/contact_inbox_spec.rb @@ -66,11 +66,11 @@ RSpec.describe ContactInbox do expect(valid_source_id.valid?).to be(true) expect(ci_character_in_source_id.valid?).to be(false) expect(ci_character_in_source_id.errors.full_messages).to eq( - ['Source invalid source id for twilio sms inbox. valid Regex (?-mix:^\\+\\d{1,14}\\z)'] + ['Source invalid source id for twilio sms inbox. valid Regex (?-mix:^\\+\\d{1,15}\\z)'] ) expect(ci_without_plus_in_source_id.valid?).to be(false) expect(ci_without_plus_in_source_id.errors.full_messages).to eq( - ['Source invalid source id for twilio sms inbox. valid Regex (?-mix:^\\+\\d{1,14}\\z)'] + ['Source invalid source id for twilio sms inbox. valid Regex (?-mix:^\\+\\d{1,15}\\z)'] ) end @@ -83,11 +83,11 @@ RSpec.describe ContactInbox do expect(valid_source_id.valid?).to be(true) expect(ci_character_in_source_id.valid?).to be(false) expect(ci_character_in_source_id.errors.full_messages).to eq( - ['Source invalid source id for twilio whatsapp inbox. valid Regex (?-mix:^whatsapp:\\+\\d{1,14}\\z)'] + ['Source invalid source id for twilio whatsapp inbox. valid Regex (?-mix:^whatsapp:\\+\\d{1,15}\\z)'] ) expect(ci_without_plus_in_source_id.valid?).to be(false) expect(ci_without_plus_in_source_id.errors.full_messages).to eq( - ['Source invalid source id for twilio whatsapp inbox. valid Regex (?-mix:^whatsapp:\\+\\d{1,14}\\z)'] + ['Source invalid source id for twilio whatsapp inbox. valid Regex (?-mix:^whatsapp:\\+\\d{1,15}\\z)'] ) end end From 9b5c0de0eaf2fd8730cdfe25e1d6647fa66d2b02 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Wed, 12 Oct 2022 11:58:52 +1100 Subject: [PATCH 25/54] chore: Add router views for agent_bots (#5600) --- .../layout/config/sidebarItems/settings.js | 11 ++ .../sidebarComponents/SecondaryNavItem.vue | 19 +++- .../dashboard/i18n/locale/en/agentBots.json | 5 + .../dashboard/i18n/locale/en/index.js | 106 +++++++++--------- .../dashboard/i18n/locale/en/settings.json | 1 + .../dashboard/settings/agentBots/Index.vue | 18 +++ .../settings/agentBots/agentBot.routes.js | 40 +++++++ .../settings/agentBots/csml/Edit.vue | 6 + .../dashboard/settings/agentBots/csml/New.vue | 6 + .../dashboard/settings/settings.routes.js | 14 ++- .../FluentIcon/dashboard-icons.json | 1 + config/features.yml | 2 + 12 files changed, 168 insertions(+), 61 deletions(-) create mode 100644 app/javascript/dashboard/i18n/locale/en/agentBots.json create mode 100644 app/javascript/dashboard/routes/dashboard/settings/agentBots/Index.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/Edit.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/New.vue diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index 990b35d4a..9ea287963 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -3,6 +3,7 @@ import { frontendURL } from '../../../../helper/URLHelper'; const settings = accountId => ({ parentNav: 'settings', routes: [ + 'agent_bots', 'agent_list', 'canned_list', 'labels_list', @@ -74,10 +75,20 @@ const settings = accountId => ({ { icon: 'automation', label: 'AUTOMATION', + beta: true, hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/automation/list`), toStateName: 'automation_list', }, + { + icon: 'bot', + label: 'AGENT_BOTS', + beta: true, + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/settings/agent-bots`), + toStateName: 'agent_bots', + featureFlagKey: 'agent_bots', + }, { icon: 'chat-multiple', label: 'CANNED_RESPONSES', diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index 220ab18fa..a7a6761cf 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -1,5 +1,5 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js b/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js new file mode 100644 index 000000000..e06f18a0b --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js @@ -0,0 +1,40 @@ +import SettingsContent from '../Wrapper'; +const Bot = () => import('./Index.vue'); +const CsmlEditBot = () => import('./csml/Edit.vue'); +const CsmlNewBot = () => import('./csml/New.vue'); +import { frontendURL } from '../../../../helper/URLHelper'; + +export default { + routes: [ + { + path: frontendURL('accounts/:accountId/settings/agent-bots'), + roles: ['administrator'], + component: SettingsContent, + props: { + headerTitle: 'AGENT_BOTS.HEADER', + icon: 'bot', + showNewButton: false, + }, + children: [ + { + path: '', + name: 'agent_bots', + component: Bot, + roles: ['administrator'], + }, + { + path: 'csml/new', + name: 'agent_bots_csml_new', + component: CsmlNewBot, + roles: ['administrator'], + }, + { + path: 'csml/:botId', + name: 'agent_bots_csml_edit', + component: CsmlEditBot, + roles: ['administrator'], + }, + ], + }, + ], +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/Edit.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/Edit.vue new file mode 100644 index 000000000..112961193 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/Edit.vue @@ -0,0 +1,6 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/New.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/New.vue new file mode 100644 index 000000000..84039a483 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/New.vue @@ -0,0 +1,6 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js index 8f3de8f4e..b4ac93570 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js @@ -1,19 +1,20 @@ import { frontendURL } from '../../../helper/URLHelper'; import account from './account/account.routes'; import agent from './agents/agent.routes'; +import agentBot from './agentBots/agentBot.routes'; +import attributes from './attributes/attributes.routes'; +import automation from './automation/automation.routes'; +import billing from './billing/billing.routes'; +import campaigns from './campaigns/campaigns.routes'; import canned from './canned/canned.routes'; import inbox from './inbox/inbox.routes'; -import integrations from './integrations/integrations.routes'; import integrationapps from './integrationapps/integrations.routes'; +import integrations from './integrations/integrations.routes'; import labels from './labels/labels.routes'; import profile from './profile/profile.routes'; import reports from './reports/reports.routes'; -import campaigns from './campaigns/campaigns.routes'; -import teams from './teams/teams.routes'; -import attributes from './attributes/attributes.routes'; -import automation from './automation/automation.routes'; import store from '../../../store'; -import billing from './billing/billing.routes'; +import teams from './teams/teams.routes'; export default { routes: [ @@ -30,6 +31,7 @@ export default { }, ...account.routes, ...agent.routes, + ...agentBot.routes, ...attributes.routes, ...automation.routes, ...billing.routes, diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index 8b9412efc..0bce7c813 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -29,6 +29,7 @@ "M6.5 2A2.5 2.5 0 0 0 4 4.5v15A2.5 2.5 0 0 0 6.5 22h13.25a.75.75 0 0 0 0-1.5H6.5a1 1 0 0 1-1-1h14.25a.75.75 0 0 0 .75-.75V4.5A2.5 2.5 0 0 0 18 2H6.5ZM19 18H5.5V4.5a1 1 0 0 1 1-1H18a1 1 0 0 1 1 1V18Z" ], "book-open-globe-outline": "M3.5 5.75a.25.25 0 0 1 .25-.25H10c.69 0 1.25.56 1.25 1.25v8.959a6.49 6.49 0 0 1 1.5-2.646V6.75c0-.69.56-1.25 1.25-1.25h6.25a.25.25 0 0 1 .25.25v5.982A6.518 6.518 0 0 1 22 12.81V5.75A1.75 1.75 0 0 0 20.25 4H14c-.788 0-1.499.331-2 .863A2.742 2.742 0 0 0 10 4H3.75A1.75 1.75 0 0 0 2 5.75v12.5c0 .966.784 1.75 1.75 1.75H10c.495 0 .96-.13 1.36-.36a6.473 6.473 0 0 1-.343-1.663A1.248 1.248 0 0 1 10 18.5H3.75a.25.25 0 0 1-.25-.25V5.75ZM16.007 17c.04-1.415.248-2.669.553-3.585.171-.513.364-.893.554-1.134.195-.247.329-.281.386-.281.057 0 .192.034.386.281.19.241.383.62.554 1.134.305.916.513 2.17.553 3.585h-2.986Zm-.396-3.9c.108-.323.23-.622.368-.887A5.504 5.504 0 0 0 12.023 17h2.984c.04-1.5.26-2.866.604-3.9Zm3.778 0a6.133 6.133 0 0 0-.368-.887A5.504 5.504 0 0 1 22.978 17h-2.985c-.04-1.5-.26-2.866-.604-3.9Zm.604 4.9h2.985a5.504 5.504 0 0 1-3.957 4.787c.138-.265.26-.564.368-.886.345-1.035.564-2.4.604-3.901Zm-2.107 4.719c-.194.247-.329.281-.386.281-.057 0-.191-.034-.386-.281-.19-.241-.383-.62-.554-1.135-.305-.915-.513-2.17-.553-3.584h2.986c-.04 1.415-.248 2.669-.553 3.584-.171.514-.364.894-.554 1.135ZM12.023 18a5.504 5.504 0 0 0 3.956 4.787 6.133 6.133 0 0 1-.367-.886c-.346-1.035-.565-2.4-.605-3.901h-2.984Z", + "bot-outline": "M17.753 14a2.25 2.25 0 0 1 2.25 2.25v.905a3.75 3.75 0 0 1-1.307 2.846C17.13 21.345 14.89 22 12 22c-2.89 0-5.128-.656-6.691-2a3.75 3.75 0 0 1-1.306-2.843v-.908A2.25 2.25 0 0 1 6.253 14h11.5Zm0 1.5h-11.5a.75.75 0 0 0-.75.75v.908c0 .655.286 1.278.784 1.706C7.545 19.945 9.44 20.502 12 20.502c2.56 0 4.458-.557 5.719-1.64a2.25 2.25 0 0 0 .784-1.706v-.906a.75.75 0 0 0-.75-.75ZM11.898 2.008 12 2a.75.75 0 0 1 .743.648l.007.102V3.5h3.5a2.25 2.25 0 0 1 2.25 2.25v4.505a2.25 2.25 0 0 1-2.25 2.25h-8.5a2.25 2.25 0 0 1-2.25-2.25V5.75A2.25 2.25 0 0 1 7.75 3.5h3.5v-.749a.75.75 0 0 1 .648-.743L12 2l-.102.007ZM16.25 5h-8.5a.75.75 0 0 0-.75.75v4.505c0 .414.336.75.75.75h8.5a.75.75 0 0 0 .75-.75V5.75a.75.75 0 0 0-.75-.75Zm-6.5 1.5a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Zm4.492 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z", "building-bank-outline": "M13.032 2.325a1.75 1.75 0 0 0-2.064 0L3.547 7.74c-.978.713-.473 2.26.736 2.26H4.5v5.8A2.75 2.75 0 0 0 3 18.25v1.5c0 .413.336.75.75.75h16.5a.75.75 0 0 0 .75-.75v-1.5a2.75 2.75 0 0 0-1.5-2.45V10h.217c1.21 0 1.713-1.547.736-2.26l-7.421-5.416Zm-1.18 1.211a.25.25 0 0 1 .295 0L18.95 8.5H5.05l6.803-4.964ZM18 10v5.5h-2V10h2Zm-3.5 0v5.5h-1.75V10h1.75Zm-3.25 0v5.5H9.5V10h1.75Zm-5.5 7h12.5c.69 0 1.25.56 1.25 1.25V19h-15v-.75c0-.69.56-1.25 1.25-1.25ZM6 15.5V10h2v5.5H6Z", "calendar-clock-outline": [ "M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z", diff --git a/config/features.yml b/config/features.yml index 6fbb73a78..01bfe00e5 100644 --- a/config/features.yml +++ b/config/features.yml @@ -15,3 +15,5 @@ enabled: false - name: help_center enabled: true +- name: agent_bots + enabled: false From 6c160ccad5244f068332348febc111c162e6de10 Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Wed, 12 Oct 2022 11:24:17 +0530 Subject: [PATCH 26/54] feat: Add API module and Vuex store for Macros (#5603) --- app/javascript/dashboard/api/macros.js | 16 ++ .../dashboard/api/specs/macros.spec.js | 14 ++ .../dashboard/store/modules/macros.js | 117 ++++++++++++++ .../modules/specs/macros/actions.spec.js | 151 ++++++++++++++++++ .../store/modules/specs/macros/fixtures.js | 135 ++++++++++++++++ .../modules/specs/macros/getters.spec.js | 32 ++++ .../modules/specs/macros/mutations.spec.js | 38 +++++ .../dashboard/store/mutation-types.js | 7 + 8 files changed, 510 insertions(+) create mode 100644 app/javascript/dashboard/api/macros.js create mode 100644 app/javascript/dashboard/api/specs/macros.spec.js create mode 100644 app/javascript/dashboard/store/modules/macros.js create mode 100644 app/javascript/dashboard/store/modules/specs/macros/actions.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/macros/fixtures.js create mode 100644 app/javascript/dashboard/store/modules/specs/macros/getters.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/macros/mutations.spec.js diff --git a/app/javascript/dashboard/api/macros.js b/app/javascript/dashboard/api/macros.js new file mode 100644 index 000000000..7b123c9e8 --- /dev/null +++ b/app/javascript/dashboard/api/macros.js @@ -0,0 +1,16 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class MacrosAPI extends ApiClient { + constructor() { + super('macros', { accountScoped: true }); + } + + executeMacro({ macroId, conversationIds }) { + return axios.post(`${this.url}/${macroId}/execute`, { + conversation_ids: conversationIds, + }); + } +} + +export default new MacrosAPI(); diff --git a/app/javascript/dashboard/api/specs/macros.spec.js b/app/javascript/dashboard/api/specs/macros.spec.js new file mode 100644 index 000000000..94e936521 --- /dev/null +++ b/app/javascript/dashboard/api/specs/macros.spec.js @@ -0,0 +1,14 @@ +import macros from '../macros'; +import ApiClient from '../ApiClient'; + +describe('#macrosAPI', () => { + it('creates correct instance', () => { + expect(macros).toBeInstanceOf(ApiClient); + expect(macros).toHaveProperty('get'); + expect(macros).toHaveProperty('create'); + expect(macros).toHaveProperty('update'); + expect(macros).toHaveProperty('delete'); + expect(macros).toHaveProperty('show'); + expect(macros.url).toBe('/api/v1/macros'); + }); +}); diff --git a/app/javascript/dashboard/store/modules/macros.js b/app/javascript/dashboard/store/modules/macros.js new file mode 100644 index 000000000..952f53f17 --- /dev/null +++ b/app/javascript/dashboard/store/modules/macros.js @@ -0,0 +1,117 @@ +import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; +import types from '../mutation-types'; +import MacrosAPI from '../../api/macros'; +import { throwErrorMessage } from '../utils/api'; + +export const state = { + records: [], + uiFlags: { + isFetchingItem: false, + isFetching: false, + isCreating: false, + isDeleting: false, + isUpdating: false, + isExecuting: false, + }, +}; + +export const getters = { + getMacros($state) { + return $state.records; + }, + getMacro: $state => id => { + return $state.records.find(record => record.id === Number(id)); + }, + getUIFlags($state) { + return $state.uiFlags; + }, +}; + +export const actions = { + get: async function getMacros({ commit }) { + commit(types.SET_MACROS_UI_FLAG, { isFetching: true }); + try { + const response = await MacrosAPI.get(); + commit(types.SET_MACROS, response.data.payload); + } catch (error) { + // Ignore error + } finally { + commit(types.SET_MACROS_UI_FLAG, { isFetching: false }); + } + }, + getSingleMacro: async function getMacroById({ commit }, macroId) { + commit(types.SET_MACROS_UI_FLAG, { isFetchingItem: true }); + try { + const response = await MacrosAPI.show(macroId); + commit(types.ADD_MACRO, response.data.payload); + } catch (error) { + // Ignore error + } finally { + commit(types.SET_MACROS_UI_FLAG, { isFetchingItem: false }); + } + }, + create: async function createMacro({ commit }, macrosObj) { + commit(types.SET_MACROS_UI_FLAG, { isCreating: true }); + try { + const response = await MacrosAPI.create(macrosObj); + commit(types.ADD_MACRO, response.data.payload); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_MACROS_UI_FLAG, { isCreating: false }); + } + }, + execute: async function executeMacro({ commit }, macrosObj) { + commit(types.SET_MACROS_UI_FLAG, { isExecuting: true }); + try { + await MacrosAPI.executeMacro(macrosObj); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_MACROS_UI_FLAG, { isExecuting: false }); + } + }, + update: async ({ commit }, { id, ...updateObj }) => { + commit(types.SET_MACROS_UI_FLAG, { isUpdating: true }); + try { + const response = await MacrosAPI.update(id, updateObj); + commit(types.EDIT_MACRO, response.data.payload); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_MACROS_UI_FLAG, { isUpdating: false }); + } + }, + delete: async ({ commit }, id) => { + commit(types.SET_MACROS_UI_FLAG, { isDeleting: true }); + try { + await MacrosAPI.delete(id); + commit(types.DELETE_MACRO, id); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_MACROS_UI_FLAG, { isDeleting: false }); + } + }, +}; + +export const mutations = { + [types.SET_MACROS_UI_FLAG]($state, data) { + $state.uiFlags = { + ...$state.uiFlags, + ...data, + }; + }, + [types.ADD_MACRO]: MutationHelpers.setSingleRecord, + [types.SET_MACROS]: MutationHelpers.set, + [types.EDIT_MACRO]: MutationHelpers.update, + [types.DELETE_MACRO]: MutationHelpers.destroy, +}; + +export default { + namespaced: true, + actions, + state, + getters, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/specs/macros/actions.spec.js b/app/javascript/dashboard/store/modules/specs/macros/actions.spec.js new file mode 100644 index 000000000..95bba8e1d --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/macros/actions.spec.js @@ -0,0 +1,151 @@ +import axios from 'axios'; +import { actions } from '../../macros'; +import * as types from '../../../mutation-types'; +import macrosList from './fixtures'; + +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#get', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ data: { payload: macrosList } }); + await actions.get({ commit }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isFetching: true }], + [types.default.SET_MACROS, macrosList], + [types.default.SET_MACROS_UI_FLAG, { isFetching: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.get({ commit }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isFetching: true }], + [types.default.SET_MACROS_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#getMacroById', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ data: { payload: macrosList[0] } }); + await actions.getSingleMacro({ commit }, 22); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isFetchingItem: true }], + [types.default.ADD_MACRO, macrosList[0]], + [types.default.SET_MACROS_UI_FLAG, { isFetchingItem: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.getSingleMacro({ commit }, 22); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isFetchingItem: true }], + [types.default.SET_MACROS_UI_FLAG, { isFetchingItem: false }], + ]); + }); + }); + + describe('#create', () => { + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: { payload: macrosList[0] } }); + await actions.create({ commit }, macrosList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isCreating: true }], + [types.default.ADD_MACRO, macrosList[0]], + [types.default.SET_MACROS_UI_FLAG, { isCreating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.create({ commit })).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isCreating: true }], + [types.default.SET_MACROS_UI_FLAG, { isCreating: false }], + ]); + }); + }); + + describe('#execute', () => { + const macroId = 12; + const conversationIds = [1]; + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: null }); + await actions.execute( + { commit }, + { + macroId, + conversationIds, + } + ); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isExecuting: true }], + [types.default.SET_MACROS_UI_FLAG, { isExecuting: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.execute( + { commit }, + { + macroId, + conversationIds, + } + ) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isExecuting: true }], + [types.default.SET_MACROS_UI_FLAG, { isExecuting: false }], + ]); + }); + }); + + describe('#update', () => { + it('sends correct actions if API is success', async () => { + axios.patch.mockResolvedValue({ + data: { payload: macrosList[0] }, + }); + await actions.update({ commit }, macrosList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isUpdating: true }], + [types.default.EDIT_MACRO, macrosList[0]], + [types.default.SET_MACROS_UI_FLAG, { isUpdating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.patch.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.update({ commit }, macrosList[0])).rejects.toThrow( + Error + ); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isUpdating: true }], + [types.default.SET_MACROS_UI_FLAG, { isUpdating: false }], + ]); + }); + }); + + describe('#delete', () => { + it('sends correct actions if API is success', async () => { + axios.delete.mockResolvedValue({ data: macrosList[0] }); + await actions.delete({ commit }, macrosList[0].id); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isDeleting: true }], + [types.default.DELETE_MACRO, macrosList[0].id], + [types.default.SET_MACROS_UI_FLAG, { isDeleting: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.delete({ commit }, macrosList[0].id) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_MACROS_UI_FLAG, { isDeleting: true }], + [types.default.SET_MACROS_UI_FLAG, { isDeleting: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/macros/fixtures.js b/app/javascript/dashboard/store/modules/specs/macros/fixtures.js new file mode 100644 index 000000000..b75bd2836 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/macros/fixtures.js @@ -0,0 +1,135 @@ +export default [ + { + id: 22, + name: 'Assign billing label and sales team and message user', + visibility: 'global', + created_by: { + id: 1, + account_id: 1, + availability_status: 'online', + auto_offline: true, + confirmed: true, + email: 'john@acme.inc', + available_name: 'Fayaz Ahmed', + name: 'Fayaz Ahmed', + role: 'administrator', + thumbnail: + 'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBUUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--16c85844c93f9c139deb782137b49c87c9bc871c/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/memoji.png', + }, + updated_by: { + id: 1, + account_id: 1, + availability_status: 'online', + auto_offline: true, + confirmed: true, + email: 'john@acme.inc', + available_name: 'Fayaz Ahmed', + name: 'Fayaz Ahmed', + role: 'administrator', + thumbnail: + 'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBUUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--16c85844c93f9c139deb782137b49c87c9bc871c/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/memoji.png', + }, + account_id: 1, + actions: [ + { + action_name: 'add_label', + action_params: ['sales', 'billing'], + }, + { + action_name: 'assign_team', + action_params: [1], + }, + { + action_name: 'send_message', + action_params: [ + "Thank you for reaching out, we're looking into this on priority and we'll get back to you asap.", + ], + }, + ], + }, + { + id: 23, + name: 'Assign label priority and send email to team', + visibility: 'global', + created_by: { + id: 1, + account_id: 1, + availability_status: 'online', + auto_offline: true, + confirmed: true, + email: 'john@acme.inc', + available_name: 'Fayaz Ahmed', + name: 'Fayaz Ahmed', + role: 'administrator', + thumbnail: + 'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBUUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--16c85844c93f9c139deb782137b49c87c9bc871c/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/memoji.png', + }, + updated_by: { + id: 1, + account_id: 1, + availability_status: 'online', + auto_offline: true, + confirmed: true, + email: 'john@acme.inc', + available_name: 'Fayaz Ahmed', + name: 'Fayaz Ahmed', + role: 'administrator', + thumbnail: + 'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBUUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--16c85844c93f9c139deb782137b49c87c9bc871c/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/memoji.png', + }, + account_id: 1, + actions: [ + { + action_name: 'add_label', + action_params: ['priority'], + }, + { + action_name: 'send_email_to_team', + action_params: [ + { + message: 'Hello team,\n\nThis looks important, please take look.', + team_ids: [1], + }, + ], + }, + ], + }, + { + id: 25, + name: 'Webhook', + visibility: 'global', + created_by: { + id: 1, + account_id: 1, + availability_status: 'online', + auto_offline: true, + confirmed: true, + email: 'john@acme.inc', + available_name: 'Fayaz Ahmed', + name: 'Fayaz Ahmed', + role: 'administrator', + thumbnail: + 'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBUUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--16c85844c93f9c139deb782137b49c87c9bc871c/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/memoji.png', + }, + updated_by: { + id: 1, + account_id: 1, + availability_status: 'online', + auto_offline: true, + confirmed: true, + email: 'john@acme.inc', + available_name: 'Fayaz Ahmed', + name: 'Fayaz Ahmed', + role: 'administrator', + thumbnail: + 'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBUUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--16c85844c93f9c139deb782137b49c87c9bc871c/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/memoji.png', + }, + account_id: 1, + actions: [ + { + action_name: 'send_webhook_event', + action_params: ['https://google.com'], + }, + ], + }, +]; diff --git a/app/javascript/dashboard/store/modules/specs/macros/getters.spec.js b/app/javascript/dashboard/store/modules/specs/macros/getters.spec.js new file mode 100644 index 000000000..d855f66ff --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/macros/getters.spec.js @@ -0,0 +1,32 @@ +import { getters } from '../../macros'; +import macros from './fixtures'; +describe('#getters', () => { + it('getMacros', () => { + const state = { records: macros }; + expect(getters.getMacros(state)).toEqual(macros); + }); + + it('getMacro', () => { + const state = { records: macros }; + expect(getters.getMacro(state)(22)).toEqual(macros[0]); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { + isFetching: true, + isCreating: false, + isUpdating: false, + isDeleting: false, + isExecuting: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isFetching: true, + isCreating: false, + isUpdating: false, + isDeleting: false, + isExecuting: false, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/macros/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/macros/mutations.spec.js new file mode 100644 index 000000000..436738638 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/macros/mutations.spec.js @@ -0,0 +1,38 @@ +import types from '../../../mutation-types'; +import { mutations } from '../../macros'; +import macros from './fixtures'; +describe('#mutations', () => { + describe('#SET_MACROS', () => { + it('set macrtos records', () => { + const state = { records: [] }; + mutations[types.SET_MACROS](state, macros); + expect(state.records).toEqual(macros); + }); + }); + + describe('#ADD_MACRO', () => { + it('push newly created macro to the store', () => { + const state = { records: [macros[0]] }; + mutations[types.ADD_MACRO](state, macros[1]); + expect(state.records).toEqual([macros[0], macros[1]]); + }); + }); + + describe('#EDIT_MACRO', () => { + it('update macro record', () => { + const state = { records: [macros[0]] }; + mutations[types.EDIT_MACRO](state, macros[0]); + expect(state.records[0].name).toEqual( + 'Assign billing label and sales team and message user' + ); + }); + }); + + describe('#DELETE_MACRO', () => { + it('delete macro record', () => { + const state = { records: [macros[0]] }; + mutations[types.DELETE_MACRO](state, 22); + expect(state.records).toEqual([]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index bf971931d..bcb6ad9a5 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -252,4 +252,11 @@ export default { ADD_AGENT_BOT: 'ADD_AGENT_BOT', EDIT_AGENT_BOT: 'EDIT_AGENT_BOT', DELETE_AGENT_BOT: 'DELETE_AGENT_BOT', + + // MACROS + SET_MACROS_UI_FLAG: 'SET_MACROS_UI_FLAG', + SET_MACROS: 'SET_MACROS', + ADD_MACRO: 'ADD_MACRO', + EDIT_MACRO: 'EDIT_MACRO', + DELETE_MACRO: 'DELETE_MACRO', }; From 32d885a19b2ead2a704fc5342a569de438b71b39 Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Wed, 12 Oct 2022 11:50:20 +0530 Subject: [PATCH 27/54] feat: Add macros routes and views (#5604) --- .../layout/config/sidebarItems/settings.js | 60 +++++++++++-------- .../dashboard/i18n/locale/en/index.js | 2 + .../dashboard/i18n/locale/en/macros.json | 5 ++ .../dashboard/i18n/locale/en/settings.json | 8 ++- .../dashboard/settings/macros/Index.vue | 11 ++++ .../dashboard/settings/macros/MacroEditor.vue | 9 +++ .../settings/macros/macros.routes.js | 38 ++++++++++++ .../dashboard/settings/settings.routes.js | 2 + .../FluentIcon/dashboard-icons.json | 1 + config/features.yml | 2 + 10 files changed, 112 insertions(+), 26 deletions(-) create mode 100644 app/javascript/dashboard/i18n/locale/en/macros.json create mode 100644 app/javascript/dashboard/routes/dashboard/settings/macros/Index.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index 9ea287963..83a0c2309 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -5,34 +5,37 @@ const settings = accountId => ({ routes: [ 'agent_bots', 'agent_list', - 'canned_list', - 'labels_list', - 'settings_inbox', 'attributes_list', - 'settings_inbox_new', - 'settings_inbox_list', - 'settings_inbox_show', - 'settings_inboxes_page_channel', - 'settings_inboxes_add_agents', - 'settings_inbox_finish', - 'settings_integrations', - 'settings_integrations_webhook', - 'settings_integrations_integration', - 'settings_applications', - 'settings_integrations_dashboard_apps', - 'settings_applications_webhook', - 'settings_applications_integration', - 'general_settings', + 'automation_list', + 'billing_settings_index', + 'canned_list', 'general_settings_index', + 'general_settings', + 'labels_list', + 'macros_edit', + 'macros_new', + 'macros_wrapper', + 'settings_applications_integration', + 'settings_applications_webhook', + 'settings_applications', + 'settings_inbox_finish', + 'settings_inbox_list', + 'settings_inbox_new', + 'settings_inbox_show', + 'settings_inbox', + 'settings_inboxes_add_agents', + 'settings_inboxes_page_channel', + 'settings_integrations_dashboard_apps', + 'settings_integrations_integration', + 'settings_integrations_webhook', + 'settings_integrations', + 'settings_teams_add_agents', + 'settings_teams_edit_finish', + 'settings_teams_edit_members', + 'settings_teams_edit', + 'settings_teams_finish', 'settings_teams_list', 'settings_teams_new', - 'settings_teams_add_agents', - 'settings_teams_finish', - 'settings_teams_edit', - 'settings_teams_edit_members', - 'settings_teams_edit_finish', - 'billing_settings_index', - 'automation_list', ], menuItems: [ { @@ -89,6 +92,15 @@ const settings = accountId => ({ toStateName: 'agent_bots', featureFlagKey: 'agent_bots', }, + { + icon: 'flash-settings', + label: 'MACROS', + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/settings/macros`), + toStateName: 'macros_wrapper', + beta: true, + featureFlagKey: 'macros', + }, { icon: 'chat-multiple', label: 'CANNED_RESPONSES', diff --git a/app/javascript/dashboard/i18n/locale/en/index.js b/app/javascript/dashboard/i18n/locale/en/index.js index 6e45310b0..93e560119 100644 --- a/app/javascript/dashboard/i18n/locale/en/index.js +++ b/app/javascript/dashboard/i18n/locale/en/index.js @@ -18,6 +18,7 @@ import integrationApps from './integrationApps.json'; import integrations from './integrations.json'; import labelsMgmt from './labelsMgmt.json'; import login from './login.json'; +import macros from './macros.json'; import report from './report.json'; import resetPassword from './resetPassword.json'; import setNewPassword from './setNewPassword.json'; @@ -47,6 +48,7 @@ export default { ...integrations, ...labelsMgmt, ...login, + ...macros, ...report, ...resetPassword, ...setNewPassword, diff --git a/app/javascript/dashboard/i18n/locale/en/macros.json b/app/javascript/dashboard/i18n/locale/en/macros.json new file mode 100644 index 000000000..3923e4374 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/en/macros.json @@ -0,0 +1,5 @@ +{ + "MACROS": { + "HEADER": "Macros" + } +} diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index a99bfc2e8..c5f954b1c 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -99,7 +99,11 @@ }, "AVAILABILITY": { "LABEL": "Availability", - "STATUSES_LIST": ["Online", "Busy", "Offline"] + "STATUSES_LIST": [ + "Online", + "Busy", + "Offline" + ] }, "EMAIL": { "LABEL": "Your email address", @@ -186,6 +190,7 @@ "LABELS": "Labels", "CUSTOM_ATTRIBUTES": "Custom Attributes", "AUTOMATION": "Automation", + "MACROS": "Macros", "TEAMS": "Teams", "BILLING": "Billing", "CUSTOM_VIEWS_FOLDER": "Folders", @@ -230,7 +235,6 @@ "DESCRIPTION": "View your previous invoices, edit your billing details, or cancel your subscription.", "BUTTON_TXT": "Go to the billing portal" }, - "CHAT_WITH_US": { "TITLE": "Need help?", "DESCRIPTION": "Do you face any issues in billing? We are here to help.", diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/macros/Index.vue new file mode 100644 index 000000000..39f017fef --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/Index.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue b/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue new file mode 100644 index 000000000..2de1f787c --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js b/app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js new file mode 100644 index 000000000..9752b79fb --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js @@ -0,0 +1,38 @@ +import SettingsContent from '../Wrapper'; +import Macros from './Index'; +const MacroEditor = () => import('./MacroEditor'); +import { frontendURL } from 'dashboard/helper/URLHelper'; + +export default { + routes: [ + { + path: frontendURL('accounts/:accountId/settings/macros'), + component: SettingsContent, + props: { + headerTitle: 'MACROS.HEADER', + icon: 'flash-settings', + showNewButton: false, + }, + children: [ + { + path: '', + name: 'macros_wrapper', + component: Macros, + roles: ['administrator', 'agent'], + }, + { + path: 'new', + name: 'macros_new', + component: MacroEditor, + roles: ['administrator', 'agent'], + }, + { + path: ':macroId/edit', + name: 'macros_edit', + component: MacroEditor, + roles: ['administrator', 'agent'], + }, + ], + }, + ], +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js index b4ac93570..7c8cc11fa 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js @@ -11,6 +11,7 @@ import inbox from './inbox/inbox.routes'; import integrationapps from './integrationapps/integrations.routes'; import integrations from './integrations/integrations.routes'; import labels from './labels/labels.routes'; +import macros from './macros/macros.routes'; import profile from './profile/profile.routes'; import reports from './reports/reports.routes'; import store from '../../../store'; @@ -41,6 +42,7 @@ export default { ...integrationapps.routes, ...integrations.routes, ...labels.routes, + ...macros.routes, ...profile.routes, ...reports.routes, ...teams.routes, diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index 0bce7c813..9a5eaa893 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -80,6 +80,7 @@ "filter-outline": "M13.5 16a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h3Zm3-5a.75.75 0 0 1 0 1.5h-9a.75.75 0 0 1 0-1.5h9Zm3-5a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1 0-1.5h15Z", "file-upload-outline": "M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6Zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7ZM5.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9Zm2.354-4.854a.5.5 0 1 1-.708.708L6 13.707V16.5a.5.5 0 0 1-1 0v-2.793l-1.146 1.147a.5.5 0 1 1-.708-.707l2-2A.5.5 0 0 1 5.497 12h.006a.498.498 0 0 1 .348.144l.003.003l2 2Z", "flash-on-outline": "m8.294 14-1.767 7.068c-.187.746.736 1.256 1.269.701L19.79 9.27A.75.75 0 0 0 19.25 8h-4.46l1.672-5.013A.75.75 0 0 0 15.75 2h-7a.75.75 0 0 0-.721.544l-3 10.5A.75.75 0 0 0 5.75 14h2.544Zm4.745-5.487a.75.75 0 0 0 .711.987h3.74l-8.824 9.196 1.316-5.264a.75.75 0 0 0-.727-.932h-2.51l2.57-9h5.394l-1.67 5.013Z", + "flash-settings-outline": "M6.19 2.77c.13-.455.547-.77 1.02-.77h5.25c.724 0 1.236.71 1.007 1.398l-.002.008L12.204 7h2.564c.946 0 1.407 1.144.766 1.811l-.003.004l-.237.242a5.545 5.545 0 0 0-1.374-.027l.894-.912a.056.056 0 0 0 .017-.032a.084.084 0 0 0-.007-.044a.079.079 0 0 0-.025-.034c-.005-.004-.013-.008-.031-.008h-3.27a.5.5 0 0 1-.471-.666L12.52 3.08a.062.062 0 0 0-.06-.08H7.211a.062.062 0 0 0-.06.045l-2.25 7.874c-.01.04.019.08.06.08H6.87a.5.5 0 0 1 .485.62l-1.325 5.3a.086.086 0 0 0-.003.03a.02.02 0 0 0 .003.011a.08.08 0 0 0 .072.04a.03.03 0 0 0 .01-.004a.087.087 0 0 0 .024-.018l.003-.004l2.882-2.94a5.573 5.573 0 0 0 .054 1.372l-2.22 2.267c-.754.782-2.059.06-1.795-.996l1.17-4.679H4.96a1.062 1.062 0 0 1-1.021-1.354l2.25-7.873Zm5.877 8.673a2 2 0 0 1-1.431 2.478l-.461.118a4.702 4.702 0 0 0 .01 1.016l.35.083a2 2 0 0 1 1.456 2.519l-.127.422c.257.204.537.378.835.518l.325-.344a2 2 0 0 1 2.91.002l.337.358c.292-.135.568-.302.822-.498l-.157-.556a2 2 0 0 1 1.431-2.479l.46-.117a4.702 4.702 0 0 0-.01-1.017l-.348-.082a2 2 0 0 1-1.456-2.52l.126-.421a4.32 4.32 0 0 0-.835-.519l-.325.344a2 2 0 0 1-2.91-.001l-.337-.358a4.316 4.316 0 0 0-.821.497l.156.557ZM14.5 15.5a1 1 0 1 1 0-2a1 1 0 0 1 0 2Z", "folder-outline": "M8.207 4c.46 0 .908.141 1.284.402l.156.12L12.022 6.5h7.728a2.25 2.25 0 0 1 2.229 1.938l.016.158.005.154v9a2.25 2.25 0 0 1-2.096 2.245L19.75 20H4.25a2.25 2.25 0 0 1-2.245-2.096L2 17.75V6.25a2.25 2.25 0 0 1 2.096-2.245L4.25 4h3.957Zm1.44 5.979a2.25 2.25 0 0 1-1.244.512l-.196.009-4.707-.001v7.251c0 .38.282.694.648.743l.102.007h15.5a.75.75 0 0 0 .743-.648l.007-.102v-9a.75.75 0 0 0-.648-.743L19.75 8h-7.729L9.647 9.979ZM8.207 5.5H4.25a.75.75 0 0 0-.743.648L3.5 6.25v2.749L8.207 9a.75.75 0 0 0 .395-.113l.085-.06 1.891-1.578-1.89-1.575a.75.75 0 0 0-.377-.167L8.207 5.5Z", "globe-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999ZM14.939 16.5H9.06c.652 2.414 1.786 4.002 2.939 4.002s2.287-1.588 2.939-4.002Zm-7.43 0H4.785a8.532 8.532 0 0 0 4.094 3.411c-.522-.82-.953-1.846-1.27-3.015l-.102-.395Zm11.705 0h-2.722c-.324 1.335-.792 2.5-1.373 3.411a8.528 8.528 0 0 0 3.91-3.127l.185-.283ZM7.094 10H3.735l-.005.017a8.525 8.525 0 0 0-.233 1.984c0 1.056.193 2.067.545 3h3.173a20.847 20.847 0 0 1-.123-5Zm8.303 0H8.603a18.966 18.966 0 0 0 .135 5h6.524a18.974 18.974 0 0 0 .135-5Zm4.868 0h-3.358c.062.647.095 1.317.095 2a20.3 20.3 0 0 1-.218 3h3.173a8.482 8.482 0 0 0 .544-3c0-.689-.082-1.36-.236-2ZM8.88 4.09l-.023.008A8.531 8.531 0 0 0 4.25 8.5h3.048c.314-1.752.86-3.278 1.583-4.41ZM12 3.499l-.116.005C10.62 3.62 9.396 5.622 8.83 8.5h6.342c-.566-2.87-1.783-4.869-3.045-4.995L12 3.5Zm3.12.59.107.175c.669 1.112 1.177 2.572 1.475 4.237h3.048a8.533 8.533 0 0 0-4.339-4.29l-.291-.121Z", "globe-desktop-outline": "M22.002 12C22.002 6.477 17.524 2 12 2 6.476 1.999 2 6.477 2 12.001c0 5.186 3.947 9.45 9.001 9.952V20.11c-.778-.612-1.478-1.905-1.939-3.61h1.94V15H8.737a18.969 18.969 0 0 1-.135-5h6.794c.068.64.105 1.31.105 2h1.5c0-.684-.033-1.353-.095-2h3.358c.154.64.237 1.31.237 2h1.5ZM4.786 16.5h2.722l.102.396c.317 1.17.748 2.195 1.27 3.015a8.532 8.532 0 0 1-4.094-3.41ZM3.736 10h3.358a20.847 20.847 0 0 0-.095 2c0 1.043.075 2.051.217 3H4.043a8.483 8.483 0 0 1-.544-3c0-.682.08-1.347.232-1.983L3.736 10Zm5.122-5.902.023-.008C8.16 5.222 7.611 6.748 7.298 8.5H4.25c.905-2 2.56-3.587 4.608-4.402Zm3.026-.594L12 3.5l.126.006c1.262.126 2.48 2.125 3.045 4.995H8.83c.568-2.878 1.79-4.88 3.055-4.996Zm3.343.76-.107-.174.291.121a8.533 8.533 0 0 1 4.339 4.29h-3.048c-.298-1.665-.806-3.125-1.475-4.237Z M12 19a1 1 0 0 0 1 1h3v2h-.5a.5.5 0 1 0 0 1h4a.5.5 0 0 0 0-1H19v-2h3a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1v5Z", diff --git a/config/features.yml b/config/features.yml index 01bfe00e5..ec779cd08 100644 --- a/config/features.yml +++ b/config/features.yml @@ -17,3 +17,5 @@ enabled: true - name: agent_bots enabled: false +- name: macros + enabled: false From 1bdd59f0259d3d7204145acea2ec0c98120eb0e9 Mon Sep 17 00:00:00 2001 From: Jordan Brough Date: Wed, 12 Oct 2022 02:08:18 -0600 Subject: [PATCH 28/54] Find by downcased email in SupportMailbox (#5211) --- app/mailboxes/support_mailbox.rb | 2 +- spec/fixtures/files/support_uppercase.eml | 632 ++++++++++++++++++++++ spec/mailboxes/support_mailbox_spec.rb | 31 +- spec/support/negated_matchers.rb | 3 + 4 files changed, 662 insertions(+), 6 deletions(-) create mode 100644 spec/fixtures/files/support_uppercase.eml create mode 100644 spec/support/negated_matchers.rb diff --git a/app/mailboxes/support_mailbox.rb b/app/mailboxes/support_mailbox.rb index b0a1a68df..8accda902 100644 --- a/app/mailboxes/support_mailbox.rb +++ b/app/mailboxes/support_mailbox.rb @@ -72,7 +72,7 @@ class SupportMailbox < ApplicationMailbox end def find_or_create_contact - @contact = @inbox.contacts.find_by(email: @processed_mail.original_sender) + @contact = @inbox.contacts.find_by(email: @processed_mail.original_sender&.downcase) if @contact.present? @contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact) else diff --git a/spec/fixtures/files/support_uppercase.eml b/spec/fixtures/files/support_uppercase.eml new file mode 100644 index 000000000..9354312ee --- /dev/null +++ b/spec/fixtures/files/support_uppercase.eml @@ -0,0 +1,632 @@ +From: Sony Mathew +Mime-Version: 1.0 (Apple Message framework v1244.3) +Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74" +Subject: Discussion: Let's debate these attachments +Date: Tue, 20 Apr 2020 04:20:20 -0400 +In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail> +Reply-To: Sony Mathew +To: "Replies" +References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail> +Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@chatwoot.com> +X-Mailer: Apple Mail (2.1244.3) + +--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=utf-8 + +Let's talk about these images: + + +--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74 +Content-Type: multipart/related; + type="text/html"; + boundary="Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1" + + +--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1 +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename=avatar1.jpeg +Content-Type: image/jpg; + name="avatar1.jpeg" +Content-Id: <7AAEB353-2341-4D46-A054-5CA5CB2363B7> + +/9j/4AAQSkZJRgABAQAAAQABAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdC +IFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAA +AADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFj +cHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAA +ABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAAD +TAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJD +AAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5 +OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEA +AAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAA +AAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAA +AA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBo +dHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAt +IHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAt +IHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcg +Q29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENv +bmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAA +ABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAA +AAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAK +AA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUA +mgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEy +ATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMC +DAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMh +Ay0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4E +jASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3 +BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDII +RghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqY +Cq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUAN +Wg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBh +EH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT +5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReu +F9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9oc +AhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCY +IMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZcl +xyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2 +K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIx +SjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDec +N9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+ +oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXe +RiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN +3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYP +VlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1f +D19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/ +aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfBy +S3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyB +fOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuH +n4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLj +k02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6f +HZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1 +q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm4 +0blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZG +xsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnU +y9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj +4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozz +GfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDAAICAgIC +AQICAgICAgIDAwYEAwMDAwcFBQQGCAcICAgHCAgJCg0LCQkMCggICw8LDA0ODg4OCQsQEQ8OEQ0O +Dg7/2wBDAQICAgMDAwYEBAYOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O +Dg4ODg4ODg4ODg4ODg7/wAARCADwAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAEC +AwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0Kx +wRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1 +dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ +2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QA +tREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk +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 +mk8VWW5WRGJGAOaAP//Z + +--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1 +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename=avatar2.jpg +Content-Type: image/jpg; + x-unix-mode=0700; + name="avatar2.jpg" +Content-Id: <4594E827-6E69-4329-8691-6BC35E3E73A0> + +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 + +--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1-- + +--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74-- \ No newline at end of file diff --git a/spec/mailboxes/support_mailbox_spec.rb b/spec/mailboxes/support_mailbox_spec.rb index 59217b212..946275b71 100644 --- a/spec/mailboxes/support_mailbox_spec.rb +++ b/spec/mailboxes/support_mailbox_spec.rb @@ -103,14 +103,35 @@ RSpec.describe SupportMailbox, type: :mailbox do end describe 'handle inbox contacts' do - let(:contact) { create(:contact, account: account, email: support_mail.mail.from.first) } - let(:contact_inbox) { create(:contact_inbox, inbox: channel_email.inbox, contact: contact) } + let!(:contact) { create(:contact, account: account, email: support_mail.mail.from.first) } + let!(:contact_inbox) { create(:contact_inbox, inbox: channel_email.inbox, contact: contact) } it 'does not create new contact if that contact exists in the inbox' do - # making sure we have a contact already present - expect(contact_inbox.contact.email).to eq(support_mail.mail.from.first) - described_subject + expect do + described_subject + end + .to(not_change { Contact.count } + .and(not_change { ContactInbox.count })) + expect(conversation.messages.last.sender.id).to eq(contact.id) + expect(conversation.contact_inbox).to eq(contact_inbox) + end + + context 'with uppercase reply-to' do + let(:support_mail) { create_inbound_email_from_fixture('support_uppercase.eml') } + let!(:contact) { create(:contact, account: account, email: support_mail.mail.from.first) } + let!(:contact_inbox) { create(:contact_inbox, inbox: channel_email.inbox, contact: contact) } + + it 'does not create new contact if that contact exists in the inbox' do + expect do + described_subject + end + .to(not_change { Contact.count } + .and(not_change { ContactInbox.count })) + + expect(conversation.messages.last.sender.id).to eq(contact.id) + expect(conversation.contact_inbox).to eq(contact_inbox) + end end end diff --git a/spec/support/negated_matchers.rb b/spec/support/negated_matchers.rb new file mode 100644 index 000000000..2adeec64a --- /dev/null +++ b/spec/support/negated_matchers.rb @@ -0,0 +1,3 @@ +# "not_change" is needed to support chaining "change" matchers +# see https://stackoverflow.com/a/34969429/58876 +RSpec::Matchers.define_negated_matcher :not_change, :change From 242de0b3f973e1ae8205b4c2ba18236c283b7aa5 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 12 Oct 2022 23:32:24 +0530 Subject: [PATCH 29/54] chore: `Vscode` extension recommendations (#5607) * Added vscode extension recommendations * Revert the change * Revert .gitignore * Change `volar` to `vetur` --- .gitignore | 5 +---- .vscode/extensions.json | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 .vscode/extensions.json diff --git a/.gitignore b/.gitignore index e4b14d2f5..11a8c50e3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,6 @@ public/packs* *.un~ .jest-cache -#VS Code files -.vscode - # ignore jetbrains IDE files .idea @@ -62,4 +59,4 @@ package-lock.json test/cypress/videos/* /config/master.key -/config/*.enc \ No newline at end of file +/config/*.enc diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..254e696a4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,32 @@ +{ + "recommendations": [ + // Spell check + "streetsidesoftware.code-spell-checker", + // Better Comments + "aaron-bond.better-comments", + // Rails Test Runner + "davidpallinder.rails-test-runner", + // Eslint + "dbaeumer.vscode-eslint", + // Auto Close Tag + "formulahendry.auto-close-tag", + // Auto Rename Tag + "formulahendry.auto-rename-tag", + // Hight light colors + "naumovs.color-highlight", + // GitLens + "eamodio.gitlens", + // Ruby + "rebornix.ruby", + // Vue + "octref.vetur", + // Prettier + "esbenp.prettier-vscode", + // Dot Env + "mikestead.dotenv", + // HTML CSS Support + "ecmel.vscode-html-css", + // Tailwind CSS Intellisense + "bradlc.vscode-tailwindcss", + ] +} From 1b5a335f93564885f2ab179fa712f798bc5dda65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Kube=C5=A1?= <46596180+KubesDavid@users.noreply.github.com> Date: Wed, 12 Oct 2022 23:00:42 +0200 Subject: [PATCH 30/54] fix: Update .editorconfig to fix spaces and indent_style (#5612) --- .editorconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 7203adb09..2a5fe28cf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,8 +7,8 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -indent_style = spaces +indent_style = space tab_width = 2 -[{*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}] +[*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}] indent_size = 2 From fca629a32afdc98afdaac526f9b240d12560143d Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Thu, 13 Oct 2022 03:02:54 +0530 Subject: [PATCH 31/54] fix: Update timezone to get `wday` from working_hours (#5605) Co-authored-by: Pranav Raj S --- app/models/working_hour.rb | 5 ++++- spec/models/working_hour_spec.rb | 14 ++++++++++++++ spec/services/contacts/filter_service_spec.rb | 1 + spec/services/conversations/filter_service_spec.rb | 1 + 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/models/working_hour.rb b/app/models/working_hour.rb index 885165da2..5ad4d0446 100644 --- a/app/models/working_hour.rb +++ b/app/models/working_hour.rb @@ -40,7 +40,10 @@ class WorkingHour < ApplicationRecord validate :open_all_day_and_closed_all_day def self.today - find_by(day_of_week: Date.current.wday) + # While getting the day of the week, consider the timezone as well. `first` would + # return the first working hour from the list of working hours available per week. + inbox = first.inbox + find_by(day_of_week: Time.zone.now.in_time_zone(inbox.timezone).to_date.wday) end def open_at?(time) diff --git a/spec/models/working_hour_spec.rb b/spec/models/working_hour_spec.rb index c126f6f87..a5018a28e 100644 --- a/spec/models/working_hour_spec.rb +++ b/spec/models/working_hour_spec.rb @@ -88,4 +88,18 @@ RSpec.describe WorkingHour do 'Validation failed: open_all_day and closed_all_day cannot be true at the same time') end end + + context 'when on monday 9am in Sydney timezone' do + let(:inbox) { create(:inbox) } + + before do + Time.zone = 'Australia/Sydney' + inbox.update(timezone: 'Australia/Sydney') + travel_to '10.10.2022 9:00 AEDT' + end + + it 'is considered working hour' do + expect(described_class.today.open_now?).to be true + end + end end diff --git a/spec/services/contacts/filter_service_spec.rb b/spec/services/contacts/filter_service_spec.rb index 5f52a1a3c..7e6661aed 100644 --- a/spec/services/contacts/filter_service_spec.rb +++ b/spec/services/contacts/filter_service_spec.rb @@ -167,6 +167,7 @@ describe ::Contacts::FilterService do context 'with x_days_before filter' do before do + Time.zone = 'UTC' el_contact.update(last_activity_at: (Time.zone.today - 4.days)) cs_contact.update(last_activity_at: (Time.zone.today - 5.days)) en_contact.update(last_activity_at: (Time.zone.today - 2.days)) diff --git a/spec/services/conversations/filter_service_spec.rb b/spec/services/conversations/filter_service_spec.rb index ed489bd30..a17351ec3 100644 --- a/spec/services/conversations/filter_service_spec.rb +++ b/spec/services/conversations/filter_service_spec.rb @@ -309,6 +309,7 @@ describe ::Conversations::FilterService do context 'with x_days_before filter' do before do + Time.zone = 'UTC' en_conversation_1.update!(last_activity_at: (Time.zone.today - 4.days)) en_conversation_2.update!(last_activity_at: (Time.zone.today - 5.days)) user_2_assigned_conversation.update!(last_activity_at: (Time.zone.today - 2.days)) From 0c8f744c33b5d2fc691cab43755285fd671fb1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Kube=C5=A1?= <46596180+KubesDavid@users.noreply.github.com> Date: Wed, 12 Oct 2022 23:42:06 +0200 Subject: [PATCH 32/54] chore: Remove unnecessary methods and polyfills (#5614) --- app/javascript/packs/sdk.js | 10 ++--- app/javascript/sdk/DOMHelpers.js | 58 +++-------------------------- app/javascript/sdk/IFrameHelper.js | 15 ++++---- app/javascript/sdk/bubbleHelpers.js | 14 +++---- 4 files changed, 24 insertions(+), 73 deletions(-) diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index 064033841..2fdd6c57b 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -10,7 +10,7 @@ import { getUserCookieName, hasUserKeys, } from '../sdk/cookieHelpers'; -import { addClass, removeClass } from '../sdk/DOMHelpers'; +import { addClasses, removeClasses } from '../sdk/DOMHelpers'; import { SDK_SET_BUBBLE_VISIBILITY } from 'shared/constants/sharedFrameEvents'; const runSDK = ({ baseUrl, websiteToken }) => { if (window.$chatwoot) { @@ -41,12 +41,12 @@ const runSDK = ({ baseUrl, websiteToken }) => { let widgetElm = document.querySelector('.woot--bubble-holder'); let widgetHolder = document.querySelector('.woot-widget-holder'); if (visibility === 'hide') { - addClass(widgetHolder, 'woot-widget--without-bubble'); - addClass(widgetElm, 'woot-hidden'); + addClasses(widgetHolder, 'woot-widget--without-bubble'); + addClasses(widgetElm, 'woot-hidden'); window.$chatwoot.hideMessageBubble = true; } else if (visibility === 'show') { - removeClass(widgetElm, 'woot-hidden'); - removeClass(widgetHolder, 'woot-widget--without-bubble'); + removeClasses(widgetElm, 'woot-hidden'); + removeClasses(widgetHolder, 'woot-widget--without-bubble'); window.$chatwoot.hideMessageBubble = false; } IFrameHelper.sendMessage(SDK_SET_BUBBLE_VISIBILITY, { diff --git a/app/javascript/sdk/DOMHelpers.js b/app/javascript/sdk/DOMHelpers.js index 2ac6188e6..47a45cc78 100644 --- a/app/javascript/sdk/DOMHelpers.js +++ b/app/javascript/sdk/DOMHelpers.js @@ -3,68 +3,20 @@ import { IFrameHelper } from './IFrameHelper'; export const loadCSS = () => { const css = document.createElement('style'); - css.type = 'text/css'; css.innerHTML = `${SDK_CSS}`; document.body.appendChild(css); }; -export const wootOn = (elm, event, fn) => { - if (document.addEventListener) { - elm.addEventListener(event, fn, false); - } else if (document.attachEvent) { - // <= IE 8 loses scope so need to apply, we add this to object so we - // can detach later (can't detach anonymous functions) - // eslint-disable-next-line - elm[event + fn] = function() { - // eslint-disable-next-line - return fn.apply(elm, arguments); - }; - elm.attachEvent(`on${event}`, elm[event + fn]); - } -}; - -export const classHelper = (classes, action, elm) => { - let search; - let replace; - let i; - let has = false; - if (classes) { - // Trim any whitespace - const classarray = classes.split(/\s+/); - for (i = 0; i < classarray.length; i += 1) { - search = new RegExp(`\\b${classarray[i]}\\b`, 'g'); - replace = new RegExp(` *${classarray[i]}\\b`, 'g'); - if (action === 'remove') { - // eslint-disable-next-line - elm.className = elm.className.replace(replace, ''); - } else if (action === 'toggle') { - // eslint-disable-next-line - elm.className = elm.className.match(search) - ? elm.className.replace(replace, '') - : `${elm.className} ${classarray[i]}`; - } else if (action === 'has') { - if (elm.className.match(search)) { - has = true; - break; - } - } - } - } - return has; -}; - -export const addClass = (elm, classes) => { - if (classes) { - elm.className += ` ${classes}`; - } +export const addClasses = (elm, classes) => { + elm.classList.add(...classes.split(' ')); }; export const toggleClass = (elm, classes) => { - classHelper(classes, 'toggle', elm); + elm.classList.toggle(classes); }; -export const removeClass = (elm, classes) => { - classHelper(classes, 'remove', elm); +export const removeClasses = (elm, classes) => { + elm.classList.remove(...classes.split(' ')); }; export const onLocationChange = ({ referrerURL, referrerHost }) => { diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index 47596bce3..bbc2cfafd 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -1,9 +1,8 @@ import Cookies from 'js-cookie'; import { - wootOn, - addClass, + addClasses, loadCSS, - removeClass, + removeClasses, onLocationChangeListener, } from './DOMHelpers'; import { @@ -68,7 +67,7 @@ export const IFrameHelper = { holderClassName += ` woot-widget-holder--flat`; } - addClass(widgetHolder, holderClassName); + addClasses(widgetHolder, holderClassName); widgetHolder.appendChild(iframe); body.appendChild(widgetHolder); IFrameHelper.initPostMessageCommunication(); @@ -99,7 +98,7 @@ export const IFrameHelper = { }; }, initWindowSizeListener: () => { - wootOn(window, 'resize', () => IFrameHelper.toggleCloseButton()); + window.addEventListener('resize', () => IFrameHelper.toggleCloseButton()); }, preventDefaultScroll: () => { widgetHolder.addEventListener('wheel', event => { @@ -241,9 +240,9 @@ export const IFrameHelper = { event.unreadMessageCount > 0 && !bubbleElement.classList.contains('unread-notification') ) { - addClass(bubbleElement, 'unread-notification'); + addClasses(bubbleElement, 'unread-notification'); } else if (event.unreadMessageCount === 0) { - removeClass(bubbleElement, 'unread-notification'); + removeClasses(bubbleElement, 'unread-notification'); } }, @@ -284,7 +283,7 @@ export const IFrameHelper = { target: chatBubble, }); - addClass(closeBubble, closeBtnClassName); + addClasses(closeBubble, closeBtnClassName); chatIcon.style.background = widgetColor; closeBubble.style.background = widgetColor; diff --git a/app/javascript/sdk/bubbleHelpers.js b/app/javascript/sdk/bubbleHelpers.js index b1ef9110f..5eab20f5d 100644 --- a/app/javascript/sdk/bubbleHelpers.js +++ b/app/javascript/sdk/bubbleHelpers.js @@ -1,4 +1,4 @@ -import { addClass, removeClass, toggleClass, wootOn } from './DOMHelpers'; +import { addClasses, removeClasses, toggleClass } from './DOMHelpers'; import { IFrameHelper } from './IFrameHelper'; import { isExpandedView } from './settingsHelper'; @@ -41,14 +41,14 @@ export const createBubbleIcon = ({ className, src, target }) => { export const createBubbleHolder = hideMessageBubble => { if (hideMessageBubble) { - addClass(bubbleHolder, 'woot-hidden'); + addClasses(bubbleHolder, 'woot-hidden'); } - addClass(bubbleHolder, 'woot--bubble-holder'); + addClasses(bubbleHolder, 'woot--bubble-holder'); body.appendChild(bubbleHolder); }; export const createNotificationBubble = () => { - addClass(notificationBubble, 'woot--notification'); + addClasses(notificationBubble, 'woot--notification'); return notificationBubble; }; @@ -71,15 +71,15 @@ export const onBubbleClick = (props = {}) => { }; export const onClickChatBubble = () => { - wootOn(bubbleHolder, 'click', onBubbleClick); + bubbleHolder.addEventListener('click', onBubbleClick); }; export const addUnreadClass = () => { const holderEl = document.querySelector('.woot-widget-holder'); - addClass(holderEl, 'has-unread-view'); + addClasses(holderEl, 'has-unread-view'); }; export const removeUnreadClass = () => { const holderEl = document.querySelector('.woot-widget-holder'); - removeClass(holderEl, 'has-unread-view'); + removeClasses(holderEl, 'has-unread-view'); }; From 6c048626d0a8b8fc38e7e9244d2ac7de399a26bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Kube=C5=A1?= <46596180+KubesDavid@users.noreply.github.com> Date: Wed, 12 Oct 2022 23:55:59 +0200 Subject: [PATCH 33/54] chore: Replace deprecated functions (#5611) Co-authored-by: Pranav Raj S --- app/javascript/dashboard/components/widgets/Avatar.vue | 2 +- .../dashboard/components/widgets/conversation/ReplyBox.vue | 2 +- .../dashboard/conversation/contact/ConversationForm.vue | 2 +- .../routes/dashboard/settings/inbox/WidgetBuilder.vue | 2 +- app/javascript/shared/mixins/campaignMixin.js | 4 ++-- app/javascript/survey/views/Response.vue | 2 +- app/models/working_hour.rb | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/Avatar.vue b/app/javascript/dashboard/components/widgets/Avatar.vue index daf044288..df654ee7e 100644 --- a/app/javascript/dashboard/components/widgets/Avatar.vue +++ b/app/javascript/dashboard/components/widgets/Avatar.vue @@ -78,7 +78,7 @@ export default { if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) { initials = initials.replace(/[a-z]+/g, ''); } - initials = initials.substr(0, 2).toUpperCase(); + initials = initials.substring(0, 2).toUpperCase(); return initials; }, }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 1b0b9ca36..bbc2f017e 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -477,7 +477,7 @@ export default { const hasNextWord = updatedMessage.includes(' '); const isShortCodeActive = this.hasSlashCommand && !hasNextWord; if (isShortCodeActive) { - this.mentionSearchKey = updatedMessage.substr(1, updatedMessage.length); + this.mentionSearchKey = updatedMessage.substring(1); this.showMentions = true; } else { this.mentionSearchKey = ''; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue index 9dda5c02c..c9294aa2a 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue @@ -239,7 +239,7 @@ export default { const hasNextWord = value.includes(' '); const isShortCodeActive = this.hasSlashCommand && !hasNextWord; if (isShortCodeActive) { - this.cannedResponseSearchKey = value.substr(1, value.length); + this.cannedResponseSearchKey = value.substring(1); this.showCannedResponseMenu = true; } else { this.cannedResponseSearchKey = ''; diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue index 8da5183d7..bdc20e14e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue @@ -243,7 +243,7 @@ export default { this.$t('INBOX_MGMT.WIDGET_BUILDER.SCRIPT_SETTINGS', { options: JSON.stringify(options), }) + - script.substring(13, script.length) + script.substring(13) ); }, getWidgetViewOptions() { diff --git a/app/javascript/shared/mixins/campaignMixin.js b/app/javascript/shared/mixins/campaignMixin.js index b9353ff29..ebf98aa37 100644 --- a/app/javascript/shared/mixins/campaignMixin.js +++ b/app/javascript/shared/mixins/campaignMixin.js @@ -1,10 +1,10 @@ import { CAMPAIGN_TYPES } from '../constants/campaign'; + export default { computed: { campaignType() { const pageURL = window.location.href; - const type = pageURL.substr(pageURL.lastIndexOf('/') + 1); - return type; + return pageURL.substring(pageURL.lastIndexOf('/') + 1); }, isOngoingType() { return this.campaignType === CAMPAIGN_TYPES.ONGOING; diff --git a/app/javascript/survey/views/Response.vue b/app/javascript/survey/views/Response.vue index 6ec4ec100..828a9cbee 100644 --- a/app/javascript/survey/views/Response.vue +++ b/app/javascript/survey/views/Response.vue @@ -93,7 +93,7 @@ export default { computed: { surveyId() { const pageURL = window.location.href; - return pageURL.substr(pageURL.lastIndexOf('/') + 1); + return pageURL.substring(pageURL.lastIndexOf('/') + 1); }, isRatingSubmitted() { return this.surveyDetails && this.surveyDetails.rating; diff --git a/app/models/working_hour.rb b/app/models/working_hour.rb index 5ad4d0446..01b972bd4 100644 --- a/app/models/working_hour.rb +++ b/app/models/working_hour.rb @@ -40,7 +40,7 @@ class WorkingHour < ApplicationRecord validate :open_all_day_and_closed_all_day def self.today - # While getting the day of the week, consider the timezone as well. `first` would + # While getting the day of the week, consider the timezone as well. `first` would # return the first working hour from the list of working hours available per week. inbox = first.inbox find_by(day_of_week: Time.zone.now.in_time_zone(inbox.timezone).to_date.wday) From ee520bdf982186eec0c74f32741a61363b50cdf4 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Thu, 13 Oct 2022 04:52:44 +0530 Subject: [PATCH 34/54] feat: Show last active portal articles when sidebar portal icon is clicked (#5616) --- .../layout/config/sidebarItems/primaryMenu.js | 2 +- .../components/HelpCenterLayout.vue | 11 +++++-- .../helpcenter/components/PortalListItem.vue | 8 ++++- .../dashboard/helpcenter/helpcenter.routes.js | 8 +++++ .../pages/articles/DefaultPortalArticles.vue | 31 +++++++++++++++++++ 5 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/DefaultPortalArticles.vue diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js b/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js index cc4287503..fa4633ec1 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js @@ -39,7 +39,7 @@ const primaryMenuItems = accountId => [ label: 'HELP_CENTER.TITLE', featureFlag: 'help_center', toState: frontendURL(`accounts/${accountId}/portals`), - toStateName: 'list_all_portals', + toStateName: 'default_portal_articles', roles: ['administrator'], }, { diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue index 528b55575..562d6bd07 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue @@ -59,6 +59,7 @@ import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue'; import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue'; import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal'; import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel'; +import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import portalMixin from '../mixins/portalMixin'; import AddCategory from '../pages/categories/AddCategory'; @@ -72,7 +73,7 @@ export default { PortalPopover, AddCategory, }, - mixins: [portalMixin], + mixins: [portalMixin, uiSettingsMixin], data() { return { isSidebarOpen: false, @@ -231,7 +232,13 @@ export default { }, updated() { const slug = this.$route.params.portalSlug; - if (slug) this.lastActivePortalSlug = slug; + if (slug) { + this.lastActivePortalSlug = slug; + this.updateUISettings({ + last_active_portal_slug: slug, + last_active_locale_code: this.selectedLocaleInPortal, + }); + } }, methods: { handleResize() { diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue index e0e002002..0354ad22b 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue @@ -188,13 +188,15 @@ From d2fd05ee4e18701fcc10ba95e1404d4af364abef Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Wed, 12 Oct 2022 21:45:28 -0700 Subject: [PATCH 35/54] fix: Show webhook url only on WhatsApp inbox (#5618) --- .../routes/dashboard/settings/inbox/FinishSetup.vue | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue index 01c46f5dc..7931016e4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue @@ -19,15 +19,11 @@ :script="currentInbox.callback_webhook_url" />
-
+

{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_URL') }}

- +

{{ $t( @@ -36,7 +32,6 @@ }}

From 8f4944fda065e4e6282930ff76bdfc18798b0984 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Thu, 13 Oct 2022 13:46:28 +0530 Subject: [PATCH 36/54] chore: revert arm64 docker build in gh action (#5619) ref: https://github.com/chatwoot/chatwoot/pull/5575 https://github.com/chatwoot/chatwoot/pull/5575#issuecomment-1277208625 --- .github/workflows/publish_foss_docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml index aa1b15df2..2ddaba7e5 100644 --- a/.github/workflows/publish_foss_docker.yml +++ b/.github/workflows/publish_foss_docker.yml @@ -58,6 +58,6 @@ jobs: with: context: . file: docker/Dockerfile - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: true tags: ${{ env.DOCKER_TAG }} From a533f43fbf5ed6273ad3e0026d87d2d2a934c83e Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Fri, 14 Oct 2022 02:01:49 +0530 Subject: [PATCH 37/54] fix: Stop raising errors for unsupported Whatsapp messages (#5541) - Handle unsupported Whatsapp messages --- .../whatsapp/incoming_message_base_service.rb | 6 ++- .../jobs/webhooks/whatsapp_events_job_spec.rb | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/app/services/whatsapp/incoming_message_base_service.rb b/app/services/whatsapp/incoming_message_base_service.rb index e55d5fd26..40dafaced 100644 --- a/app/services/whatsapp/incoming_message_base_service.rb +++ b/app/services/whatsapp/incoming_message_base_service.rb @@ -12,7 +12,7 @@ class Whatsapp::IncomingMessageBaseService set_conversation - return if @processed_params[:messages].blank? + return if @processed_params[:messages].blank? || unprocessable_message_type? @message = @conversation.messages.build( content: message_content(@processed_params[:messages].first), @@ -86,6 +86,10 @@ class Whatsapp::IncomingMessageBaseService @processed_params[:messages].first[:type] end + def unprocessable_message_type? + %w[reaction contacts].include?(message_type) + end + def attach_files return if %w[text button interactive].include?(message_type) diff --git a/spec/jobs/webhooks/whatsapp_events_job_spec.rb b/spec/jobs/webhooks/whatsapp_events_job_spec.rb index 2a28fe41f..00fde3d41 100644 --- a/spec/jobs/webhooks/whatsapp_events_job_spec.rb +++ b/spec/jobs/webhooks/whatsapp_events_job_spec.rb @@ -62,6 +62,59 @@ RSpec.describe Webhooks::WhatsappEventsJob, type: :job do job.perform_now(wb_params) end + it 'Ignore reaction type message and stop raising error' do + other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false, + validate_provider_config: false) + wb_params = { + phone_number: channel.phone_number, + object: 'whatsapp_business_account', + entry: [{ + changes: [{ + value: { + contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }], + messages: [{ + from: '1111981136571', reaction: { emoji: '👍' }, timestamp: '1664799904', type: 'reaction' + }], + metadata: { + phone_number_id: other_channel.provider_config['phone_number_id'], + display_phone_number: other_channel.phone_number.delete('+') + } + } + }] + }] + }.with_indifferent_access + expect do + Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform + end.not_to change(Message, :count) + end + + it 'Ignore contacts type message and stop raising error' do + other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false, + validate_provider_config: false) + wb_params = { + phone_number: channel.phone_number, + object: 'whatsapp_business_account', + entry: [{ + changes: [{ + value: { + contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }], + messages: [{ from: '1111981136571', + contacts: [{ phones: [{ phone: '+1987654' }], name: { first_name: 'contact name' } }], + timestamp: '1664799904', + type: 'contacts' }], + metadata: { + phone_number_id: other_channel.provider_config['phone_number_id'], + display_phone_number: other_channel.phone_number.delete('+') + } + } + }] + }] + }.with_indifferent_access + expect do + Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform + end.not_to change(Message, :count) + end + it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) From bf4338ef9e3fc7637f0d8d3549afc7a002873e47 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Fri, 14 Oct 2022 02:05:11 +0530 Subject: [PATCH 38/54] feat: Make category name in article table clickable (#5626) Co-authored-by: Pranav Raj S --- .../helpcenter/components/ArticleItem.vue | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue index cecf1f39e..b602baaa8 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue @@ -16,7 +16,12 @@
- {{ category.name }} + + {{ category.name }} + @@ -43,6 +48,8 @@ From 1f271356ca065e6a9a0169503cababad240cd3bc Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Fri, 14 Oct 2022 02:06:42 +0530 Subject: [PATCH 39/54] feat: Update the design for dropdown buttons (#5625) --- .../assets/scss/widgets/_buttons.scss | 17 +++++++++++- .../dashboard/assets/scss/widgets/_modal.scss | 10 ++----- app/javascript/dashboard/components/Modal.vue | 10 ++++--- .../components/ui/MultiselectDropdown.vue | 26 ++++++++++++++++--- .../ui/MultiselectDropdownItems.vue | 13 +++++----- .../components/ui/dropdown/DropdownItem.vue | 8 ++---- 6 files changed, 57 insertions(+), 27 deletions(-) diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 9d3f84b25..478045000 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -113,9 +113,22 @@ $default-button-height: 4.0rem; } &.clear { + color: var(--w-700); + + &.secondary { + color: var(--s-700) + } + + &.success { + color: var(--g-700) + } + + &.alert { + color: var(--r-700) + } &.warning { - color: var(--y-600); + color: var(--y-700) } &:hover { @@ -146,6 +159,8 @@ $default-button-height: 4.0rem; &.small { height: var(--space-large); + padding-bottom: var(--space-smaller); + padding-top: var(--space-smaller); } &.large { diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss index fc497f069..543a60797 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss @@ -14,15 +14,9 @@ } .modal--close { - border-radius: 50%; - color: $color-heading; - cursor: pointer; - font-size: $font-size-big; - line-height: $space-normal; - padding: $space-normal; position: absolute; - right: $space-micro; - top: $space-micro; + right: $space-small; + top: $space-small; &:hover { background: $color-background; diff --git a/app/javascript/dashboard/components/Modal.vue b/app/javascript/dashboard/components/Modal.vue index f4cad844a..d5dd55ffa 100644 --- a/app/javascript/dashboard/components/Modal.vue +++ b/app/javascript/dashboard/components/Modal.vue @@ -7,9 +7,13 @@ @click="onBackDropClick" >
- +
diff --git a/app/javascript/shared/components/ui/MultiselectDropdown.vue b/app/javascript/shared/components/ui/MultiselectDropdown.vue index 76908c380..1636ca252 100644 --- a/app/javascript/shared/components/ui/MultiselectDropdown.vue +++ b/app/javascript/shared/components/ui/MultiselectDropdown.vue @@ -41,9 +41,18 @@ :class="{ 'dropdown-pane--open': showSearchDropdown }" class="dropdown-pane" > -

- {{ multiselectorTitle }} -

+ diff --git a/app/javascript/shared/components/ui/MultiselectDropdownItems.vue b/app/javascript/shared/components/ui/MultiselectDropdownItems.vue index 8aadcb7cb..96aa99323 100644 --- a/app/javascript/shared/components/ui/MultiselectDropdownItems.vue +++ b/app/javascript/shared/components/ui/MultiselectDropdownItems.vue @@ -20,6 +20,7 @@ .dropdown-menu__item { list-style: none; + margin-bottom: var(--space-micro); ::v-deep { a, .button { + display: inline-flex; width: 100%; text-align: left; color: var(--s-700); - white-space: nowrap; - display: inline-flex; - padding: var(--space-small); - padding-top: var(--space-small); - padding-bottom: var(--space-small); - border-radius: var(--border-radius-normal); &:hover { background: var(--color-background); From e310230f624df59f99c4c602129d29652bb26c5a Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 13 Oct 2022 15:12:04 -0700 Subject: [PATCH 40/54] chore: Refactor Contact Inbox Builders (#5617) - Remove duplicate code and move everything to builders - fixes: #4680 --- .rubocop.yml | 1 + app/builders/contact_inbox_builder.rb | 43 ++-- ... => contact_inbox_with_contact_builder.rb} | 56 +++-- .../messages/facebook/message_builder.rb | 32 +-- .../contacts/contact_inboxes_controller.rb | 7 +- .../api/v1/accounts/contacts_controller.rb | 7 +- .../v1/accounts/conversations_controller.rb | 27 ++- .../concerns/request_exception_handler.rb | 2 + .../api/v1/inboxes/contacts_controller.rb | 2 +- app/mailboxes/mailbox_helper.rb | 2 +- app/models/channel/facebook_page.rb | 15 +- app/models/channel/twitter_profile.rb | 15 +- app/models/channel/web_widget.rb | 18 +- app/services/line/incoming_message_service.rb | 2 +- app/services/sms/incoming_message_service.rb | 2 +- .../telegram/incoming_message_service.rb | 2 +- .../twilio/incoming_message_service.rb | 2 +- .../whatsapp/incoming_message_base_service.rb | 2 +- db/seeds.rb | 9 +- spec/builders/contact_inbox_builder_spec.rb | 196 +++++++++--------- ...ontact_inbox_with_contact_builder_spec.rb} | 2 +- 21 files changed, 234 insertions(+), 210 deletions(-) rename app/builders/{contact_builder.rb => contact_inbox_with_contact_builder.rb} (51%) rename spec/builders/{contact_builder_spec.rb => contact_inbox_with_contact_builder_spec.rb} (98%) diff --git a/.rubocop.yml b/.rubocop.yml index dafd9a620..3665ad2e3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,6 +16,7 @@ Metrics/ClassLength: - 'app/models/message.rb' - 'app/builders/messages/facebook/message_builder.rb' - 'app/controllers/api/v1/accounts/contacts_controller.rb' + - 'app/controllers/api/v1/accounts/conversations_controller.rb' - 'app/listeners/action_cable_listener.rb' - 'app/models/conversation.rb' RSpec/ExampleLength: diff --git a/app/builders/contact_inbox_builder.rb b/app/builders/contact_inbox_builder.rb index 1b8782c51..8fcd2b158 100644 --- a/app/builders/contact_inbox_builder.rb +++ b/app/builders/contact_inbox_builder.rb @@ -1,13 +1,12 @@ +# This Builder will create a contact inbox with specified attributes. If the contact inbox already exists, it will be returned. +# For Specific Channels like whatsapp, email etc . it smartly generated appropriate the source id when none is provided. + class ContactInboxBuilder - pattr_initialize [:contact_id!, :inbox_id!, :source_id] + pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }] def perform - @contact = Contact.find(contact_id) - @inbox = @contact.account.inboxes.find(inbox_id) - return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type - - source_id = @source_id || generate_source_id - create_contact_inbox(source_id) if source_id.present? + @source_id ||= generate_source_id + create_contact_inbox if source_id.present? end private @@ -19,23 +18,37 @@ class ContactInboxBuilder when 'Channel::Whatsapp' wa_source_id when 'Channel::Email' - @contact.email + email_source_id when 'Channel::Sms' - @contact.phone_number - when 'Channel::Api' + phone_source_id + when 'Channel::Api', 'Channel::WebWidget' SecureRandom.uuid + else + raise "Unsupported operation for this channel: #{@inbox.channel_type}" end end + def email_source_id + raise ActionController::ParameterMissing, 'contact email' unless @contact.email + + @contact.email + end + + def phone_source_id + raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number + + @contact.phone_number + end + def wa_source_id - return unless @contact.phone_number + raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number # whatsapp doesn't want the + in e164 format @contact.phone_number.delete('+').to_s end def twilio_source_id - return unless @contact.phone_number + raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number case @inbox.channel.medium when 'sms' @@ -45,11 +58,11 @@ class ContactInboxBuilder end end - def create_contact_inbox(source_id) - ::ContactInbox.find_or_create_by!( + def create_contact_inbox + ::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!( contact_id: @contact.id, inbox_id: @inbox.id, - source_id: source_id + source_id: @source_id ) end end diff --git a/app/builders/contact_builder.rb b/app/builders/contact_inbox_with_contact_builder.rb similarity index 51% rename from app/builders/contact_builder.rb rename to app/builders/contact_inbox_with_contact_builder.rb index 938072643..d97f64cfe 100644 --- a/app/builders/contact_builder.rb +++ b/app/builders/contact_inbox_with_contact_builder.rb @@ -1,25 +1,47 @@ -class ContactBuilder - pattr_initialize [:source_id!, :inbox!, :contact_attributes!, :hmac_verified] +# This Builder will create a contact and contact inbox with specified attributes. +# If an existing identified contact exisits, it will be returned. +# for contact inbox logic it uses the contact inbox builder + +class ContactInboxWithContactBuilder + pattr_initialize [:inbox!, :contact_attributes!, :source_id, :hmac_verified] def perform - contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) - return contact_inbox if contact_inbox + find_or_create_contact_and_contact_inbox + # in case of race conditions where contact is created by another thread + # we will try to find the contact and create a contact inbox + rescue ActiveRecord::RecordNotUnique + find_or_create_contact_and_contact_inbox + end - build_contact_inbox + def find_or_create_contact_and_contact_inbox + @contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) if source_id.present? + return @contact_inbox if @contact_inbox + + ActiveRecord::Base.transaction(requires_new: true) do + build_contact_with_contact_inbox + update_contact_avatar(@contact) unless @contact.avatar.attached? + @contact_inbox + end end private + def build_contact_with_contact_inbox + @contact = find_contact || create_contact + @contact_inbox = create_contact_inbox + end + def account @account ||= inbox.account end - def create_contact_inbox(contact) - ::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!( - contact_id: contact.id, - inbox_id: inbox.id, - source_id: source_id - ) + def create_contact_inbox + ContactInboxBuilder.new( + contact: @contact, + inbox: @inbox, + source_id: @source_id, + hmac_verified: hmac_verified + ).perform end def update_contact_avatar(contact) @@ -61,16 +83,4 @@ class ContactBuilder account.contacts.find_by(phone_number: phone_number) end - - def build_contact_inbox - ActiveRecord::Base.transaction do - contact = find_contact || create_contact - contact_inbox = create_contact_inbox(contact) - update_contact_avatar(contact) - contact_inbox - rescue StandardError => e - Rails.logger.error e - raise e - end - end end diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index 9f670602a..42d567e54 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -22,10 +22,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder return if @inbox.channel.reauthorization_required? ActiveRecord::Base.transaction do - build_contact + build_contact_inbox build_message end - ensure_contact_avatar rescue Koala::Facebook::AuthenticationError @inbox.channel.authorization_error! rescue StandardError => e @@ -35,15 +34,12 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder private - def contact - @contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact - end - - def build_contact - return if contact.present? - - @contact = Contact.create!(contact_params.except(:remote_avatar_url)) - @contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id) + def build_contact_inbox + @contact_inbox = ::ContactInboxWithContactBuilder.new( + source_id: @sender_id, + inbox: @inbox, + contact_attributes: contact_params + ).perform end def build_message @@ -54,19 +50,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder end end - def ensure_contact_avatar - return if contact_params[:remote_avatar_url].blank? - return if @contact.avatar.attached? - - Avatar::AvatarFromUrlJob.perform_later(@contact, contact_params[:remote_avatar_url]) - end - def conversation @conversation ||= Conversation.find_by(conversation_params) || build_conversation end def build_conversation - @contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id) Conversation.create!(conversation_params.merge( contact_inbox_id: @contact_inbox.id )) @@ -94,7 +82,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder { account_id: @inbox.account_id, inbox_id: @inbox.id, - contact_id: contact.id + contact_id: @contact_inbox.contact_id } end @@ -105,7 +93,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder message_type: @message_type, content: response.content, source_id: response.identifier, - sender: @outgoing_echo ? nil : contact + sender: @outgoing_echo ? nil : @contact_inbox.contact } end @@ -113,7 +101,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder { name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}", account_id: @inbox.account_id, - remote_avatar_url: result['profile_pic'] || '' + avatar_url: result['profile_pic'] } end diff --git a/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb b/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb index fdcdcaf9e..b4287ae08 100644 --- a/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb @@ -2,8 +2,11 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts: before_action :ensure_inbox, only: [:create] def create - source_id = params[:source_id] || SecureRandom.uuid - @contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id) + @contact_inbox = ContactInboxBuilder.new( + contact: @contact, + inbox: @inbox, + source_id: params[:source_id] + ).perform end private diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 1c56e9c04..b86b973df 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -134,8 +134,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController return if params[:inbox_id].blank? inbox = Current.account.inboxes.find(params[:inbox_id]) - source_id = params[:source_id] || SecureRandom.uuid - ContactInbox.create!(contact: @contact, inbox: inbox, source_id: source_id) + ContactInboxBuilder.new( + contact: @contact, + inbox: inbox, + source_id: params[:source_id] + ).perform end def permitted_params diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 0515eabca..8734a3dd4 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro include DateRangeHelper before_action :conversation, except: [:index, :meta, :search, :create, :filter] - before_action :contact_inbox, only: [:create] + before_action :inbox, :contact, :contact_inbox, only: [:create] def index result = conversation_finder.perform @@ -109,22 +109,35 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro authorize @conversation.inbox, :show? end + def inbox + return if params[:inbox_id].blank? + + @inbox = Current.account.inboxes.find(params[:inbox_id]) + authorize @inbox, :show? + end + + def contact + return if params[:contact_id].blank? + + @contact = Current.account.contacts.find(params[:contact_id]) + end + def contact_inbox @contact_inbox = build_contact_inbox + # fallback for the old case where we do look up only using source id + # In future we need to change this and make sure we do look up on combination of inbox_id and source_id + # and deprecate the support of passing only source_id as the param @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id]) authorize @contact_inbox.inbox, :show? end def build_contact_inbox - return if params[:contact_id].blank? || params[:inbox_id].blank? - - inbox = Current.account.inboxes.find(params[:inbox_id]) - authorize inbox, :show? + return if @inbox.blank? || @contact.blank? ContactInboxBuilder.new( - contact_id: params[:contact_id], - inbox_id: inbox.id, + contact: @contact, + inbox: @inbox, source_id: params[:source_id] ).perform end diff --git a/app/controllers/concerns/request_exception_handler.rb b/app/controllers/concerns/request_exception_handler.rb index 6e9ed04cc..2f53fdc2b 100644 --- a/app/controllers/concerns/request_exception_handler.rb +++ b/app/controllers/concerns/request_exception_handler.rb @@ -13,6 +13,8 @@ module RequestExceptionHandler render_not_found_error('Resource could not be found') rescue Pundit::NotAuthorizedError render_unauthorized('You are not authorized to do this action') + rescue ActionController::ParameterMissing => e + render_could_not_create_error(e.message) ensure # to address the thread variable leak issues in Puma/Thin webserver Current.reset diff --git a/app/controllers/public/api/v1/inboxes/contacts_controller.rb b/app/controllers/public/api/v1/inboxes/contacts_controller.rb index eb794f2a0..1fde3051e 100644 --- a/app/controllers/public/api/v1/inboxes/contacts_controller.rb +++ b/app/controllers/public/api/v1/inboxes/contacts_controller.rb @@ -4,7 +4,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon def create source_id = params[:source_id] || SecureRandom.uuid - @contact_inbox = ::ContactBuilder.new( + @contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: source_id, inbox: @inbox_channel.inbox, contact_attributes: permitted_params.except(:identifier, :identifier_hash) diff --git a/app/mailboxes/mailbox_helper.rb b/app/mailboxes/mailbox_helper.rb index 1519343ca..216d5c2c3 100644 --- a/app/mailboxes/mailbox_helper.rb +++ b/app/mailboxes/mailbox_helper.rb @@ -34,7 +34,7 @@ module MailboxHelper end def create_contact - @contact_inbox = ::ContactBuilder.new( + @contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: processed_mail.original_sender, inbox: @inbox, contact_attributes: { diff --git a/app/models/channel/facebook_page.rb b/app/models/channel/facebook_page.rb index 2153708e2..bba45f326 100644 --- a/app/models/channel/facebook_page.rb +++ b/app/models/channel/facebook_page.rb @@ -37,16 +37,11 @@ class Channel::FacebookPage < ApplicationRecord end def create_contact_inbox(instagram_id, name) - ActiveRecord::Base.transaction do - contact = inbox.account.contacts.create!(name: name) - ::ContactInbox.create!( - contact_id: contact.id, - inbox_id: inbox.id, - source_id: instagram_id - ) - rescue StandardError => e - Rails.logger.error e - end + @contact_inbox = ::ContactInboxWithContactBuilder.new({ + source_id: instagram_id, + inbox: inbox, + contact_attributes: { name: name } + }).perform end def subscribe diff --git a/app/models/channel/twitter_profile.rb b/app/models/channel/twitter_profile.rb index 4f6fa7ba1..d0f765e9f 100644 --- a/app/models/channel/twitter_profile.rb +++ b/app/models/channel/twitter_profile.rb @@ -32,16 +32,11 @@ class Channel::TwitterProfile < ApplicationRecord end def create_contact_inbox(profile_id, name, additional_attributes) - ActiveRecord::Base.transaction do - contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name) - ::ContactInbox.create!( - contact_id: contact.id, - inbox_id: inbox.id, - source_id: profile_id - ) - rescue StandardError => e - Rails.logger.error e - end + ::ContactInboxWithContactBuilder.new({ + source_id: profile_id, + inbox: inbox, + contact_attributes: { name: name, additional_attributes: additional_attributes } + }).perform end def twitter_client diff --git a/app/models/channel/web_widget.rb b/app/models/channel/web_widget.rb index b85443633..59d392892 100644 --- a/app/models/channel/web_widget.rb +++ b/app/models/channel/web_widget.rb @@ -98,19 +98,9 @@ class Channel::WebWidget < ApplicationRecord end def create_contact_inbox(additional_attributes = {}) - ActiveRecord::Base.transaction do - contact = inbox.account.contacts.create!( - name: ::Haikunator.haikunate(1000), - additional_attributes: additional_attributes - ) - contact_inbox = ::ContactInbox.create!( - contact_id: contact.id, - inbox_id: inbox.id, - source_id: SecureRandom.uuid - ) - contact_inbox - rescue StandardError => e - Rails.logger.error e - end + ::ContactInboxWithContactBuilder.new({ + inbox: inbox, + contact_attributes: { additional_attributes: additional_attributes } + }).perform end end diff --git a/app/services/line/incoming_message_service.rb b/app/services/line/incoming_message_service.rb index 535c03dc7..48a52eb8f 100644 --- a/app/services/line/incoming_message_service.rb +++ b/app/services/line/incoming_message_service.rb @@ -81,7 +81,7 @@ class Line::IncomingMessageService end def set_contact - contact_inbox = ::ContactBuilder.new( + contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: line_contact_info['userId'], inbox: inbox, contact_attributes: contact_attributes diff --git a/app/services/sms/incoming_message_service.rb b/app/services/sms/incoming_message_service.rb index 7ee6e3e63..7aa22b19e 100644 --- a/app/services/sms/incoming_message_service.rb +++ b/app/services/sms/incoming_message_service.rb @@ -37,7 +37,7 @@ class Sms::IncomingMessageService end def set_contact - contact_inbox = ::ContactBuilder.new( + contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: params[:from], inbox: @inbox, contact_attributes: contact_attributes diff --git a/app/services/telegram/incoming_message_service.rb b/app/services/telegram/incoming_message_service.rb index a26bda14e..e8ab8a72c 100644 --- a/app/services/telegram/incoming_message_service.rb +++ b/app/services/telegram/incoming_message_service.rb @@ -31,7 +31,7 @@ class Telegram::IncomingMessageService end def set_contact - contact_inbox = ::ContactBuilder.new( + contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: params[:message][:from][:id], inbox: inbox, contact_attributes: contact_attributes diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb index 50c77111c..4473131df 100644 --- a/app/services/twilio/incoming_message_service.rb +++ b/app/services/twilio/incoming_message_service.rb @@ -47,7 +47,7 @@ class Twilio::IncomingMessageService end def set_contact - contact_inbox = ::ContactBuilder.new( + contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: params[:From], inbox: inbox, contact_attributes: contact_attributes diff --git a/app/services/whatsapp/incoming_message_base_service.rb b/app/services/whatsapp/incoming_message_base_service.rb index 40dafaced..da08e02d6 100644 --- a/app/services/whatsapp/incoming_message_base_service.rb +++ b/app/services/whatsapp/incoming_message_base_service.rb @@ -48,7 +48,7 @@ class Whatsapp::IncomingMessageBaseService contact_params = @processed_params[:contacts]&.first return if contact_params.blank? - contact_inbox = ::ContactBuilder.new( + contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: contact_params[:wa_id], inbox: inbox, contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" } diff --git a/db/seeds.rb b/db/seeds.rb index c4c7eda71..ead862669 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -46,8 +46,13 @@ unless Rails.env.production? inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support') InboxMember.create!(user: user, inbox: inbox) - contact = Contact.create!(name: 'jane', email: 'jane@example.com', phone_number: '+2320000', account: account) - contact_inbox = ContactInbox.create!(inbox: inbox, contact: contact, source_id: user.id, hmac_verified: true) + contact = ::ContactInboxWithContactBuilder.new( + source_id: user.id, + inbox: inbox, + hmac_verified: true, + contact_attributes: { name: 'jane', email: 'jane@example.com', phone_number: '+2320000' } + ).perform&.contact + conversation = Conversation.create!( account: account, inbox: inbox, diff --git a/spec/builders/contact_inbox_builder_spec.rb b/spec/builders/contact_inbox_builder_spec.rb index 47210f6ca..46b0d749c 100644 --- a/spec/builders/contact_inbox_builder_spec.rb +++ b/spec/builders/contact_inbox_builder_spec.rb @@ -12,8 +12,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id, + contact: contact, + inbox: twilio_inbox, source_id: contact.phone_number ).perform @@ -23,8 +23,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id + contact: contact, + inbox: twilio_inbox ).perform expect(contact_inbox.id).to eq(existing_contact_inbox.id) @@ -33,8 +33,8 @@ describe ::ContactInboxBuilder do it 'creates a new contact inbox when different source id is provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id, + contact: contact, + inbox: twilio_inbox, source_id: '+224213223422' ).perform @@ -44,12 +44,23 @@ describe ::ContactInboxBuilder do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id + contact: contact, + inbox: twilio_inbox ).perform expect(contact_inbox.source_id).to eq(contact.phone_number) end + + it 'raises error when contact phone number is not present and no source id is provided' do + contact.update!(phone_number: nil) + + expect do + described_class.new( + contact: contact, + inbox: twilio_inbox + ).perform + end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number') + end end describe 'twilio whatsapp inbox' do @@ -59,8 +70,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id, + contact: contact, + inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}" ).perform @@ -70,8 +81,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id + contact: contact, + inbox: twilio_inbox ).perform expect(contact_inbox.id).to eq(existing_contact_inbox.id) @@ -80,8 +91,8 @@ describe ::ContactInboxBuilder do it 'creates a new contact inbox when different source id is provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id, + contact: contact, + inbox: twilio_inbox, source_id: 'whatsapp:+555555' ).perform @@ -91,12 +102,23 @@ describe ::ContactInboxBuilder do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id + contact: contact, + inbox: twilio_inbox ).perform expect(contact_inbox.source_id).to eq("whatsapp:#{contact.phone_number}") end + + it 'raises error when contact phone number is not present and no source id is provided' do + contact.update!(phone_number: nil) + + expect do + described_class.new( + contact: contact, + inbox: twilio_inbox + ).perform + end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number') + end end describe 'whatsapp inbox' do @@ -105,8 +127,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: whatsapp_inbox.id, + contact: contact, + inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+') ).perform @@ -116,8 +138,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: whatsapp_inbox.id + contact: contact, + inbox: whatsapp_inbox ).perform expect(contact_inbox.id).to be(existing_contact_inbox.id) @@ -126,8 +148,8 @@ describe ::ContactInboxBuilder do it 'creates a new contact inbox when different source id is provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: whatsapp_inbox.id, + contact: contact, + inbox: whatsapp_inbox, source_id: '555555' ).perform @@ -137,12 +159,23 @@ describe ::ContactInboxBuilder do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: whatsapp_inbox.id + contact: contact, + inbox: whatsapp_inbox ).perform expect(contact_inbox.source_id).to eq(contact.phone_number&.delete('+')) end + + it 'raises error when contact phone number is not present and no source id is provided' do + contact.update!(phone_number: nil) + + expect do + described_class.new( + contact: contact, + inbox: whatsapp_inbox + ).perform + end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number') + end end describe 'sms inbox' do @@ -152,8 +185,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: sms_inbox.id, + contact: contact, + inbox: sms_inbox, source_id: contact.phone_number ).perform @@ -163,8 +196,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: sms_inbox.id + contact: contact, + inbox: sms_inbox ).perform expect(contact_inbox.id).to eq(existing_contact_inbox.id) @@ -173,8 +206,8 @@ describe ::ContactInboxBuilder do it 'creates a new contact inbox when different source id is provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: sms_inbox.id, + contact: contact, + inbox: sms_inbox, source_id: '+224213223422' ).perform @@ -184,12 +217,23 @@ describe ::ContactInboxBuilder do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: sms_inbox.id + contact: contact, + inbox: sms_inbox ).perform expect(contact_inbox.source_id).to eq(contact.phone_number) end + + it 'raises error when contact phone number is not present and no source id is provided' do + contact.update!(phone_number: nil) + + expect do + described_class.new( + contact: contact, + inbox: sms_inbox + ).perform + end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number') + end end describe 'email inbox' do @@ -199,8 +243,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: email_inbox.id, + contact: contact, + inbox: email_inbox, source_id: contact.email ).perform @@ -210,8 +254,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: email_inbox.id + contact: contact, + inbox: email_inbox ).perform expect(contact_inbox.id).to eq(existing_contact_inbox.id) @@ -220,8 +264,8 @@ describe ::ContactInboxBuilder do it 'creates a new contact inbox when different source id is provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: email_inbox.id, + contact: contact, + inbox: email_inbox, source_id: 'xyc@xyc.com' ).perform @@ -231,12 +275,23 @@ describe ::ContactInboxBuilder do it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: email_inbox.id + contact: contact, + inbox: email_inbox ).perform expect(contact_inbox.source_id).to eq(contact.email) end + + it 'raises error when contact email is not present and no source id is provided' do + contact.update!(email: nil) + + expect do + described_class.new( + contact: contact, + inbox: email_inbox + ).perform + end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact email') + end end describe 'api inbox' do @@ -246,8 +301,8 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: 'test') contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: api_inbox.id, + contact: contact, + inbox: api_inbox, source_id: 'test' ).perform @@ -257,8 +312,8 @@ describe ::ContactInboxBuilder do it 'creates a new contact inbox when different source id is provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: SecureRandom.uuid) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: api_inbox.id, + contact: contact, + inbox: api_inbox, source_id: 'test' ).perform @@ -268,61 +323,12 @@ describe ::ContactInboxBuilder do it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: api_inbox.id + contact: contact, + inbox: api_inbox ).perform expect(contact_inbox.source_id).not_to be_nil end end - - describe 'web widget' do - let!(:website_channel) { create(:channel_widget, account: account) } - let!(:website_inbox) { create(:inbox, channel: website_channel, account: account) } - - it 'does not create contact inbox' do - contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: website_inbox.id, - source_id: 'test' - ).perform - - expect(contact_inbox).to be_nil - end - end - - describe 'facebook inbox' do - before do - stub_request(:post, /graph.facebook.com/) - end - - let!(:facebook_channel) { create(:channel_facebook_page, account: account) } - let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) } - - it 'does not create contact inbox' do - contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: facebook_inbox.id, - source_id: 'test' - ).perform - - expect(contact_inbox).to be_nil - end - end - - describe 'twitter inbox' do - let!(:twitter_channel) { create(:channel_twitter_profile, account: account) } - let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) } - - it 'does not create contact inbox' do - contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twitter_inbox.id, - source_id: 'test' - ).perform - - expect(contact_inbox).to be_nil - end - end end end diff --git a/spec/builders/contact_builder_spec.rb b/spec/builders/contact_inbox_with_contact_builder_spec.rb similarity index 98% rename from spec/builders/contact_builder_spec.rb rename to spec/builders/contact_inbox_with_contact_builder_spec.rb index 29df0da22..e76d199d4 100644 --- a/spec/builders/contact_builder_spec.rb +++ b/spec/builders/contact_inbox_with_contact_builder_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe ::ContactBuilder do +describe ::ContactInboxWithContactBuilder do let(:account) { create(:account) } let(:inbox) { create(:inbox, account: account) } let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account, identifier: '123') } From a6960dc2d38f64f02eb17af88c59ca3f50a24c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Kube=C5=A1?= <46596180+KubesDavid@users.noreply.github.com> Date: Fri, 14 Oct 2022 05:43:11 +0200 Subject: [PATCH 41/54] chore: Refactor widget (#5621) --- app/javascript/widget/api/agent.js | 3 +-- app/javascript/widget/api/campaign.js | 3 +-- app/javascript/widget/api/conversation.js | 12 ++++-------- app/javascript/widget/assets/scss/woot.scss | 5 +---- app/javascript/widget/components/AgentMessage.vue | 3 +-- app/javascript/widget/components/ChatAttachment.vue | 4 ++-- .../widget/components/ChatHeaderExpanded.vue | 7 ++++++- app/javascript/widget/components/ChatInputWrap.vue | 4 +--- app/javascript/widget/components/FileBubble.vue | 3 +-- app/javascript/widget/components/PreChat/Form.vue | 7 ++----- .../widget/components/TeamAvailability.vue | 4 ++-- .../widget/components/UnreadMessageList.vue | 3 +-- app/javascript/widget/helpers/IframeEventHelper.js | 2 +- app/javascript/widget/helpers/utils.js | 11 +---------- .../widget/store/modules/conversation/getters.js | 6 ++---- .../widget/store/modules/conversation/helpers.js | 2 +- .../widget/store/modules/conversation/mutations.js | 3 +-- .../widget/store/modules/conversationLabels.js | 4 ++-- 18 files changed, 31 insertions(+), 55 deletions(-) diff --git a/app/javascript/widget/api/agent.js b/app/javascript/widget/api/agent.js index 0debeccaf..5dceecad7 100644 --- a/app/javascript/widget/api/agent.js +++ b/app/javascript/widget/api/agent.js @@ -3,6 +3,5 @@ import { API } from 'widget/helpers/axios'; export const getAvailableAgents = async websiteToken => { const urlData = endPoints.getAvailableAgents(websiteToken); - const result = await API.get(urlData.url, { params: urlData.params }); - return result; + return API.get(urlData.url, { params: urlData.params }); }; diff --git a/app/javascript/widget/api/campaign.js b/app/javascript/widget/api/campaign.js index 57d81e084..efa154f41 100644 --- a/app/javascript/widget/api/campaign.js +++ b/app/javascript/widget/api/campaign.js @@ -3,8 +3,7 @@ import { API } from 'widget/helpers/axios'; const getCampaigns = async websiteToken => { const urlData = endPoints.getCampaigns(websiteToken); - const result = await API.get(urlData.url, { params: urlData.params }); - return result; + return API.get(urlData.url, { params: urlData.params }); }; const triggerCampaign = async ({ diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js index fdb3842fd..4cf4de25e 100755 --- a/app/javascript/widget/api/conversation.js +++ b/app/javascript/widget/api/conversation.js @@ -3,26 +3,22 @@ import { API } from 'widget/helpers/axios'; const createConversationAPI = async content => { const urlData = endPoints.createConversation(content); - const result = await API.post(urlData.url, urlData.params); - return result; + return API.post(urlData.url, urlData.params); }; const sendMessageAPI = async content => { const urlData = endPoints.sendMessage(content); - const result = await API.post(urlData.url, urlData.params); - return result; + return API.post(urlData.url, urlData.params); }; const sendAttachmentAPI = async attachment => { const urlData = endPoints.sendAttachment(attachment); - const result = await API.post(urlData.url, urlData.params); - return result; + return API.post(urlData.url, urlData.params); }; const getMessagesAPI = async ({ before }) => { const urlData = endPoints.getConversation({ before }); - const result = await API.get(urlData.url, { params: urlData.params }); - return result; + return API.get(urlData.url, { params: urlData.params }); }; const getConversationAPI = async () => { diff --git a/app/javascript/widget/assets/scss/woot.scss b/app/javascript/widget/assets/scss/woot.scss index 9a2a6a8e6..3f882eb38 100755 --- a/app/javascript/widget/assets/scss/woot.scss +++ b/app/javascript/widget/assets/scss/woot.scss @@ -61,10 +61,7 @@ body { .is-flat-design { .chat-bubble { - border-bottom-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; + border-radius: 0 !important; box-shadow: none; } diff --git a/app/javascript/widget/components/AgentMessage.vue b/app/javascript/widget/components/AgentMessage.vue index c338dce83..6b1fb782b 100755 --- a/app/javascript/widget/components/AgentMessage.vue +++ b/app/javascript/widget/components/AgentMessage.vue @@ -104,8 +104,7 @@ export default { ) { return false; } - if (!this.message.content) return false; - return true; + return this.message.content; }, readableTime() { const { created_at: createdAt = '' } = this.message; diff --git a/app/javascript/widget/components/ChatAttachment.vue b/app/javascript/widget/components/ChatAttachment.vue index 87da3f5df..0412ca5fb 100755 --- a/app/javascript/widget/components/ChatAttachment.vue +++ b/app/javascript/widget/components/ChatAttachment.vue @@ -54,9 +54,9 @@ export default { }, async onFileUpload(file) { if (this.globalConfig.directUploadsEnabled) { - this.onDirectFileUpload(file); + await this.onDirectFileUpload(file); } else { - this.onIndirectFileUpload(file); + await this.onIndirectFileUpload(file); } }, async onDirectFileUpload(file) { diff --git a/app/javascript/widget/components/ChatHeaderExpanded.vue b/app/javascript/widget/components/ChatHeaderExpanded.vue index c1823f504..f82f66c46 100755 --- a/app/javascript/widget/components/ChatHeaderExpanded.vue +++ b/app/javascript/widget/components/ChatHeaderExpanded.vue @@ -7,7 +7,12 @@ class="flex items-start" :class="[avatarUrl ? 'justify-between' : 'justify-end']" > - + Avatar

{ - if ( + return !( (isUserEmailAvailable && field.name === 'emailAddress') || (isUserPhoneNumberAvailable && field.name === 'phoneNumber') - ) { - return false; - } - return true; + ); }); }, enabledPreChatFields() { diff --git a/app/javascript/widget/components/TeamAvailability.vue b/app/javascript/widget/components/TeamAvailability.vue index 1f4c5d046..a3a6b0c02 100644 --- a/app/javascript/widget/components/TeamAvailability.vue +++ b/app/javascript/widget/components/TeamAvailability.vue @@ -13,7 +13,7 @@ }}
- {{ replyWaitMeessage }} + {{ replyWaitMessage }}
@@ -75,7 +75,7 @@ export default { } return anyAgentOnline; }, - replyWaitMeessage() { + replyWaitMessage() { const { workingHoursEnabled } = this.channelConfig; if (this.isOnline) { diff --git a/app/javascript/widget/components/UnreadMessageList.vue b/app/javascript/widget/components/UnreadMessageList.vue index 19afe49d1..6683c585e 100644 --- a/app/javascript/widget/components/UnreadMessageList.vue +++ b/app/javascript/widget/components/UnreadMessageList.vue @@ -107,13 +107,12 @@ export default { .clear-button { background: transparent; color: $color-woot; - padding: 0; border: 0; font-weight: $font-weight-bold; font-size: $font-size-medium; transition: all 0.3s var(--ease-in-cubic); margin-left: $space-smaller; - padding-right: $space-one; + padding: 0 $space-one 0 0; &:hover { transform: translateX($space-smaller); diff --git a/app/javascript/widget/helpers/IframeEventHelper.js b/app/javascript/widget/helpers/IframeEventHelper.js index 953802df8..40c0e1f1c 100644 --- a/app/javascript/widget/helpers/IframeEventHelper.js +++ b/app/javascript/widget/helpers/IframeEventHelper.js @@ -10,7 +10,7 @@ export const loadedEventConfig = () => { export const getExtraSpaceToScroll = () => { // This function calculates the extra space needed for the view to - // accomodate the height of close button + height of + // accommodate the height of close button + height of // read messages button. So that scrollbar won't appear const unreadMessageWrap = document.querySelector('.unread-messages'); const unreadCloseWrap = document.querySelector('.close-unread-wrap'); diff --git a/app/javascript/widget/helpers/utils.js b/app/javascript/widget/helpers/utils.js index 1a4b2c1d6..a4d5dafa3 100755 --- a/app/javascript/widget/helpers/utils.js +++ b/app/javascript/widget/helpers/utils.js @@ -3,13 +3,6 @@ import { WOOT_PREFIX } from './constants'; export const isEmptyObject = obj => Object.keys(obj).length === 0 && obj.constructor === Object; -export const arrayToHashById = array => - array.reduce((map, obj) => { - const newMap = map; - newMap[obj.id] = obj; - return newMap; - }, {}); - export const sendMessage = msg => { window.parent.postMessage( `chatwoot-widget:${JSON.stringify({ ...msg })}`, @@ -22,9 +15,7 @@ export const IFrameHelper = { sendMessage, isAValidEvent: e => { const isDataAString = typeof e.data === 'string'; - const isAValidWootEvent = - isDataAString && e.data.indexOf(WOOT_PREFIX) === 0; - return isAValidWootEvent; + return isDataAString && e.data.indexOf(WOOT_PREFIX) === 0; }, getMessage: e => JSON.parse(e.data.replace(WOOT_PREFIX, '')), }; diff --git a/app/javascript/widget/store/modules/conversation/getters.js b/app/javascript/widget/store/modules/conversation/getters.js index 9d1c067f4..74e582348 100644 --- a/app/javascript/widget/store/modules/conversation/getters.js +++ b/app/javascript/widget/store/modules/conversation/getters.js @@ -32,7 +32,7 @@ export const getters = { }, getUnreadMessageCount: _state => { const { userLastSeenAt } = _state.meta; - const count = Object.values(_state.conversations).filter(chat => { + return Object.values(_state.conversations).filter(chat => { const { created_at: createdAt, message_type: messageType } = chat; const isOutGoing = messageType === MESSAGE_TYPE.OUTGOING; const hasNotSeen = userLastSeenAt @@ -40,7 +40,6 @@ export const getters = { : true; return hasNotSeen && isOutGoing; }).length; - return count; }, getUnreadTextMessages: (_state, _getters) => { const unreadCount = _getters.getUnreadMessageCount; @@ -50,7 +49,6 @@ export const getters = { return messageType === MESSAGE_TYPE.OUTGOING; }); const maxUnreadCount = Math.min(unreadCount, 3); - const allUnreadMessages = unreadAgentMessages.splice(-maxUnreadCount); - return allUnreadMessages; + return unreadAgentMessages.splice(-maxUnreadCount); }, }; diff --git a/app/javascript/widget/store/modules/conversation/helpers.js b/app/javascript/widget/store/modules/conversation/helpers.js index 44e2a6729..2ebac5242 100644 --- a/app/javascript/widget/store/modules/conversation/helpers.js +++ b/app/javascript/widget/store/modules/conversation/helpers.js @@ -29,7 +29,7 @@ const shouldShowAvatar = (message, nextMessage) => { export const groupConversationBySender = conversationsForADate => conversationsForADate.map((message, index) => { - let showAvatar = false; + let showAvatar; const isLastMessage = index === conversationsForADate.length - 1; if (isASubmittedFormMessage(message)) { showAvatar = false; diff --git a/app/javascript/widget/store/modules/conversation/mutations.js b/app/javascript/widget/store/modules/conversation/mutations.js index f47971f07..ca6dafada 100644 --- a/app/javascript/widget/store/modules/conversation/mutations.js +++ b/app/javascript/widget/store/modules/conversation/mutations.js @@ -88,8 +88,7 @@ export const mutations = { }, toggleAgentTypingStatus($state, { status }) { - const isTyping = status === 'on'; - $state.uiFlags.isAgentTyping = isTyping; + $state.uiFlags.isAgentTyping = status === 'on'; }, setMetaUserLastSeenAt($state, lastSeen) { diff --git a/app/javascript/widget/store/modules/conversationLabels.js b/app/javascript/widget/store/modules/conversationLabels.js index 3fbcd230d..3ae600082 100644 --- a/app/javascript/widget/store/modules/conversationLabels.js +++ b/app/javascript/widget/store/modules/conversationLabels.js @@ -9,14 +9,14 @@ export const actions = { try { await conversationLabels.create(label); } catch (error) { - // Ingore error + // Ignore error } }, destroy: async (_, label) => { try { await conversationLabels.destroy(label); } catch (error) { - // Ingore error + // Ignore error } }, }; From db53af91e740f9a0efecf855d9da63250d1be4d2 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 14 Oct 2022 12:46:41 -0700 Subject: [PATCH 42/54] chore (#5636) * New translations conversation.json (Latvian) * New translations bulkActions.json (Portuguese, Brazilian) * New translations agentMgmt.json (Latvian) * New translations teamsSettings.json (Latvian) * New translations contactFilters.json (Latvian) * New translations automation.json (Latvian) * New translations attributesMgmt.json (Latvian) * New translations labelsMgmt.json (Latvian) * New translations settings.json (Latvian) * New translations integrations.json (Latvian) * New translations inboxMgmt.json (Latvian) * New translations generalSettings.json (Latvian) * New translations contact.json (Latvian) * New translations helpCenter.json (Latvian) --- app/javascript/dashboard/i18n/locale/lv/agentMgmt.json | 6 +++--- .../dashboard/i18n/locale/lv/attributesMgmt.json | 2 +- .../dashboard/i18n/locale/lv/automation.json | 2 +- app/javascript/dashboard/i18n/locale/lv/contact.json | 2 +- .../dashboard/i18n/locale/lv/contactFilters.json | 2 +- .../dashboard/i18n/locale/lv/conversation.json | 2 +- .../dashboard/i18n/locale/lv/generalSettings.json | 2 +- .../dashboard/i18n/locale/lv/helpCenter.json | 10 +++++----- app/javascript/dashboard/i18n/locale/lv/inboxMgmt.json | 6 ++++-- .../dashboard/i18n/locale/lv/integrations.json | 4 ++-- .../dashboard/i18n/locale/lv/labelsMgmt.json | 2 +- app/javascript/dashboard/i18n/locale/lv/settings.json | 2 ++ .../dashboard/i18n/locale/lv/teamsSettings.json | 2 +- .../dashboard/i18n/locale/pt_BR/bulkActions.json | 4 +++- 14 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/javascript/dashboard/i18n/locale/lv/agentMgmt.json b/app/javascript/dashboard/i18n/locale/lv/agentMgmt.json index 5b4319962..933a88bab 100644 --- a/app/javascript/dashboard/i18n/locale/lv/agentMgmt.json +++ b/app/javascript/dashboard/i18n/locale/lv/agentMgmt.json @@ -12,12 +12,12 @@ "404": "Šim kontam nav piesaistīts neviens aģents", "TITLE": "Pārvaldīt Jūsu komandas aģentus", "DESC": "Jūs varat pievienot/noņemt aģentus pie/no savas komandas.", - "NAME": "Vārds", - "EMAIL": "e-pasts", + "NAME": "Nosaukums", + "EMAIL": "Epasts", "STATUS": "Statuss", "ACTIONS": "Darbības", "VERIFIED": "Pārbaudīts", - "VERIFICATION_PENDING": "Gaida apstiprinājumu" + "VERIFICATION_PENDING": "Tiek gaidīta verifikācija" }, "ADD": { "TITLE": "Pievienot aģentu Jūsu komandai", diff --git a/app/javascript/dashboard/i18n/locale/lv/attributesMgmt.json b/app/javascript/dashboard/i18n/locale/lv/attributesMgmt.json index 716f98705..ef12fa631 100644 --- a/app/javascript/dashboard/i18n/locale/lv/attributesMgmt.json +++ b/app/javascript/dashboard/i18n/locale/lv/attributesMgmt.json @@ -81,7 +81,7 @@ }, "LIST": { "TABLE_HEADER": [ - "Vārds", + "Nosaukums", "Apraksts", "Tips", "Atslēga" diff --git a/app/javascript/dashboard/i18n/locale/lv/automation.json b/app/javascript/dashboard/i18n/locale/lv/automation.json index a7edf9c25..84d8beedb 100644 --- a/app/javascript/dashboard/i18n/locale/lv/automation.json +++ b/app/javascript/dashboard/i18n/locale/lv/automation.json @@ -40,7 +40,7 @@ }, "LIST": { "TABLE_HEADER": [ - "Vārds", + "Nosaukums", "Apraksts", "Aktīvs", "Izveidots" diff --git a/app/javascript/dashboard/i18n/locale/lv/contact.json b/app/javascript/dashboard/i18n/locale/lv/contact.json index 2e167ae2c..ffe81f9af 100644 --- a/app/javascript/dashboard/i18n/locale/lv/contact.json +++ b/app/javascript/dashboard/i18n/locale/lv/contact.json @@ -202,7 +202,7 @@ "404": "Neviena kontaktpersona neatbilst jūsu meklēšanas vaicājumam 🔍", "NO_CONTACTS": "Nav pieejamu kontaktpersonu", "TABLE_HEADER": { - "NAME": "Vārds", + "NAME": "Nosaukums", "PHONE_NUMBER": "Telefona Numurs", "CONVERSATIONS": "Sarunas", "LAST_ACTIVITY": "Pēdējās Darbības", diff --git a/app/javascript/dashboard/i18n/locale/lv/contactFilters.json b/app/javascript/dashboard/i18n/locale/lv/contactFilters.json index e86ac2152..5ec1c3f4b 100644 --- a/app/javascript/dashboard/i18n/locale/lv/contactFilters.json +++ b/app/javascript/dashboard/i18n/locale/lv/contactFilters.json @@ -26,7 +26,7 @@ "days_before": "Ir x dienas pirms" }, "ATTRIBUTES": { - "NAME": "Vārds", + "NAME": "Nosaukums", "EMAIL": "E-pasts", "PHONE_NUMBER": "Tālruņa numurs", "IDENTIFIER": "Identifikators", diff --git a/app/javascript/dashboard/i18n/locale/lv/conversation.json b/app/javascript/dashboard/i18n/locale/lv/conversation.json index bb6274007..088a5e8bb 100644 --- a/app/javascript/dashboard/i18n/locale/lv/conversation.json +++ b/app/javascript/dashboard/i18n/locale/lv/conversation.json @@ -151,7 +151,7 @@ "CONTEXT_MENU": { "COPY": "Kopēt", "DELETE": "Dzēst", - "CREATE_A_CANNED_RESPONSE": "Add to canned responses" + "CREATE_A_CANNED_RESPONSE": "Pievienot sagatavotajām atbildēm" } }, "EMAIL_TRANSCRIPT": { diff --git a/app/javascript/dashboard/i18n/locale/lv/generalSettings.json b/app/javascript/dashboard/i18n/locale/lv/generalSettings.json index f2a3c3b6f..5e5e39a98 100644 --- a/app/javascript/dashboard/i18n/locale/lv/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/lv/generalSettings.json @@ -71,7 +71,7 @@ "LOADING_MESSAGE": "Notiek paziņojumu ielāde...", "404": "Nav paziņojumu", "TABLE_HEADER": [ - "Vārds", + "Nosaukums", "Telefona numurs", "Sarunas", "Pēdējā Sazināšanās" diff --git a/app/javascript/dashboard/i18n/locale/lv/helpCenter.json b/app/javascript/dashboard/i18n/locale/lv/helpCenter.json index c877bdb20..39705cf26 100644 --- a/app/javascript/dashboard/i18n/locale/lv/helpCenter.json +++ b/app/javascript/dashboard/i18n/locale/lv/helpCenter.json @@ -92,7 +92,7 @@ "PORTAL_CONFIG": { "TITLE": "Portāla Konfigurācijas", "ITEMS": { - "NAME": "Vārds", + "NAME": "Nosaukums", "DOMAIN": "Pielāgots domēns", "SLUG": "Slug", "TITLE": "Portāla nosaukums", @@ -144,7 +144,7 @@ "TITLE": "Kategorijas iekš", "NEW_CATEGORY": "Jauna kategorija", "TABLE": { - "NAME": "Vārds", + "NAME": "Nosaukums", "DESCRIPTION": "Apraksts", "LOCALE": "Lokalizācija", "ARTICLE_COUNT": "Rakstu skaits", @@ -204,7 +204,7 @@ "HELP_TEXT": "Šis logotips tiks attēlots portāla galvenē." }, "NAME": { - "LABEL": "Vārds", + "LABEL": "Nosaukums", "PLACEHOLDER": "Portāla nosaukums", "HELP_TEXT": "Nosaukums tiks izmantots publiskajā portālā iekšēji.", "ERROR": "Nepieciešams nosaukums" @@ -344,7 +344,7 @@ "PORTAL": "Portāls", "LOCALE": "Lokalizācija", "NAME": { - "LABEL": "Vārds", + "LABEL": "Nosaukums", "PLACEHOLDER": "Kategorijas nosaukums", "HELP_TEXT": "Kategorijas nosaukums tiks izmantots publiskajā portālā, lai klasificētu rakstus.", "ERROR": "Nepieciešams nosaukums" @@ -375,7 +375,7 @@ "PORTAL": "Portāls", "LOCALE": "Lokalizācija", "NAME": { - "LABEL": "Vārds", + "LABEL": "Nosaukums", "PLACEHOLDER": "Kategorijas nosaukums", "HELP_TEXT": "Kategorijas nosaukums tiks izmantots publiskajā portālā, lai klasificētu rakstus.", "ERROR": "Nepieciešams nosaukums" diff --git a/app/javascript/dashboard/i18n/locale/lv/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/lv/inboxMgmt.json index 12f32f6f0..f62035e3c 100644 --- a/app/javascript/dashboard/i18n/locale/lv/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/lv/inboxMgmt.json @@ -239,7 +239,9 @@ }, "API_CALLBACK": { "TITLE": "Atzvanīšanas URL", - "SUBTITLE": "Jums ir facebook izstrādātāju portālā jānokonfigurē webhook URL, izmantojot šeit minēto URL." + "SUBTITLE": "Jums Facebook izstrādātāju portālā ir jānokonfigurē webhoot URL un verifikācijas token ar tālāk norādītajām vērtībām.", + "WEBHOOK_URL": "Webhook URL", + "WEBHOOK_VERIFICATION_TOKEN": "Webhook Verifikācijas Token" }, "SUBMIT_BUTTON": "Izveidot WhatsApp kanālu", "API": { @@ -357,7 +359,7 @@ }, "FINISH": { "TITLE": "Jūsu Iesūtne ir gatava!", - "MESSAGE": "Tagad Jūs varat izmantot savu jauno Kanālu lai sazinātos ar saviem klientiem. Priecīgu atbalstīšanu ", + "MESSAGE": "Tagad Jūs varat izmantot savu jauno Kanālu lai sazinātos ar saviem klientiem. Priecīgu atbalstīšanu", "BUTTON_TEXT": "Iet uz", "MORE_SETTINGS": "Papildu iestatījumi", "WEBSITE_SUCCESS": "Jūs esat veiksmīgi pabeidzis tīmekļa vietnes kanāla izveidi. Nokopējiet tālāk redzamo kodu un ievietojiet to savā tīmekļa vietnē. Nākamreiz, kad klients izmantos tiešsaistes tērzēšanu, saruna automātiski tiks parādīta Jūsu iesūtnē." diff --git a/app/javascript/dashboard/i18n/locale/lv/integrations.json b/app/javascript/dashboard/i18n/locale/lv/integrations.json index 372061132..48752ccce 100644 --- a/app/javascript/dashboard/i18n/locale/lv/integrations.json +++ b/app/javascript/dashboard/i18n/locale/lv/integrations.json @@ -94,14 +94,14 @@ "404": "Šajā kontā vēl nav nokonfigurēta neviena informācijas paneļa lietotne", "LOADING": "Notiek informācijas paneļa lietotņu iegūšana...", "TABLE_HEADER": [ - "Vārds", + "Nosaukums", "Endpoint" ], "EDIT_TOOLTIP": "Rediģēt lietotni", "DELETE_TOOLTIP": "Dzēst lietotni" }, "FORM": { - "TITLE_LABEL": "Vārds", + "TITLE_LABEL": "Nosaukums", "TITLE_PLACEHOLDER": "Ievadiet informācijas paneļa lietotnes nosaukumu", "TITLE_ERROR": "Informācijas paneļa lietotnei ir jānorāda nosaukums", "URL_LABEL": "Endpoint", diff --git a/app/javascript/dashboard/i18n/locale/lv/labelsMgmt.json b/app/javascript/dashboard/i18n/locale/lv/labelsMgmt.json index 81b975bbc..011b780c1 100644 --- a/app/javascript/dashboard/i18n/locale/lv/labelsMgmt.json +++ b/app/javascript/dashboard/i18n/locale/lv/labelsMgmt.json @@ -10,7 +10,7 @@ "TITLE": "Pārvaldīt Etiķetes", "DESC": "Etiķetes ļauj grupēt sarunas kopā.", "TABLE_HEADER": [ - "Vārds", + "Nosaukums", "Apraksts", "Krāsa" ] diff --git a/app/javascript/dashboard/i18n/locale/lv/settings.json b/app/javascript/dashboard/i18n/locale/lv/settings.json index effdabc47..e2b82e781 100644 --- a/app/javascript/dashboard/i18n/locale/lv/settings.json +++ b/app/javascript/dashboard/i18n/locale/lv/settings.json @@ -179,6 +179,7 @@ "CONTACTS": "Kontaktpersonas", "HOME": "Sākums", "AGENTS": "Aģenti", + "AGENT_BOTS": "Bots", "INBOXES": "Iesūtnes", "NOTIFICATIONS": "Paziņojumi", "CANNED_RESPONSES": "Sagatavotās Atbildes", @@ -189,6 +190,7 @@ "LABELS": "Etiķetes", "CUSTOM_ATTRIBUTES": "Pielāgotas Īpašības", "AUTOMATION": "Automatizācija", + "MACROS": "Macros", "TEAMS": "Komandas", "BILLING": "Norēķini", "CUSTOM_VIEWS_FOLDER": "Mapes", diff --git a/app/javascript/dashboard/i18n/locale/lv/teamsSettings.json b/app/javascript/dashboard/i18n/locale/lv/teamsSettings.json index 960b5b26d..8d6f4b4da 100644 --- a/app/javascript/dashboard/i18n/locale/lv/teamsSettings.json +++ b/app/javascript/dashboard/i18n/locale/lv/teamsSettings.json @@ -69,7 +69,7 @@ }, "AGENTS": { "AGENT": "AĢENTS", - "EMAIL": "e-pasts", + "EMAIL": "Epasts", "BUTTON_TEXT": "Pievienot aģentus", "ADD_AGENTS": "Notiek aģentu pievienošana Jūsu komandai...", "SELECT": "izvēlēties", diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/bulkActions.json b/app/javascript/dashboard/i18n/locale/pt_BR/bulkActions.json index f3f54ea9f..caf0ed3df 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/bulkActions.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/bulkActions.json @@ -2,9 +2,11 @@ "BULK_ACTION": { "CONVERSATIONS_SELECTED": "%{conversationCount} conversas selecionadas", "AGENT_SELECT_LABEL": "Selecione Agente", - "ASSIGN_CONFIRMATION_LABEL": "Você tem certeza que deseja atribuir %{conversationCount} %{conversationLabel} para", + "ASSIGN_CONFIRMATION_LABEL": "Você tem certeza que quer atribuir %{conversationCount} %{conversationLabel} para", + "UNASSIGN_CONFIRMATION_LABEL": "Você tem certeza que quer remover a atribuição de %{conversationCount} %{conversationLabel}?", "GO_BACK_LABEL": "Voltar atrás", "ASSIGN_LABEL": "Atribua", + "YES": "Sim", "ASSIGN_AGENT_TOOLTIP": "Atribuir Agente", "ASSIGN_SUCCESFUL": "Conversas atribuídas com sucesso", "ASSIGN_FAILED": "Falha ao atribuir conversas, por favor, tente novamente", From ce3730d64078da7546a22db0a2749e0a84875b89 Mon Sep 17 00:00:00 2001 From: Simon Pankovski <46830637+simonpankovski@users.noreply.github.com> Date: Fri, 14 Oct 2022 22:38:08 +0200 Subject: [PATCH 43/54] fix: Avoid email icon getting distorted (#5633) Co-authored-by: Pranav Raj S --- .../routes/dashboard/conversation/contact/ContactInfoRow.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue index 33559e123..4e33bc7fa 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue @@ -10,10 +10,11 @@ From 252eda14c6e62cb8f91a8f7337d2dc05dc550bdf Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Mon, 17 Oct 2022 05:18:32 +0530 Subject: [PATCH 44/54] chore: Add woot button to message context menu (#5638) --- .../components/widgets/conversation/Message.vue | 2 ++ .../conversations/components/MessageContextMenu.vue | 12 +++++++----- .../shared/components/ui/dropdown/DropdownItem.vue | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 3481125ca..7fb24301b 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -420,6 +420,8 @@ export default { diff --git a/app/javascript/shared/components/ui/dropdown/DropdownItem.vue b/app/javascript/shared/components/ui/dropdown/DropdownItem.vue index c4da84234..893026451 100644 --- a/app/javascript/shared/components/ui/dropdown/DropdownItem.vue +++ b/app/javascript/shared/components/ui/dropdown/DropdownItem.vue @@ -35,6 +35,7 @@ export default { a, .button { display: inline-flex; + white-space: nowrap; width: 100%; text-align: left; color: var(--s-700); From 706ab872f36ffb116f2bb37eea840f6c30789ffa Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 17 Oct 2022 20:59:17 +0530 Subject: [PATCH 45/54] fix: Disable name in pre-chat form if the name is already provided in `setUser` (#5466) --- .../widget/components/PreChat/Form.vue | 20 +++++++++++++++---- .../api/v1/widget/contacts/show.json.jbuilder | 1 + 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index 69e543b21..1649068c6 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -117,11 +117,23 @@ export default { filteredPreChatFields() { const isUserEmailAvailable = !!this.currentUser.email; const isUserPhoneNumberAvailable = !!this.currentUser.phone_number; + const isUserIdentifierAvailable = !!this.currentUser.identifier; + const isUserNameAvailable = !!( + isUserIdentifierAvailable || + isUserEmailAvailable || + isUserPhoneNumberAvailable + ); return this.preChatFields.filter(field => { - return !( - (isUserEmailAvailable && field.name === 'emailAddress') || - (isUserPhoneNumberAvailable && field.name === 'phoneNumber') - ); + if (isUserEmailAvailable && field.name === 'emailAddress') { + return false; + } + if (isUserPhoneNumberAvailable && field.name === 'phoneNumber') { + return false; + } + if (isUserNameAvailable && field.name === 'fullName') { + return false; + } + return true; }); }, enabledPreChatFields() { diff --git a/app/views/api/v1/widget/contacts/show.json.jbuilder b/app/views/api/v1/widget/contacts/show.json.jbuilder index d6228cbfe..2e7a38277 100644 --- a/app/views/api/v1/widget/contacts/show.json.jbuilder +++ b/app/views/api/v1/widget/contacts/show.json.jbuilder @@ -2,3 +2,4 @@ json.id @contact.id json.name @contact.name json.email @contact.email json.phone_number @contact.phone_number +json.identifier @contact.identifier From 704554d453455bcec78ad2d5338a6e596c698794 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 17 Oct 2022 11:14:29 -0700 Subject: [PATCH 46/54] chore: Update translations (#5644) --- .../dashboard/i18n/locale/el/agentBots.json | 5 + .../dashboard/i18n/locale/el/bulkActions.json | 2 + .../dashboard/i18n/locale/el/contact.json | 4 +- .../i18n/locale/el/contactFilters.json | 2 +- .../i18n/locale/el/conversation.json | 40 +- .../dashboard/i18n/locale/el/helpCenter.json | 372 +++++++++--------- .../dashboard/i18n/locale/el/inboxMgmt.json | 52 +-- .../i18n/locale/el/integrations.json | 46 +-- .../dashboard/i18n/locale/el/macros.json | 5 + .../dashboard/i18n/locale/el/settings.json | 50 +-- .../dashboard/i18n/locale/he/chatlist.json | 4 +- .../dashboard/i18n/locale/he/contact.json | 8 +- .../i18n/locale/he/conversation.json | 60 +-- .../i18n/locale/he/generalSettings.json | 18 +- .../dashboard/i18n/locale/he/inboxMgmt.json | 8 +- .../dashboard/i18n/locale/he/settings.json | 4 +- config/locales/el.yml | 10 +- 17 files changed, 355 insertions(+), 335 deletions(-) create mode 100644 app/javascript/dashboard/i18n/locale/el/agentBots.json create mode 100644 app/javascript/dashboard/i18n/locale/el/macros.json diff --git a/app/javascript/dashboard/i18n/locale/el/agentBots.json b/app/javascript/dashboard/i18n/locale/el/agentBots.json new file mode 100644 index 000000000..708b82173 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/el/agentBots.json @@ -0,0 +1,5 @@ +{ + "AGENT_BOTS": { + "HEADER": "Bots" + } +} diff --git a/app/javascript/dashboard/i18n/locale/el/bulkActions.json b/app/javascript/dashboard/i18n/locale/el/bulkActions.json index acf4a2d10..1593857a6 100644 --- a/app/javascript/dashboard/i18n/locale/el/bulkActions.json +++ b/app/javascript/dashboard/i18n/locale/el/bulkActions.json @@ -3,8 +3,10 @@ "CONVERSATIONS_SELECTED": "%{conversationCount} σινομιλίες επιλέχθηκαν", "AGENT_SELECT_LABEL": "Επιλογή πράκτορα", "ASSIGN_CONFIRMATION_LABEL": "Είσαστε σίγουροι ότι θέλετε να αντιστοιχίσετε %{conversationCount} %{conversationLabel} στον", + "UNASSIGN_CONFIRMATION_LABEL": "Είσαστε σίγουροι ότι θέλετε να αφαιρέσετε την αντιστοίχιση %{conversationCount} %{conversationLabel} στον;", "GO_BACK_LABEL": "Πίσω", "ASSIGN_LABEL": "Αντιστοίχιση", + "YES": "Ναι", "ASSIGN_AGENT_TOOLTIP": "Ανάθεση σε πράκτορα", "ASSIGN_SUCCESFUL": "Οι σινομιλίες αντιστοιχήθηκαν επιτυχώς", "ASSIGN_FAILED": "Αποτυχία στην αντιστοίχιση σινομιλιών, παρακαλώ δοκιμάστε αργότερα", diff --git a/app/javascript/dashboard/i18n/locale/el/contact.json b/app/javascript/dashboard/i18n/locale/el/contact.json index fff494d7e..dc65bd7b9 100644 --- a/app/javascript/dashboard/i18n/locale/el/contact.json +++ b/app/javascript/dashboard/i18n/locale/el/contact.json @@ -152,8 +152,8 @@ }, "DELETE_AVATAR": { "API": { - "SUCCESS_MESSAGE": "Contact avatar deleted successfully", - "ERROR_MESSAGE": "Could not delete the contact avatar. Please try again later." + "SUCCESS_MESSAGE": "Το avatar της επαφής διαγράφηκε επιτυχώς", + "ERROR_MESSAGE": "Δεν ήταν δυνατή η διαγραφή του avatar της επαφής. Παρακαλώ προσπαθήστε ξανά αργότερα." } }, "SUCCESS_MESSAGE": "Η επαφή αποθηκεύτηκε με επιτυχία", diff --git a/app/javascript/dashboard/i18n/locale/el/contactFilters.json b/app/javascript/dashboard/i18n/locale/el/contactFilters.json index 7e897fe15..1bcbee2a5 100644 --- a/app/javascript/dashboard/i18n/locale/el/contactFilters.json +++ b/app/javascript/dashboard/i18n/locale/el/contactFilters.json @@ -39,7 +39,7 @@ "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CREATED_AT": "Δημιουργήθηκε στις", "LAST_ACTIVITY": "Τελευταία Δραστηριότητα", - "REFERER_LINK": "Referrer link" + "REFERER_LINK": "Σύνδεσμος αναφοράς" }, "GROUPS": { "STANDARD_FILTERS": "Τυπικά Φίλτρα", diff --git a/app/javascript/dashboard/i18n/locale/el/conversation.json b/app/javascript/dashboard/i18n/locale/el/conversation.json index c84a077ef..9bd7b8208 100644 --- a/app/javascript/dashboard/i18n/locale/el/conversation.json +++ b/app/javascript/dashboard/i18n/locale/el/conversation.json @@ -2,8 +2,8 @@ "CONVERSATION": { "SELECT_A_CONVERSATION": "Παρακαλώ επιλέξτε συζήτηση από το αριστερό τμήμα", "CSAT_REPLY_MESSAGE": "Παρακαλώ αξιολογήστε τη συνομιλία", - "404": "Sorry, we cannot find the conversation. Please try again", - "SWITCH_VIEW_LAYOUT": "Switch the layout", + "404": "Λυπούμαστε, δεν μπορούμε να βρούμε την συνομιλία. Παρακαλώ προσπαθήστε ξανά", + "SWITCH_VIEW_LAYOUT": "Εναλλαγή διάταξης", "DASHBOARD_APP_TAB_MESSAGES": "Μηνύματα", "UNVERIFIED_SESSION": "Η ταυτότητα αυτού του χρήστη δεν επαληθεύεται", "NO_MESSAGE_1": "Ωχ ωχ! Φαίνεται ότι δεν υπάρχουν μηνύματα από τους πελάτες στα εισερχόμενά σας.", @@ -63,30 +63,30 @@ }, "CARD_CONTEXT_MENU": { "PENDING": "Σήμανση ως εκκρεμής", - "RESOLVED": "Mark as resolved", + "RESOLVED": "Σήμανση ως επιλυμένου", "REOPEN": "Άνοιγμα συνομιλίας", "SNOOZE": { - "TITLE": "Snooze", + "TITLE": "Αναβολή", "NEXT_REPLY": "Μέχρι την επόμενη απάντηση", "TOMORROW": "Μέχρι αύριο", "NEXT_WEEK": "Έως την επόμενη εβδομάδα" }, - "ASSIGN_AGENT": "Assign agent", - "ASSIGN_LABEL": "Assign label", - "AGENTS_LOADING": "Loading agents...", - "ASSIGN_TEAM": "Assign team", + "ASSIGN_AGENT": "Ανάθεση σε πράκτορα", + "ASSIGN_LABEL": "Εκχώρηση ετικέτας", + "AGENTS_LOADING": "Φόρτωση πρακτόρων...", + "ASSIGN_TEAM": "Ανάθεση ομάδας", "API": { "AGENT_ASSIGNMENT": { - "SUCCESFUL": "Conversation id %{conversationId} assigned to \"%{agentName}\"", - "FAILED": "Couldn't assign agent. Please try again." + "SUCCESFUL": "Η συνομιλία με αριθμό %{conversationId} ανατέθηκε στον \"%{agentName}\"", + "FAILED": "Αδυναμία αντιστοίχισης σε πράκτορα. Παρακαλώ δοκιμάστε ξανά." }, "LABEL_ASSIGNMENT": { - "SUCCESFUL": "Assigned label #%{labelName} to conversation id %{conversationId}", - "FAILED": "Couldn't assign label. Please try again." + "SUCCESFUL": "Εκχώρηση ετικέτας #%{labelName} στην συνομιλία με αριθμό %{conversationId}", + "FAILED": "Αποτυχία στην εκχώρηση ετικέτας, παρακαλώ δοκιμάστε αργότερα." }, "TEAM_ASSIGNMENT": { - "SUCCESFUL": "Assigned team \"%{team}\" to conversation id %{conversationId}", - "FAILED": "Couldn't assign team. Please try again." + "SUCCESFUL": "Η συνομιλία με αριθμό %{conversationId} ανατέθηκε στην ομάδα \"%{team}\"", + "FAILED": "Αδυναμία αντιστοίχισης ομάδας. Παρακαλώ δοκιμάστε ξανά." } } }, @@ -131,13 +131,13 @@ }, "VISIBLE_TO_AGENTS": "Ιδιωτική Σημείωση: Ορατή μόνο σε σας και την ομάδα σας", "CHANGE_STATUS": "Η κατάσταση της συνομιλίας άλλαξε", - "CHANGE_STATUS_FAILED": "Conversation status change failed", + "CHANGE_STATUS_FAILED": "Η αλλαγή κατάστασης συνομιλίας απέτυχε", "CHANGE_AGENT": "Η εκπροσώπηση για την συνομιλία άλλαξε", - "CHANGE_AGENT_FAILED": "Assignee change failed", - "ASSIGN_LABEL_SUCCESFUL": "Label assigned successfully", - "ASSIGN_LABEL_FAILED": "Label assignment failed", + "CHANGE_AGENT_FAILED": "Η αλλαγή της ανάθεσης απέτυχε", + "ASSIGN_LABEL_SUCCESFUL": "Επιτυχής εκχώρηση ετικέτας", + "ASSIGN_LABEL_FAILED": "Η εκχώρηση ετικέτας απέτυχε", "CHANGE_TEAM": "Η ομάδα συνομιλίας άλλαξε", - "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB attachment limit", + "FILE_SIZE_LIMIT": "Το αρχείο υπερβαίνει το όριο συνημμένου {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE}", "MESSAGE_ERROR": "Δεν είναι δυνατή η αποστολή του μηνύματος, παρακαλώ προσπαθήστε ξανά αργότερα", "SENT_BY": "Αποστολή από:", "BOT": "Bot", @@ -151,7 +151,7 @@ "CONTEXT_MENU": { "COPY": "Αντιγραφή", "DELETE": "Διαγραφή", - "CREATE_A_CANNED_RESPONSE": "Add to canned responses" + "CREATE_A_CANNED_RESPONSE": "Προσθήκη στις έτοιμες απαντήσεις" } }, "EMAIL_TRANSCRIPT": { diff --git a/app/javascript/dashboard/i18n/locale/el/helpCenter.json b/app/javascript/dashboard/i18n/locale/el/helpCenter.json index 2f6be5952..859e8373a 100644 --- a/app/javascript/dashboard/i18n/locale/el/helpCenter.json +++ b/app/javascript/dashboard/i18n/locale/el/helpCenter.json @@ -1,409 +1,409 @@ { "HELP_CENTER": { "HEADER": { - "FILTER": "Filter by", - "SORT": "Sort by", + "FILTER": "Φιλτράρισμα κατά", + "SORT": "Ταξινόμηση κατά", "SETTINGS_BUTTON": "Ρυθμίσεις", - "NEW_BUTTON": "New Article", + "NEW_BUTTON": "Νέο Άρθρο", "DROPDOWN_OPTIONS": { - "PUBLISHED": "Published", - "DRAFT": "Draft", - "ARCHIVED": "Archived" + "PUBLISHED": "Δημοσιευμένο", + "DRAFT": "Πρόχειρο", + "ARCHIVED": "Αρχειοθετημένο" }, "TITLES": { - "ALL_ARTICLES": "All Articles", - "MINE": "My Articles", - "DRAFT": "Draft Articles", - "ARCHIVED": "Archived Articles" + "ALL_ARTICLES": "Όλα Τα Άρθρα", + "MINE": "Τα Άρθρα Μου", + "DRAFT": "Πρόχειρα Άρθρα", + "ARCHIVED": "Αρχειοθετημένα Άρθρα" } }, "EDIT_HEADER": { - "ALL_ARTICLES": "All Articles", - "PUBLISH_BUTTON": "Publish", - "MOVE_TO_ARCHIVE_BUTTON": "Move to archived", - "PREVIEW": "Preview", - "ADD_TRANSLATION": "Add translation", - "OPEN_SIDEBAR": "Open sidebar", - "CLOSE_SIDEBAR": "Close sidebar", - "SAVING": "Saving...", - "SAVED": "Saved" + "ALL_ARTICLES": "Όλα Τα Άρθρα", + "PUBLISH_BUTTON": "Δημοσιευμένο", + "MOVE_TO_ARCHIVE_BUTTON": "Μετακίνηση στα αρχειοθετημένα", + "PREVIEW": "Προεπισκόπηση", + "ADD_TRANSLATION": "Προσθήκη μετάφρασης", + "OPEN_SIDEBAR": "Άνοιγμα πλευρικής μπάρας", + "CLOSE_SIDEBAR": "Κλείσιμο πλευρικής μπάρας", + "SAVING": "Αποθηκεύεται...", + "SAVED": "Αποθηκεύτηκε" }, "ARTICLE_SETTINGS": { - "TITLE": "Article Settings", + "TITLE": "Ρυθμίσεις Άρθρου", "FORM": { "CATEGORY": { "LABEL": "Κατηγορία", - "TITLE": "Select category", - "PLACEHOLDER": "Select category", - "NO_RESULT": "No category found", - "SEARCH_PLACEHOLDER": "Search category" + "TITLE": "Επιλογή κατηγορίας", + "PLACEHOLDER": "Επιλογή κατηγορίας", + "NO_RESULT": "Δεν βρέθηκε καμία κατηγορία", + "SEARCH_PLACEHOLDER": "Αναζήτηση κατηγορίας" }, "AUTHOR": { - "LABEL": "Author", - "TITLE": "Select author", - "PLACEHOLDER": "Select author", - "NO_RESULT": "No authors found", - "SEARCH_PLACEHOLDER": "Search author" + "LABEL": "Συγγραφέας", + "TITLE": "Επιλογή συγγραφέα", + "PLACEHOLDER": "Επιλογή συγγραφέα", + "NO_RESULT": "Δεν βρέθηκαν συγγραφείς", + "SEARCH_PLACEHOLDER": "Αναζήτηση συγγραφέα" }, "META_TITLE": { - "LABEL": "Meta title", - "PLACEHOLDER": "Add a meta title" + "LABEL": "Μετα-τίτλος", + "PLACEHOLDER": "Προσθήκη meta Τίτλου" }, "META_DESCRIPTION": { - "LABEL": "Meta description", - "PLACEHOLDER": "Add your meta description for better SEO results..." + "LABEL": "Meta περιγραφή", + "PLACEHOLDER": "Προσθέστε τη meta περιγραφή σας για καλύτερα αποτελέσματα SEO..." }, "META_TAGS": { "LABEL": "Meta tags", - "PLACEHOLDER": "Add meta tags separated by comma..." + "PLACEHOLDER": "Προσθήκη μετα-ετικετών διαχωρισμένων με κόμμα..." } }, "BUTTONS": { - "ARCHIVE": "Archive article", - "DELETE": "Delete article" + "ARCHIVE": "Αρχειοθέτηση άρθρου", + "DELETE": "Διαγραφή άρθρου" } }, "PORTAL": { - "HEADER": "Portals", - "DEFAULT": "Default", - "NEW_BUTTON": "New Portal", + "HEADER": "Πύλες", + "DEFAULT": "Προεπιλογή", + "NEW_BUTTON": "Νέα Πύλη", "ACTIVE_BADGE": "ενεργή", - "CHOOSE_LOCALE_LABEL": "Choose a locale", - "LOADING_MESSAGE": "Loading portals...", - "ARTICLES_LABEL": "articles", - "NO_PORTALS_MESSAGE": "There are no available portals", - "ADD_NEW_LOCALE": "Add a new locale", + "CHOOSE_LOCALE_LABEL": "Επιλέξτε μια γλώσσα", + "LOADING_MESSAGE": "Φόρτωση πυλών...", + "ARTICLES_LABEL": "άρθρα", + "NO_PORTALS_MESSAGE": "Δεν υπάρχουν διαθέσιμες πύλες", + "ADD_NEW_LOCALE": "Προσθέστε μια νέα γλώσσα", "POPOVER": { - "TITLE": "Portals", - "PORTAL_SETTINGS": "Portal settings", - "SUBTITLE": "You have multiple portals and can have different locales for each portal.", + "TITLE": "Πύλες", + "PORTAL_SETTINGS": "Ρυθμίσεις πύλης", + "SUBTITLE": "Έχετε πολλαπλές πύλες με διαφορετικές γλώσσες για κάθε πύλη.", "CANCEL_BUTTON_LABEL": "Άκυρο", - "CHOOSE_LOCALE_BUTTON": "Choose Locale" + "CHOOSE_LOCALE_BUTTON": "Επιλέξτε γλώσσα" }, "PORTAL_SETTINGS": { "LIST_ITEM": { "HEADER": { - "COUNT_LABEL": "articles", - "ADD": "Add locale", - "VISIT": "Visit site", + "COUNT_LABEL": "άρθρα", + "ADD": "Προσθήκη γλώσσας", + "VISIT": "Επίσκεψη site", "SETTINGS": "Ρυθμίσεις", "DELETE": "Διαγραφή" }, "PORTAL_CONFIG": { - "TITLE": "Portal Configurations", + "TITLE": "Ρυθμίσεις Πύλης", "ITEMS": { "NAME": "Όνομα", - "DOMAIN": "Custom domain", + "DOMAIN": "Προσαρμοσμένο Domain", "SLUG": "Slug", - "TITLE": "Portal title", - "THEME": "Theme color", - "SUB_TEXT": "Portal sub text" + "TITLE": "Τίτλος πύλης", + "THEME": "Χρώμα θέματος", + "SUB_TEXT": "Υποκείμενο πύλης" } }, "AVAILABLE_LOCALES": { - "TITLE": "Available locales", + "TITLE": "Διαθέσιμες γλώσσες", "TABLE": { - "NAME": "Locale name", - "CODE": "Locale code", - "ARTICLE_COUNT": "No. of articles", - "CATEGORIES": "No. of categories", - "SWAP": "Swap", + "NAME": "Όνομα γλώσσας", + "CODE": "Κωδικός γλώσσας", + "ARTICLE_COUNT": "Αριθμός άρθρων", + "CATEGORIES": "Αριθμός κατηγοριών", + "SWAP": "Εναλλαγή", "DELETE": "Διαγραφή", - "DEFAULT_LOCALE": "Default" + "DEFAULT_LOCALE": "Προεπιλογή" } } }, "DELETE_PORTAL": { - "TITLE": "Delete portal", - "MESSAGE": "Are you sure you want to delete this portal", - "YES": "Yes, delete portal", - "NO": "No, keep portal", + "TITLE": "Διαγραφή πύλης", + "MESSAGE": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την πύλη", + "YES": "Ναι, διαγραφή πύλης", + "NO": "Όχι, διατήρηση πύλης", "API": { - "DELETE_SUCCESS": "Portal deleted successfully", - "DELETE_ERROR": "Error while deleting portal" + "DELETE_SUCCESS": "Η πύλη διαγράφηκε επιτυχώς", + "DELETE_ERROR": "Σφάλμα κατά τη διαγραφή της πύλης" } } }, "EDIT": { - "HEADER_TEXT": "Edit portal", + "HEADER_TEXT": "Επεξεργασία πύλης", "TABS": { "BASIC_SETTINGS": { - "TITLE": "Basic information" + "TITLE": "Βασικές πληροφορίες" }, "CUSTOMIZATION_SETTINGS": { - "TITLE": "Portal customization" + "TITLE": "Προσαρμογή πύλης" }, "CATEGORY_SETTINGS": { - "TITLE": "Categories" + "TITLE": "Κατηγορίες" }, "LOCALE_SETTINGS": { - "TITLE": "Locales" + "TITLE": "Γλώσσες" } }, "CATEGORIES": { - "TITLE": "Categories in", - "NEW_CATEGORY": "New category", + "TITLE": "Κατηγορίες στο", + "NEW_CATEGORY": "Νέα κατηγορία", "TABLE": { "NAME": "Όνομα", "DESCRIPTION": "Περιγραφή", - "LOCALE": "Locale", - "ARTICLE_COUNT": "No. of articles", + "LOCALE": "Γλώσσα", + "ARTICLE_COUNT": "Αριθμός άρθρων", "ACTION_BUTTON": { - "EDIT": "Edit category", - "DELETE": "Delete category" + "EDIT": "Επεξεργασία κατηγορίας", + "DELETE": "Διαγραφή κατηγορίας" }, - "EMPTY_TEXT": "No categories found" + "EMPTY_TEXT": "Δεν βρέθηκαν κατηγορίες" } }, "EDIT_BASIC_INFO": { - "BUTTON_TEXT": "Update basic settings" + "BUTTON_TEXT": "Ενημέρωση βασικών ρυθμίσεων" } }, "ADD": { "CREATE_FLOW": [ { - "title": "Help center information", + "title": "Πληροφορίες κέντρου βοήθειας", "route": "new_portal_information", - "body": "Basic information about portal", - "CREATE_BASIC_SETTING_BUTTON": "Create portal basic settings" + "body": "Βασικές πληροφορίες σχετικά με την πύλη", + "CREATE_BASIC_SETTING_BUTTON": "Δημιουργία βασικών ρυθμίσεων πύλης" }, { - "title": "Help center customization", + "title": "Προσαρμογή του κέντρου βοήθειας", "route": "portal_customization", - "body": "Customize portal", - "UPDATE_PORTAL_BUTTON": "Update portal settings" + "body": "Προσαρμογή πύλης", + "UPDATE_PORTAL_BUTTON": "Ενημέρωση ρυθμίσεων πύλης" }, { - "title": "Voila! 🎉", + "title": "Έξοχα! 🎉", "route": "portal_finish", - "body": "You're all set!", + "body": "Είναι όλα έτοιμα!", "FINISH": "Τέλος" } ], "CREATE_FLOW_PAGE": { "BACK_BUTTON": "Πίσω", "BASIC_SETTINGS_PAGE": { - "HEADER": "Create Portal", - "TITLE": "Help center information", - "CREATE_BASIC_SETTING_BUTTON": "Create portal basic settings" + "HEADER": "Δημιουργία Πύλης", + "TITLE": "Πληροφορίες κέντρου βοήθειας", + "CREATE_BASIC_SETTING_BUTTON": "Δημιουργία βασικών ρυθμίσεων πύλης" }, "CUSTOMIZATION_PAGE": { - "HEADER": "Portal customisation", - "TITLE": "Help center customization", - "UPDATE_PORTAL_BUTTON": "Update portal settings" + "HEADER": "Προσαρμογή πύλης", + "TITLE": "Προσαρμογή του κέντρου βοήθειας", + "UPDATE_PORTAL_BUTTON": "Ενημέρωση ρυθμίσεων πύλης" }, "FINISH_PAGE": { - "TITLE": "Voila!🎉 You're all set up!", - "MESSAGE": "You can now see this created portal on your all portals page.", - "FINISH": "Go to all portals page" + "TITLE": "Έξοχα!🎉 Έχετε ρυθμιστεί!", + "MESSAGE": "Τώρα μπορείτε να δείτε την πύλη που δημιουργήθηκε στη σελίδα με όλες τις πύλες.", + "FINISH": "Μετάβαση στις πύλες" } }, "LOGO": { - "LABEL": "Logo", - "UPLOAD_BUTTON": "Upload logo", - "HELP_TEXT": "This logo will be displayed on the portal header." + "LABEL": "Λογότυπο", + "UPLOAD_BUTTON": "Μεταφόρτωση λογότυπου", + "HELP_TEXT": "Το λογότυπο θα εμφανιστεί στην κεφαλίδα της πύλης." }, "NAME": { "LABEL": "Όνομα", - "PLACEHOLDER": "Portal name", - "HELP_TEXT": "The name will be used in the public facing portal internally.", + "PLACEHOLDER": "Όνομα πύλης", + "HELP_TEXT": "Το όνομα θα χρησιμοποιηθεί στο κοινό που βλέπει την πύλη εσωτερικά.", "ERROR": "Απαιτείται όνομα" }, "SLUG": { "LABEL": "Slug", - "PLACEHOLDER": "Portal slug for urls", - "ERROR": "Slug is required" + "PLACEHOLDER": "Slug Πύλης για τα urls", + "ERROR": "Το Slug είναι απαραίτητο" }, "DOMAIN": { - "LABEL": "Custom Domain", - "PLACEHOLDER": "Portal custom domain", - "HELP_TEXT": "Add only If you want to use a custom domain for your portals.", - "ERROR": "Custom Domain is required" + "LABEL": "Προσαρμοσμένο Domain", + "PLACEHOLDER": "Προσαρμοσμένος τομέας πύλης", + "HELP_TEXT": "Προσθήκη μόνο Αν θέλετε να χρησιμοποιήσετε ένα προσαρμοσμένο τομέα για τις πύλες σας. (Automatic Translation).", + "ERROR": "Ο Προσαρμοσμένος Τομέας απαιτείται" }, "HOME_PAGE_LINK": { - "LABEL": "Home Page Link", - "PLACEHOLDER": "Portal home page link", - "HELP_TEXT": "The link used to return from the portal to the home page.", - "ERROR": "Home Page Link is required" + "LABEL": "Σύνδεσμος Αρχικής Σελίδας", + "PLACEHOLDER": "Σύνδεσμος αρχικής σελίδας πύλης", + "HELP_TEXT": "Ο σύνδεσμος που χρησιμοποιείται για την επιστροφή από την πύλη στην αρχική σελίδα.", + "ERROR": "Ο σύνδεσμος αρχικής σελίδας απαιτείται" }, "THEME_COLOR": { - "LABEL": "Portal theme color", - "HELP_TEXT": "This color will show as the theme color for the portal." + "LABEL": "Χρώμα θέματος πύλης", + "HELP_TEXT": "Αυτό το χρώμα θα εμφανίζεται ως το χρώμα θέματος της πύλης. " }, "PAGE_TITLE": { - "LABEL": "Page Title", - "PLACEHOLDER": "Portal page title", - "HELP_TEXT": "The page title will be used in the public facing portal.", - "ERROR": "Page title is required" + "LABEL": "Τίτλος Σελίδας", + "PLACEHOLDER": "Τίτλος σελίδας πύλης", + "HELP_TEXT": "Ο τίτλος της σελίδας θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό.", + "ERROR": "Ο τίτλος είναι απαραίτητος" }, "HEADER_TEXT": { - "LABEL": "Header Text", - "PLACEHOLDER": "Portal header text", - "HELP_TEXT": "The Portal header text will be used in the public facing portal.", - "ERROR": "Portal header text is required" + "LABEL": "Κείμενο Κεφαλίδας", + "PLACEHOLDER": "Κείμενο κεφαλίδας πύλης", + "HELP_TEXT": "Το κείμενο κεφαλίδας πύλης θα χρησιμοποιηθεί στο κοινό που βλέπει πύλη.", + "ERROR": "Απαιτείται κείμενο κεφαλίδας πύλης" }, "API": { - "SUCCESS_MESSAGE_FOR_BASIC": "Portal created successfully.", - "ERROR_MESSAGE_FOR_BASIC": "Couldn't create the portal. Try again.", - "SUCCESS_MESSAGE_FOR_UPDATE": "Portal updated successfully.", - "ERROR_MESSAGE_FOR_UPDATE": "Couldn't update the portal. Try again." + "SUCCESS_MESSAGE_FOR_BASIC": "Ο φάκελος δημιουργήθηκε με επιτυχία.", + "ERROR_MESSAGE_FOR_BASIC": "Δεν ήταν δυνατή η δημιουργία της πύλης. Δοκιμάστε ξανά.", + "SUCCESS_MESSAGE_FOR_UPDATE": "Η πύλη ενημερώθηκε με επιτυχία.", + "ERROR_MESSAGE_FOR_UPDATE": "Δεν ήταν δυνατή η ενημέρωση της πύλης. Δοκιμάστε ξανά." } }, "ADD_LOCALE": { - "TITLE": "Add a new locale", - "SUB_TITLE": "This adds a new locale to your available translation list.", - "PORTAL": "Portal", + "TITLE": "Προσθέστε μια νέα γλώσσα", + "SUB_TITLE": "Προσθέτει μια νέα γλώσσα στη διαθέσιμη λίστα μεταφράσεών σας.", + "PORTAL": "Πύλη", "LOCALE": { - "LABEL": "Locale", - "PLACEHOLDER": "Choose a locale", - "ERROR": "Locale is required" + "LABEL": "Γλώσσα", + "PLACEHOLDER": "Επιλέξτε μια γλώσσα", + "ERROR": "Η γλώσσα απαιτείται" }, "BUTTONS": { - "CREATE": "Create locale", + "CREATE": "Δημιουργία γλώσσας", "CANCEL": "Άκυρο" }, "API": { - "SUCCESS_MESSAGE": "Locale added successfully", - "ERROR_MESSAGE": "Unable to add locale. Try again." + "SUCCESS_MESSAGE": "Η γλώσσα προστέθηκε επιτυχώς", + "ERROR_MESSAGE": "Δεν είναι δυνατή η προσθήκη γλώσσας. Δοκιμάστε ξανά." } }, "CHANGE_DEFAULT_LOCALE": { "API": { - "SUCCESS_MESSAGE": "Default locale updated successfully", - "ERROR_MESSAGE": "Unable to update default locale. Try again." + "SUCCESS_MESSAGE": "Η προεπιλεγμένη γλώσσα ενημερώθηκε επιτυχώς", + "ERROR_MESSAGE": "Δεν είναι δυνατή η ενημέρωση της προεπιλεγμένης γλώσσας. Δοκιμάστε ξανά." } }, "DELETE_LOCALE": { "API": { - "SUCCESS_MESSAGE": "Locale removed from portal successfully", - "ERROR_MESSAGE": "Unable to remove locale from portal. Try again." + "SUCCESS_MESSAGE": "Η γλώσσα αφαιρέθηκε επιτυχώς από την πύλη", + "ERROR_MESSAGE": "Δεν είναι δυνατή η αφαίρεση γλώσσας από την πύλη. Δοκιμάστε ξανά." } } }, "TABLE": { - "LOADING_MESSAGE": "Loading articles...", - "404": "No articles matches your search 🔍", - "NO_ARTICLES": "There are no available articles", + "LOADING_MESSAGE": "Φόρτωση άρθρων...", + "404": "Δεν υπάρχουν άρθρα που να ταιριάζουν στην αναζήτησή σας 🔍", + "NO_ARTICLES": "Δεν υπάρχουν διαθέσιμα άρθρα", "HEADERS": { "TITLE": "Τίτλος", "CATEGORY": "Κατηγορία", - "READ_COUNT": "Read count", + "READ_COUNT": "Πλήθος ανάγνωσεων", "STATUS": "Κατάσταση", - "LAST_EDITED": "Last edited" + "LAST_EDITED": "Τελευταία επεξεργασία" }, "COLUMNS": { - "BY": "by" + "BY": "από" } }, "EDIT_ARTICLE": { - "LOADING": "Loading article...", - "TITLE_PLACEHOLDER": "Article title goes here", - "CONTENT_PLACEHOLDER": "Write your article here", + "LOADING": "Φόρτωση άρθρου...", + "TITLE_PLACEHOLDER": "Ο τίτλος του άρθρου εμφανίζεται εδώ", + "CONTENT_PLACEHOLDER": "Γράψτε το άρθρο σας εδώ", "API": { - "ERROR": "Error while saving article" + "ERROR": "Σφάλμα κατά την αποθήκευση άρθρου" } }, "PUBLISH_ARTICLE": { "API": { - "ERROR": "Error while publishing article", - "SUCCESS": "Article publishied successfully" + "ERROR": "Σφάλμα κατά τη δημοσίευση του άρθρου", + "SUCCESS": "Το Άρθρο δημοσιεύθηκε με επιτυχία" } }, "ARCHIVE_ARTICLE": { "API": { - "ERROR": "Error while archiving article", - "SUCCESS": "Article archived successfully" + "ERROR": "Σφάλμα κατά την αρχειοθέτηση άρθρου", + "SUCCESS": "Το άρθρο αρχειοθετήθηκε επιτυχώς" } }, "DELETE_ARTICLE": { "MODAL": { "CONFIRM": { "TITLE": "Επιβεβαίωση Διαγραφής", - "MESSAGE": "Are you sure to delete the article?", + "MESSAGE": "Είστε βέβαιοι να διαγράψετε το άρθρο;", "YES": "Ναι, Διέγραψε το", "NO": "Όχι, Κράτησε τον/την" } }, "API": { - "SUCCESS_MESSAGE": "Article deleted successfully", - "ERROR_MESSAGE": "Error while deleting article" + "SUCCESS_MESSAGE": "Η επαφή διαγράφηκε επιτυχώς", + "ERROR_MESSAGE": "Σφάλμα κατά τη διαγραφή άρθρου" } }, "CREATE_ARTICLE": { - "ERROR_MESSAGE": "Please add the article heading and content then only you can update the settings" + "ERROR_MESSAGE": "Παρακαλώ προσθέστε την επικεφαλίδα και το περιεχόμενο του άρθρου για να μπορείτε να ενημερώσετε τις ρυθμίσεις" }, "SIDEBAR": { "SEARCH": { - "PLACEHOLDER": "Search for articles" + "PLACEHOLDER": "Αναζήτηση άρθρων" } }, "CATEGORY": { "ADD": { - "TITLE": "Create a category", - "SUB_TITLE": "The category will be used in the public facing portal to categorize articles.", - "PORTAL": "Portal", - "LOCALE": "Locale", + "TITLE": "Δημιουργία κατηγορίας", + "SUB_TITLE": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.", + "PORTAL": "Πύλη", + "LOCALE": "Γλώσσα", "NAME": { "LABEL": "Όνομα", - "PLACEHOLDER": "Category name", - "HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", + "PLACEHOLDER": "Όνομα κατηγορίας", + "HELP_TEXT": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.", "ERROR": "Απαιτείται όνομα" }, "SLUG": { "LABEL": "Slug", - "PLACEHOLDER": "Category slug for urls", + "PLACEHOLDER": "Slug κατηγορίας για urls", "HELP_TEXT": "app.chatwoot.com/hc/my-portal/en-US/categories/my-slug", - "ERROR": "Slug is required" + "ERROR": "Το Slug είναι απαραίτητο" }, "DESCRIPTION": { "LABEL": "Περιγραφή", - "PLACEHOLDER": "Give a short description about the category.", + "PLACEHOLDER": "Δώστε μια σύντομη περιγραφή της κατηγορίας.", "ERROR": "Η περιγραφή απαιτείται" }, "BUTTONS": { - "CREATE": "Create category", + "CREATE": "Δημιουργία κατηγορίας", "CANCEL": "Άκυρο" }, "API": { - "SUCCESS_MESSAGE": "Category created successfully", - "ERROR_MESSAGE": "Unable to create category" + "SUCCESS_MESSAGE": "Η κατηγορία δημιουργήθηκε με επιτυχία", + "ERROR_MESSAGE": "Αδυναμία δημιουργίας κατηγορίας" } }, "EDIT": { - "TITLE": "Edit a category", - "SUB_TITLE": "Editing a category will update the category in the public facing portal.", - "PORTAL": "Portal", - "LOCALE": "Locale", + "TITLE": "Επεξεργασία κατηγορίας", + "SUB_TITLE": "Η επεξεργασία μιας κατηγορίας θα ενημερώσει την κατηγορία στην πύλη που βλέπει το κοινό.", + "PORTAL": "Πύλη", + "LOCALE": "Γλώσσα", "NAME": { "LABEL": "Όνομα", - "PLACEHOLDER": "Category name", - "HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", + "PLACEHOLDER": "Όνομα κατηγορίας", + "HELP_TEXT": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.", "ERROR": "Απαιτείται όνομα" }, "SLUG": { "LABEL": "Slug", - "PLACEHOLDER": "Category slug for urls", + "PLACEHOLDER": "Slug κατηγορίας για urls", "HELP_TEXT": "app.chatwoot.com/hc/my-portal/en-US/categories/my-slug", - "ERROR": "Slug is required" + "ERROR": "Το Slug είναι απαραίτητο" }, "DESCRIPTION": { "LABEL": "Περιγραφή", - "PLACEHOLDER": "Give a short description about the category.", + "PLACEHOLDER": "Δώστε μια σύντομη περιγραφή της κατηγορίας.", "ERROR": "Η περιγραφή απαιτείται" }, "BUTTONS": { - "CREATE": "Update category", + "CREATE": "Επεξεργασία κατηγορίας", "CANCEL": "Άκυρο" }, "API": { - "SUCCESS_MESSAGE": "Category updated successfully", - "ERROR_MESSAGE": "Unable to update category" + "SUCCESS_MESSAGE": "Η ετικέτα ενημερώθηκε επιτυχώς", + "ERROR_MESSAGE": "Αδύνατη η ενημέρωση της κατηγορίας" } }, "DELETE": { "API": { - "SUCCESS_MESSAGE": "Category deleted successfully", - "ERROR_MESSAGE": "Unable to delete category" + "SUCCESS_MESSAGE": "Η καμπάνια διαγράφηκε επιτυχώς", + "ERROR_MESSAGE": "Δεν είναι δυνατή η διαγραφή κατηγορίας" } } } diff --git a/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json index e92a89ebf..07baaba4b 100644 --- a/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json @@ -112,10 +112,10 @@ "ERROR": "Το πεδίο είναι απαραίτητο" }, "MESSAGING_SERVICE_SID": { - "LABEL": "Messaging Service SID", - "PLACEHOLDER": "Please enter your Twilio Messaging Service SID", + "LABEL": "SID Υπηρεσίας Μηνυμάτων", + "PLACEHOLDER": "Παρακαλώ εισάγετε το Twilio Messaging Service SID σας", "ERROR": "Το πεδίο είναι απαραίτητο", - "USE_MESSAGING_SERVICE": "Use a Twilio Messaging Service" + "USE_MESSAGING_SERVICE": "Χρήση μιας υπηρεσίας μηνυμάτων Twilio" }, "CHANNEL_TYPE": { "LABEL": "Τύπος Καναλιού", @@ -239,7 +239,9 @@ }, "API_CALLBACK": { "TITLE": "URL επανάκλησης", - "SUBTITLE": "Θα πρέπει να ρυθμίσετε το URL webhook στο facebook developer portal με το URL που αναφέρεται εδώ." + "SUBTITLE": "Πρέπει να ρυθμίσετε τη διεύθυνση URL του webhook και το διακριτικό επαλήθευσης στην πύλη Προγραμματιστή Facebook με τις τιμές που εμφανίζονται παρακάτω.", + "WEBHOOK_URL": "Σύνδεσμος Webhook", + "WEBHOOK_VERIFICATION_TOKEN": "Token Επαλήθευσης Webhook" }, "SUBMIT_BUTTON": "Δημιουργία Καναλιού WhatsApp", "API": { @@ -357,7 +359,7 @@ }, "FINISH": { "TITLE": "Το κιβώτιο σας είναι έτοιμο!", - "MESSAGE": "Μπορείτε να συνομιλείτε με τους πελάτες σας από το νέο κανάλι. Καλή υποστήριξη ", + "MESSAGE": "Μπορείτε να συνομιλείτε με τους πελάτες σας από το νέο κανάλι. Καλή υποστήριξη", "BUTTON_TEXT": "Μετάβαση", "MORE_SETTINGS": "Περισσότερες ρυθμίσεις", "WEBSITE_SUCCESS": "Επιτυχής δημιουργία του καναλιού ιστοσελίδας. Αντιγράψτε τον κώδικα που παρουσιάζεται παρακάτω, και τοποθετήστε τον στην ιστοσελίδα σας. Την επόμενη φορά που κάποιος πελάτης χρησιμοποιήσει το 'live chat', η συνομιλία θα εμφανιστεί στο κιβώτιο εισερχομένων σας." @@ -414,7 +416,7 @@ "CAMPAIGN": "Καμπάνιες", "PRE_CHAT_FORM": "Φόρμα Προ-Συνομιλίας", "BUSINESS_HOURS": "Ώρες Εργασίας", - "WIDGET_BUILDER": "Widget Builder" + "WIDGET_BUILDER": "Δημιουργός Widget" }, "SETTINGS": "Ρυθμίσεις", "FEATURES": { @@ -579,10 +581,10 @@ "WIDGET_BUILDER": { "WIDGET_OPTIONS": { "AVATAR": { - "LABEL": "Website Avatar", + "LABEL": "Avatar Ιστοσελίδας", "DELETE": { "API": { - "SUCCESS_MESSAGE": "Avatar deleted successfully", + "SUCCESS_MESSAGE": "Το Avatar διαγράφηκε επιτυχώς", "ERROR_MESSAGE": "Υπήρξε ένα σφάλμα, παρακαλώ προσπαθήστε ξανά" } } @@ -590,7 +592,7 @@ "WEBSITE_NAME": { "LABEL": "Όνομα Ιστοσελίδας", "PLACE_HOLDER": "Συμπληρώστε την ονομασία της ιστοσελίδας σας (π.χ: Ελληνικό Μεσογειακό Πανεπιστήμιο)", - "ERROR": "Please enter a valid website name" + "ERROR": "Παρακαλώ δώστε ένα έγκυρο όνομα ιστοσελίδας" }, "WELCOME_HEADING": { "LABEL": "Καλώς ήλθατε (Heading)", @@ -601,42 +603,42 @@ "PLACE_HOLDER": "Είναι απλό να συνδεθείτε μαζί μας. Ζητήστε μας οτιδήποτε, ή μοιραστείτε την εμπειρία σας." }, "REPLY_TIME": { - "LABEL": "Reply Time", + "LABEL": "Χρόνος Απάντησης", "IN_A_FEW_MINUTES": "Σε μερικά λεπτά", "IN_A_FEW_HOURS": "Σε μερικές ώρες", "IN_A_DAY": "Σε μία ημέρα" }, "WIDGET_COLOR_LABEL": "Χρώμα Widget", - "WIDGET_BUBBLE_POSITION_LABEL": "Widget Bubble Position", - "WIDGET_BUBBLE_TYPE_LABEL": "Widget Bubble Type", + "WIDGET_BUBBLE_POSITION_LABEL": "Θέση Φυσαλίδας Widget", + "WIDGET_BUBBLE_TYPE_LABEL": "Τύπος Φυσαλίδας Widget", "WIDGET_BUBBLE_LAUNCHER_TITLE": { "DEFAULT": "Συνομιλήστε μαζί μας", - "LABEL": "Widget Bubble Launcher Title", + "LABEL": "Τίτλος Εκκίνησης Φυσαλίδας Widget", "PLACE_HOLDER": "Συνομιλήστε μαζί μας" }, "UPDATE": { - "BUTTON_TEXT": "Update Widget Settings", + "BUTTON_TEXT": "Ενημέρωση Ρυθμίσεων Widget", "API": { - "SUCCESS_MESSAGE": "Widget settings updated successfully", - "ERROR_MESSAGE": "Unable to update widget settings" + "SUCCESS_MESSAGE": "Οι ρυθμίσεις widget ενημερώθηκαν με επιτυχία", + "ERROR_MESSAGE": "Δεν είναι δυνατή η ενημέρωση ρυθμίσεων widget" } }, "WIDGET_VIEW_OPTION": { - "PREVIEW": "Preview", + "PREVIEW": "Προεπισκόπηση", "SCRIPT": "Script" }, "WIDGET_BUBBLE_POSITION": { - "LEFT": "Left", - "RIGHT": "Right" + "LEFT": "Αριστερά", + "RIGHT": "Δεξιά" }, "WIDGET_BUBBLE_TYPE": { "STANDARD": "Standard", - "EXPANDED_BUBBLE": "Expanded Bubble" + "EXPANDED_BUBBLE": "Εκτεταμένη Φυσαλίδα" } }, "WIDGET_SCREEN": { - "DEFAULT": "Default", - "CHAT": "Chat" + "DEFAULT": "Προεπιλογή", + "CHAT": "Συνομιλία" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Τυπικά έχετε απάντηση σε μερικά λεπτά", @@ -649,11 +651,11 @@ }, "BODY": { "TEAM_AVAILABILITY": { - "ONLINE": "We are Online", + "ONLINE": "Είμαστε online", "OFFLINE": "Προς το παρόν, είμαστε εκτός" }, - "USER_MESSAGE": "Hi", - "AGENT_MESSAGE": "Hello" + "USER_MESSAGE": "Γειά", + "AGENT_MESSAGE": "Γειά σας" }, "BRANDING_TEXT": "με την δύναμη του Chatwoot", "SCRIPT_SETTINGS": "\n window.chatwootSettings = {options};" diff --git a/app/javascript/dashboard/i18n/locale/el/integrations.json b/app/javascript/dashboard/i18n/locale/el/integrations.json index 5302f0bf5..b4a9e2c3f 100644 --- a/app/javascript/dashboard/i18n/locale/el/integrations.json +++ b/app/javascript/dashboard/i18n/locale/el/integrations.json @@ -86,49 +86,49 @@ "BUTTON_TEXT": "Σύνδεση" }, "DASHBOARD_APPS": { - "TITLE": "Dashboard Apps", - "HEADER_BTN_TXT": "Add a new dashboard app", - "SIDEBAR_TXT": "

Dashboard Apps

Dashboard Apps allow organizations to embed an application inside the Chatwoot dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that inside the dashboard to provide user information, their orders, or their previous payment history.

When you embed your application using the dashboard in Chatwoot, your application will get the context of the conversation and contact as a window event. Implement a listener for the message event on your page to receive the context.

To add a new dashboard app, click on the button 'Add a new dashboard app'.

", - "DESCRIPTION": "Dashboard Apps allow organizations to embed an application inside the dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that to provide user information, their orders, or their previous payment history.", + "TITLE": "Εφαρμογές Dashboard", + "HEADER_BTN_TXT": "Προσθήκη νέας εφαρμογής Dashboard", + "SIDEBAR_TXT": "

Εφαρμογές Dashboard

Οι εφαρμογές Dashboard επιτρέπουν σε οργανισμούς να ενσωματώσουν μια εφαρμογή μέσα στο ταμπλό Chatwoot για να παρέχουν το πλαίσιο για τους πράκτορες υποστήριξης πελατών. Αυτό το χαρακτηριστικό σας επιτρέπει να δημιουργήσετε μια εφαρμογή ανεξάρτητα και ενσωματωμένη που μέσα στον πίνακα ελέγχου για να παρέχει πληροφορίες χρήστη, τις παραγγελίες τους, ή το ιστορικό προηγούμενων πληρωμών τους.

Όταν ενσωματώσετε την εφαρμογή σας χρησιμοποιώντας το Dashboard στο Chatwoot, η εφαρμογή σας θα πάρει το πλαίσιο της συνομιλίας και θα επικοινωνήσει ως ένα παράθυρο εκδήλωσης. Εφαρμόστε έναν ακροατή για το γεγονός του μηνύματος στη σελίδα σας για να λάβετε το πλαίσιο.

Για να προσθέσετε μια νέα εφαρμογή ταμπλό, κάντε κλικ στο κουμπί 'Προσθήκη μιας νέας εφαρμογής Dashboard'.

", + "DESCRIPTION": "Οι εφαρμογές Dashboard επιτρέπουν στους οργανισμούς να ενσωματώσουν μια εφαρμογή μέσα στον πίνακα ελέγχου για να παρέχουν το περιεχόμενο για τους πράκτορες υποστήριξης πελατών. Αυτή η λειτουργία σας επιτρέπει να δημιουργήσετε μια εφαρμογή ανεξάρτητα και ενσωματωμένη που θα παρέχει πληροφορίες χρήστη, τις παραγγελίες τους, ή το ιστορικό προηγούμενων πληρωμών τους.", "LIST": { - "404": "There are no dashboard apps configured on this account yet", - "LOADING": "Fetching dashboard apps...", + "404": "Δεν έχουν δημιουργηθεί εφαρμογές Dashboard για αυτόν το λογαριασμό", + "LOADING": "Λήψη εφαρμογών dashboard ...", "TABLE_HEADER": [ "Όνομα", "Endpoint" ], - "EDIT_TOOLTIP": "Edit app", - "DELETE_TOOLTIP": "Delete app" + "EDIT_TOOLTIP": "Επεξεργασία εφαρμογής", + "DELETE_TOOLTIP": "Διαγραφή εφαρμογής" }, "FORM": { "TITLE_LABEL": "Όνομα", - "TITLE_PLACEHOLDER": "Enter a name for your dashboard app", - "TITLE_ERROR": "A name for the dashboard app is required", + "TITLE_PLACEHOLDER": "Εισάγετε όνομα για την εφαρμογή dashboard", + "TITLE_ERROR": "Απαιτείται ένα όνομα για την εφαρμογή dashboard", "URL_LABEL": "Endpoint", - "URL_PLACEHOLDER": "Enter the endpoint URL where your app is hosted", - "URL_ERROR": "A valid URL is required" + "URL_PLACEHOLDER": "Εισάγετε το URL του endpoint όπου φιλοξενείται η εφαρμογή σας", + "URL_ERROR": "Απαιτείται ένα έγκυρο URL" }, "CREATE": { - "HEADER": "Add a new dashboard app", + "HEADER": "Προσθήκη νέας εφαρμογής Dashboard", "FORM_SUBMIT": "Καταχώρηση", "FORM_CANCEL": "Άκυρο", - "API_SUCCESS": "Dashboard app configured successfully", - "API_ERROR": "We couldn't create an app. Please try again later" + "API_SUCCESS": "Η εφαρμογή dashboard ρυθμίστηκε επιτυχώς", + "API_ERROR": "Δεν μπορούμε να δημιουργήσουμε μια εφαρμογή. Παρακαλώ δοκιμάστε ξανά αργότερα" }, "UPDATE": { - "HEADER": "Edit dashboard app", + "HEADER": "Επεξεργασία εφαρμογής dashboard", "FORM_SUBMIT": "Ενημέρωση", "FORM_CANCEL": "Άκυρο", - "API_SUCCESS": "Dashboard app updated successfully", - "API_ERROR": "We couldn't update the app. Please try again later" + "API_SUCCESS": "Η εφαρμογή dashboard ενημερώθηκε με επιτυχία", + "API_ERROR": "Δεν ήταν δυνατή η ενημέρωση της εφαρμογής. Παρακαλώ δοκιμάστε ξανά αργότερα" }, "DELETE": { - "CONFIRM_YES": "Yes, delete it", - "CONFIRM_NO": "No, keep it", + "CONFIRM_YES": "Ναι, Διέγραψε την", + "CONFIRM_NO": "Όχι, Κράτησε την", "TITLE": "Επιβεβαίωση Διαγραφής", - "MESSAGE": "Are you sure to delete the app - %{appName}?", - "API_SUCCESS": "Dashboard app deleted successfully", - "API_ERROR": "We couldn't delete the app. Please try again later" + "MESSAGE": "Είστε βέβαιοι να διαγράψετε την εφαρμογή - %{appName};", + "API_SUCCESS": "Η εφαρμογή dashboard διαγράφηκε επιτυχώς", + "API_ERROR": "Δεν μπορούμε να διαγράψουμε την εφαρμογή. Παρακαλώ δοκιμάστε ξανά αργότερα" } } } diff --git a/app/javascript/dashboard/i18n/locale/el/macros.json b/app/javascript/dashboard/i18n/locale/el/macros.json new file mode 100644 index 000000000..439241619 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/el/macros.json @@ -0,0 +1,5 @@ +{ + "MACROS": { + "HEADER": "Μακροεντολές" + } +} diff --git a/app/javascript/dashboard/i18n/locale/el/settings.json b/app/javascript/dashboard/i18n/locale/el/settings.json index 69cd0a401..651e2de5d 100644 --- a/app/javascript/dashboard/i18n/locale/el/settings.json +++ b/app/javascript/dashboard/i18n/locale/el/settings.json @@ -20,17 +20,17 @@ "NOTE": "Η διεύθυνση email είναι η ταυτότητά σας και χρησιμοποιείται για την είσοδο (login) σας." }, "SEND_MESSAGE": { - "TITLE": "Hotkey to send messages", - "NOTE": "You can select a hotkey (either Enter or Cmd/Ctrl+Enter) based on your preference of writing.", - "UPDATE_SUCCESS": "Your settings have been updated successfully", + "TITLE": "Πλήκτρο συντόμευσης για αποστολή μηνυμάτων", + "NOTE": "Μπορείτε να επιλέξετε μια συντόμευση (είτε εισάγετε είτε Cmd/Ctrl+Enter) με βάση την προτίμηση για το γράψιμο.", + "UPDATE_SUCCESS": "Οι ρυθμίσεις σας έχουν ενημερωθεί με επιτυχία", "CARD": { "ENTER_KEY": { "HEADING": "Enter (↵)", - "CONTENT": "Send messages by pressing Enter key instead of clicking the send button." + "CONTENT": "Αποστολή μηνυμάτων πατώντας το πλήκτρο Enter αντί να πατήσετε το κουμπί αποστολής." }, "CMD_ENTER_KEY": { "HEADING": "Cmd/Ctrl + Enter (⌘ + ↵)", - "CONTENT": "Send messages by pressing Cmd/Ctrl + enter key instead of clicking the send button." + "CONTENT": "Αποστολή μηνυμάτων πατώντας το πλήκτρο Enter αντί να πατήσετε το κουμπί αποστολής." } } }, @@ -141,8 +141,8 @@ "TRAIL_BUTTON": "Αγόρασε τώρα", "DELETED_USER": "Διαγραμμένος Χρήστης", "ACCOUNT_SUSPENDED": { - "TITLE": "Account Suspended", - "MESSAGE": "Your account is suspended. Please reach out to the support team for more information." + "TITLE": "Αναστολή Λογαριασμού", + "MESSAGE": "Ο λογαριασμός σας έχει ανασταλεί. Επικοινωνήστε με την ομάδα υποστήριξης για περισσότερες πληροφορίες." } }, "COMPONENTS": { @@ -179,6 +179,7 @@ "CONTACTS": "Επαφές", "HOME": "Αρχική", "AGENTS": "Πράκτορες", + "AGENT_BOTS": "Bots", "INBOXES": "Κιβώτια Εισερχομένων", "NOTIFICATIONS": "Ειδοποιήσεις", "CANNED_RESPONSES": "Έτοιμες Απαντήσεις", @@ -189,8 +190,9 @@ "LABELS": "Ετικέτες", "CUSTOM_ATTRIBUTES": "Προσαρμοζόμενες Ιδιότητες", "AUTOMATION": "Αυτοματισμός", + "MACROS": "Μακροεντολές", "TEAMS": "Ομάδες", - "BILLING": "Billing", + "BILLING": "Χρεώσεις", "CUSTOM_VIEWS_FOLDER": "Φάκελοι", "CUSTOM_VIEWS_SEGMENTS": "Τμήματα", "ALL_CONTACTS": "Όλες Οι Επαφές", @@ -212,33 +214,33 @@ "REPORTS_OVERVIEW": "Επισκόπηση", "FACEBOOK_REAUTHORIZE": "Η σύνδεση Facebook έχει λήξει, παρακαλώ ξανασυνδεθείτε στο Facebook για να συνεχίσετε", "HELP_CENTER": { - "TITLE": "Help Center (Beta)", - "ALL_ARTICLES": "All Articles", - "MY_ARTICLES": "My Articles", - "DRAFT": "Draft", - "ARCHIVED": "Archived", + "TITLE": "Κέντρο Βοήθειας (Beta)", + "ALL_ARTICLES": "Όλα Τα Άρθρα", + "MY_ARTICLES": "Τα Άρθρα Μου", + "DRAFT": "Πρόχειρο", + "ARCHIVED": "Αρχειοθετημένο", "CATEGORY": "Κατηγορία", - "CATEGORY_EMPTY_MESSAGE": "No categories found" + "CATEGORY_EMPTY_MESSAGE": "Δεν βρέθηκαν κατηγορίες" }, - "DOCS": "Read docs" + "DOCS": "Ανάγνωση εγγράφων" }, "BILLING_SETTINGS": { - "TITLE": "Billing", + "TITLE": "Χρεώσεις", "CURRENT_PLAN": { - "TITLE": "Current Plan", - "PLAN_NOTE": "You are currently subscribed to the **%{plan}** plan with **%{quantity}** licenses" + "TITLE": "Τρέχον Πλάνο", + "PLAN_NOTE": "Αυτή τη στιγμή έχετε εγγραφεί στο πλάνο **%{plan}** με **%{quantity}** άδειες" }, "MANAGE_SUBSCRIPTION": { - "TITLE": "Manage your subscription", - "DESCRIPTION": "View your previous invoices, edit your billing details, or cancel your subscription.", - "BUTTON_TXT": "Go to the billing portal" + "TITLE": "Διαχειριστείτε τη συνδρομή σας", + "DESCRIPTION": "Δείτε τα προηγούμενα τιμολόγια σας, επεξεργαστείτε τα στοιχεία χρέωσης ή ακυρώστε τη συνδρομή σας.", + "BUTTON_TXT": "Μετάβαση στην πύλη χρέωσης" }, "CHAT_WITH_US": { - "TITLE": "Need help?", - "DESCRIPTION": "Do you face any issues in billing? We are here to help.", + "TITLE": "Χρειάζεστε βοήθεια;", + "DESCRIPTION": "Αντιμετωπίζετε οποιαδήποτε προβλήματα στην τιμολόγηση? Είμαστε εδώ για να βοηθήσουμε.", "BUTTON_TXT": "Συνομιλήστε μαζί μας" }, - "NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again." + "NO_BILLING_USER": "Ο λογαριασμός χρέωσης έχει ρυθμιστεί. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά." }, "CREATE_ACCOUNT": { "NO_ACCOUNT_WARNING": "Ωχ! Δεν μπορέσαμε να βρούμε κανένα λογαριασμό Chatwoot. Παρακαλούμε δημιουργήστε ένα νέο λογαριασμό για να συνεχίσετε.", diff --git a/app/javascript/dashboard/i18n/locale/he/chatlist.json b/app/javascript/dashboard/i18n/locale/he/chatlist.json index e669054ac..7f25908b8 100644 --- a/app/javascript/dashboard/i18n/locale/he/chatlist.json +++ b/app/javascript/dashboard/i18n/locale/he/chatlist.json @@ -54,12 +54,12 @@ "RECEIVED_VIA_EMAIL": "התקבל בדואר אלקטרוני", "VIEW_TWEET_IN_TWITTER": "צפה בציוץ בטוויטר", "REPLY_TO_TWEET": "הגב לציוץ זה", - "LINK_TO_STORY": "Go to instagram story", + "LINK_TO_STORY": "מעבר לסטורי באינסטגרם", "SENT": "נשלח בהצלחה", "NO_MESSAGES": "אין הודעות", "NO_CONTENT": "אין תוכן זמין", "HIDE_QUOTED_TEXT": "הסתר טקסט מצוטט", "SHOW_QUOTED_TEXT": "הצג טקסט מצוטט", - "MESSAGE_READ": "Read" + "MESSAGE_READ": "קרא" } } diff --git a/app/javascript/dashboard/i18n/locale/he/contact.json b/app/javascript/dashboard/i18n/locale/he/contact.json index c4f12e24c..236066ce7 100644 --- a/app/javascript/dashboard/i18n/locale/he/contact.json +++ b/app/javascript/dashboard/i18n/locale/he/contact.json @@ -3,11 +3,11 @@ "NOT_AVAILABLE": "לא זמין", "EMAIL_ADDRESS": "כתובת מייל", "PHONE_NUMBER": "מספר טלפון", - "IDENTIFIER": "Identifier", + "IDENTIFIER": "מזהה", "COPY_SUCCESSFUL": "הועתק ללוח בהצלחה", "COMPANY": "חברה", "LOCATION": "מיקום", - "BROWSER_LANGUAGE": "Browser Language", + "BROWSER_LANGUAGE": "שפת דפדפן", "CONVERSATION_TITLE": "פרטי שיחה", "VIEW_PROFILE": "צפה בפרופיל", "BROWSER": "דפדפן", @@ -75,8 +75,8 @@ "DELETE_NOTE": { "CONFIRM": { "TITLE": "אשר מחיקה", - "MESSAGE": "Are you want sure to delete this note?", - "YES": "Yes, Delete it", + "MESSAGE": "אתה בטוח שתרצה למחוק את ההערה הזו?", + "YES": "כן, מחק", "NO": "לא, השאר" } }, diff --git a/app/javascript/dashboard/i18n/locale/he/conversation.json b/app/javascript/dashboard/i18n/locale/he/conversation.json index 310f127f9..3a121e019 100644 --- a/app/javascript/dashboard/i18n/locale/he/conversation.json +++ b/app/javascript/dashboard/i18n/locale/he/conversation.json @@ -1,10 +1,10 @@ { "CONVERSATION": { "SELECT_A_CONVERSATION": "אנא בחר שיחה מהחלונית השמאלית", - "CSAT_REPLY_MESSAGE": "Please rate the conversation", + "CSAT_REPLY_MESSAGE": "נא דרג שיחה זו", "404": "Sorry, we cannot find the conversation. Please try again", - "SWITCH_VIEW_LAYOUT": "Switch the layout", - "DASHBOARD_APP_TAB_MESSAGES": "Messages", + "SWITCH_VIEW_LAYOUT": "החלף תצוגה", + "DASHBOARD_APP_TAB_MESSAGES": "הודעות", "UNVERIFIED_SESSION": "זהות המשתמש לא מְאוּמָתת", "NO_MESSAGE_1": "או - או! נראה שאין הודעות מלקוחות בתיבת הדואר הנכנס שלך.", "NO_MESSAGE_2": " לשלוח הודעה לעמוד שלך!", @@ -34,7 +34,7 @@ "REPLYING_TO": "אתה משיב ל:", "REMOVE_SELECTION": "הסר בחירה", "DOWNLOAD": "הורד", - "UNKNOWN_FILE_TYPE": "Unknown File", + "UNKNOWN_FILE_TYPE": "קובץ לא ידוע", "UPLOADING_ATTACHMENTS": "מעלה קובץ מצורף...", "SUCCESS_DELETE_MESSAGE": "ההודעה נמחקה בהצלחה", "FAIL_DELETE_MESSSAGE": "לא ניתן למחוק את ההודעה! נסה שוב", @@ -63,18 +63,18 @@ }, "CARD_CONTEXT_MENU": { "PENDING": "סמן כממתין", - "RESOLVED": "Mark as resolved", + "RESOLVED": "סמן כפתור", "REOPEN": "פתח מחדש את השיחה", "SNOOZE": { "TITLE": "Snooze", - "NEXT_REPLY": "Until next reply", - "TOMORROW": "Until tomorrow", - "NEXT_WEEK": "Until next week" + "NEXT_REPLY": "עד תגובה הבאה", + "TOMORROW": "עד מחר", + "NEXT_WEEK": "עד שבוע הבא" }, - "ASSIGN_AGENT": "Assign agent", + "ASSIGN_AGENT": "שייך סוכן", "ASSIGN_LABEL": "Assign label", - "AGENTS_LOADING": "Loading agents...", - "ASSIGN_TEAM": "Assign team", + "AGENTS_LOADING": "טוען סוכנים...", + "ASSIGN_TEAM": "שייך צוות", "API": { "AGENT_ASSIGNMENT": { "SUCCESFUL": "Conversation id %{conversationId} assigned to \"%{agentName}\"", @@ -86,18 +86,18 @@ }, "TEAM_ASSIGNMENT": { "SUCCESFUL": "Assigned team \"%{team}\" to conversation id %{conversationId}", - "FAILED": "Couldn't assign team. Please try again." + "FAILED": "השמה לצוות לא הצליחה, בבקשה נסה שנית." } } }, "FOOTER": { "MESSAGE_SIGN_TOOLTIP": "Message signature", - "ENABLE_SIGN_TOOLTIP": "Enable signature", - "DISABLE_SIGN_TOOLTIP": "Disable signature", + "ENABLE_SIGN_TOOLTIP": "אפשר חתימה", + "DISABLE_SIGN_TOOLTIP": "נטרל חתימה", "MSG_INPUT": "Shift + Enter עבור שורה חדשה. התחל עם '/' כדי לבחור תגובה מוכנה.", "PRIVATE_MSG_INPUT": "Shift + Enter עבור שורה חדשה. זה יהיה גלוי רק לסוכנים", "MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.", - "CLICK_HERE": "Click here to update" + "CLICK_HERE": "לחץ כאן כדי לעדכן" }, "REPLYBOX": { "REPLY": "הגב", @@ -108,12 +108,12 @@ "TIP_FORMAT_ICON": "הצג עורך טקסט עשיר", "TIP_EMOJI_ICON": "הצג בחירת אימוג'ים", "TIP_ATTACH_ICON": "הוסף קבצים", - "TIP_AUDIORECORDER_ICON": "Record audio", - "TIP_AUDIORECORDER_PERMISSION": "Allow access to audio", - "TIP_AUDIORECORDER_ERROR": "Could not open the audio", + "TIP_AUDIORECORDER_ICON": "הקלט אודיו", + "TIP_AUDIORECORDER_PERMISSION": "אפשר גישה לאודיו", + "TIP_AUDIORECORDER_ERROR": "לא יכול לפתוח אודיו", "DRAG_DROP": "גרור ושחרר כאן להוספת קובץ מצורף", - "START_AUDIO_RECORDING": "Start audio recording", - "STOP_AUDIO_RECORDING": "Stop audio recording", + "START_AUDIO_RECORDING": "התחל הקלטת אודיו", + "STOP_AUDIO_RECORDING": "עצור הקלטת אודיו", "": "", "EMAIL_HEAD": { "ADD_BCC": "הוסף bcc", @@ -131,11 +131,11 @@ }, "VISIBLE_TO_AGENTS": "פתקים פרטיים: רק אתה והצוות שלך יכולים לראות", "CHANGE_STATUS": "סטטוס השיחה השתנה", - "CHANGE_STATUS_FAILED": "Conversation status change failed", + "CHANGE_STATUS_FAILED": "סטטוס השיחה השתנה לנכשלה", "CHANGE_AGENT": "שיוך שיחה השתנתה", "CHANGE_AGENT_FAILED": "Assignee change failed", - "ASSIGN_LABEL_SUCCESFUL": "Label assigned successfully", - "ASSIGN_LABEL_FAILED": "Label assignment failed", + "ASSIGN_LABEL_SUCCESFUL": "סמן משימה כבוצעה בהצלחה", + "ASSIGN_LABEL_FAILED": "סמן משימה כנכשלה", "CHANGE_TEAM": "שיחת קבוצה השתנתה", "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB attachment limit", "MESSAGE_ERROR": "לא ניתן לשלוח הודעה, אנא נסה שוב מאוחר יותר", @@ -151,7 +151,7 @@ "CONTEXT_MENU": { "COPY": "העתק", "DELETE": "מחק", - "CREATE_A_CANNED_RESPONSE": "Add to canned responses" + "CREATE_A_CANNED_RESPONSE": "הוסף לתגובות מוכנות" } }, "EMAIL_TRANSCRIPT": { @@ -182,17 +182,17 @@ "TEAM_MEMBERS": { "TITLE": "הזמן את חברי הצוות שלך", "DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email addresses to the agent list.", - "NEW_LINK": "Click here to invite a team member" + "NEW_LINK": "לחץ כאן כדי להזמין חבר צוות" }, "INBOXES": { - "TITLE": "Connect Inboxes", + "TITLE": "התחבר לתיבות", "DESCRIPTION": "Connect various channels through which your customers would be talking to you. It can be a website live-chat, your Facebook or Twitter page or even your WhatsApp number.", - "NEW_LINK": "Click here to create an inbox" + "NEW_LINK": "לחץ כאן כדי ליצור תיבה" }, "LABELS": { "TITLE": "Organize conversations with labels", "DESCRIPTION": "Labels provide an easier way to categorize your conversation. Create some labels like #support-enquiry, #billing-question etc., so that you can use them in a conversation later.", - "NEW_LINK": "Click here to create tags" + "NEW_LINK": "לחץ כאן כדי ליצור תגיות" } }, "CONVERSATION_SIDEBAR": { @@ -200,7 +200,7 @@ "SELF_ASSIGN": "Assign to me", "TEAM_LABEL": "Assigned Team", "SELECT": { - "PLACEHOLDER": "None" + "PLACEHOLDER": "כלום" }, "ACCORDION": { "CONTACT_DETAILS": "Contact Details", @@ -233,7 +233,7 @@ } }, "EMAIL_HEADER": { - "FROM": "From", + "FROM": "מאת", "TO": "אל", "BCC": "Bcc", "CC": "עותק", diff --git a/app/javascript/dashboard/i18n/locale/he/generalSettings.json b/app/javascript/dashboard/i18n/locale/he/generalSettings.json index dfda9965a..2f7b4d580 100644 --- a/app/javascript/dashboard/i18n/locale/he/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/he/generalSettings.json @@ -14,7 +14,7 @@ "NOTE": "" }, "ACCOUNT_ID": { - "TITLE": "Account ID", + "TITLE": "מזהה חשבון", "NOTE": "This ID is required if you are building an API based integration" }, "NAME": { @@ -60,12 +60,12 @@ "NOTIFICATIONS_PAGE": { "HEADER": "התראות", "MARK_ALL_DONE": "סמן הכל כבוצע", - "DELETE_TITLE": "deleted", + "DELETE_TITLE": "נמחק", "UNREAD_NOTIFICATION": { - "TITLE": "Unread Notifications", - "ALL_NOTIFICATIONS": "View all notifications", - "LOADING_UNREAD_MESSAGE": "Loading unread notifications...", - "EMPTY_MESSAGE": "You have no unread notifications" + "TITLE": "התראות שלא נקראו", + "ALL_NOTIFICATIONS": "הצג את כל ההתראות", + "LOADING_UNREAD_MESSAGE": "טוען התראות שלא נקראו...", + "EMPTY_MESSAGE": "אין לך התראות שלא נקראו" }, "LIST": { "LOADING_MESSAGE": "טוען הודעות...", @@ -93,10 +93,10 @@ } }, "COMMAND_BAR": { - "SEARCH_PLACEHOLDER": "Search or jump to", + "SEARCH_PLACEHOLDER": "חפש או קפוץ ל", "SECTIONS": { - "GENERAL": "General", - "REPORTS": "Reports", + "GENERAL": "כללי", + "REPORTS": "דוחות", "CONVERSATION": "שיחה", "CHANGE_ASSIGNEE": "Change Assignee", "CHANGE_TEAM": "Change Team", diff --git a/app/javascript/dashboard/i18n/locale/he/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/he/inboxMgmt.json index 8dcdf0131..1b04432f9 100644 --- a/app/javascript/dashboard/i18n/locale/he/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/he/inboxMgmt.json @@ -158,7 +158,7 @@ }, "BANDWIDTH": { "ACCOUNT_ID": { - "LABEL": "Account ID", + "LABEL": "מזהה חשבון", "PLACEHOLDER": "Please enter your Bandwidth Account ID", "ERROR": "שדה חובה" }, @@ -239,7 +239,9 @@ }, "API_CALLBACK": { "TITLE": "כתובת אתר להתקשרות חוזרת", - "SUBTITLE": "You have to configure the webhook URL in facebook developer portal with the URL mentioned here." + "SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.", + "WEBHOOK_URL": "Webhook URL", + "WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token" }, "SUBMIT_BUTTON": "צור ערוץ וואטסאפ", "API": { @@ -357,7 +359,7 @@ }, "FINISH": { "TITLE": "תיבת הדואר הנכנס שלך מוכנה!", - "MESSAGE": "כעת תוכל ליצור קשר עם הלקוחות שלך דרך הערוץ החדש שלך. תמיכה שמחה ", + "MESSAGE": "כעת תוכל ליצור קשר עם הלקוחות שלך דרך הערוץ החדש שלך. תמיכה שמחה", "BUTTON_TEXT": "קח אותי לשם", "MORE_SETTINGS": "הגדרות נוספות", "WEBSITE_SUCCESS": "סיימת בהצלחה ליצור ערוץ אתר אינטרנט. העתק את הקוד המוצג למטה והדבק אותו באתר שלך. בפעם הבאה שלקוח ישתמש בצ'אט החי, השיחה תופיע אוטומטית בתיבת הדואר הנכנס שלך." diff --git a/app/javascript/dashboard/i18n/locale/he/settings.json b/app/javascript/dashboard/i18n/locale/he/settings.json index 380b13761..3216cf128 100644 --- a/app/javascript/dashboard/i18n/locale/he/settings.json +++ b/app/javascript/dashboard/i18n/locale/he/settings.json @@ -58,7 +58,7 @@ "AUDIO_NOTIFICATIONS_SECTION": { "TITLE": "Audio Notifications", "NOTE": "Enable audio notifications in dashboard for new messages and conversations.", - "NONE": "None", + "NONE": "כלום", "ASSIGNED": "Assigned Conversations", "ALL_CONVERSATIONS": "All Conversations" }, @@ -179,6 +179,7 @@ "CONTACTS": "איש קשר", "HOME": "בית", "AGENTS": "סוכנים", + "AGENT_BOTS": "Bots", "INBOXES": "תיבות דואר נכנס", "NOTIFICATIONS": "התראות", "CANNED_RESPONSES": "תגובות מוכנות", @@ -189,6 +190,7 @@ "LABELS": "Labels", "CUSTOM_ATTRIBUTES": "מאפיינים בהתאמה אישית", "AUTOMATION": "Automation", + "MACROS": "Macros", "TEAMS": "Teams", "BILLING": "Billing", "CUSTOM_VIEWS_FOLDER": "Folders", diff --git a/config/locales/el.yml b/config/locales/el.yml index df0cac548..d53161c16 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -23,7 +23,7 @@ el: reset_password_failure: Ωχ όχι! Δεν υπάρχει κάποιος χρήστης με το συγκεκριμένο email. errors: validations: - presence: must not be blank + presence: δεν πρέπει να είναι κενό webhook: invalid: Μη έγκυρα συμβάντα signup: @@ -33,17 +33,17 @@ el: failed: Η εγγραφή απέτυχε data_import: data_type: - invalid: Invalid data type + invalid: Μη έγκυρος τύπος δεδομένων contacts: import: failed: Το αρχείο είναι κενό email: - invalid: Invalid email + invalid: Ακατάλληλο email phone_number: - invalid: should be in e164 format + invalid: πρέπει να είναι σε μορφή e164 categories: locale: - unique: should be unique in the category and portal + unique: πρέπει να είναι μοναδικό στην κατηγορία και την πύλη inboxes: imap: socket_error: Παρακαλώ ελέγξτε τη σύνδεση δικτύου, τη διεύθυνση IMAP και προσπαθήστε ξανά. From 73f5595762877bcc1d15144cdf4bd7a3db34069d Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Tue, 18 Oct 2022 01:43:02 +0530 Subject: [PATCH 47/54] chore: Sync colors from dashboard to tailwind config (#5656) --- .../sidebarComponents/SecondaryNavItem.vue | 2 +- .../widgets/conversation/MessagesView.vue | 2 +- .../widgets/conversation/ReplyBox.vue | 2 +- .../conversationBulkActions/Index.vue | 2 +- .../widget-preview/components/Widget.vue | 6 +- .../helpcenter/components/PortalPopover.vue | 2 +- .../pages/categories/CategoryListItem.vue | 2 +- .../shared/assets/stylesheets/colors.scss | 28 +++---- tailwind.config.js | 74 ++++++++++--------- 9 files changed, 62 insertions(+), 58 deletions(-) diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index a7a6761cf..cd155bce8 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -321,7 +321,7 @@ export default { .beta { padding-right: var(--space-smaller) !important; padding-left: var(--space-smaller) !important; - margin-left: var(--space-half) !important; + margin-left: var(--space-smaller) !important; display: inline-block; font-size: var(--font-size-micro); font-weight: var(--font-weight-medium); diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 52a43091c..5b2d322e3 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -434,7 +434,7 @@ export default { &::before { transform: rotate(0deg); - left: var(--space-half); + left: var(--space-smaller); bottom: var(--space-minus-slab); } } diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index bbc2f017e..b5bc55235 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -970,7 +970,7 @@ export default { &::before { transform: rotate(0deg); - left: var(--space-half); + left: var(--space-smaller); bottom: var(--space-minus-slab); } } diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue index 70b0464cc..600ff4393 100644 --- a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue @@ -181,7 +181,7 @@ export default { color: var(--y-700); font-size: var(--font-size-mini); margin-top: var(--space-small); - padding: var(--space-half) var(--space-one); + padding: var(--space-smaller) var(--space-small); } .popover-animation-enter-active, diff --git a/app/javascript/dashboard/modules/widget-preview/components/Widget.vue b/app/javascript/dashboard/modules/widget-preview/components/Widget.vue index d2e5622a2..8ec864042 100644 --- a/app/javascript/dashboard/modules/widget-preview/components/Widget.vue +++ b/app/javascript/dashboard/modules/widget-preview/components/Widget.vue @@ -249,8 +249,8 @@ export default { display: flex; align-items: center; border-radius: calc(var(--border-radius-small) * 10); - height: calc(var(--space-three) * 2); - width: calc(var(--space-three) * 2); + height: calc(var(--space-large) * 2); + width: calc(var(--space-large) * 2); position: relative; overflow-wrap: anywhere; cursor: pointer; @@ -274,7 +274,7 @@ export default { display: inline; height: var(--space-medium); width: var(--space-micro); - left: var(--space-three); + left: var(--space-large); position: absolute; } diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue index 1f5eebe0e..67d1b33f1 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue @@ -104,7 +104,7 @@ export default { .new-popover-link { display: flex; align-items: center; - padding: var(--space-half) var(--space-one); + padding: var(--space-smaller) var(--space-small); background-color: var(--s-25); font-size: var(--font-size-mini); color: var(--s-500); diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/CategoryListItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/CategoryListItem.vue index 3a8b7a850..45b23e5ed 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/CategoryListItem.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/CategoryListItem.vue @@ -118,6 +118,6 @@ table { justify-content: center; color: var(--s-500); font-size: var(--font-size-default); - margin-top: var(--space-three); + margin-top: var(--space-large); } diff --git a/app/javascript/shared/assets/stylesheets/colors.scss b/app/javascript/shared/assets/stylesheets/colors.scss index 7c13fc39c..64b7f3f94 100644 --- a/app/javascript/shared/assets/stylesheets/colors.scss +++ b/app/javascript/shared/assets/stylesheets/colors.scss @@ -26,16 +26,16 @@ --g-800: #009000; --g-900: #007000; - --y-50: #fefde8; - --y-100: #fdfcc4; - --y-200: #fcf68c; - --y-300: #f9e736; - --y-400: #f6d819; - --y-500: #e6c00c; - --y-600: #c69608; - --y-700: #9e6b0a; + --y-50: #FEFDE8; + --y-100: #FDFCC4; + --y-200: #FCF68C; + --y-300: #F9E736; + --y-400: #F6D819; + --y-500: #E6C00C; + --y-600: #C69608; + --y-700: #9E6b0A; --y-800: #835510; - --y-900: #6f4514; + --y-900: #6F4514; --s-25: #F8FAFC; --s-50: #F1F5F8; @@ -50,11 +50,11 @@ --s-800: #293F51; --s-900: #1B2836; - --b-50: #f7f7f7; - --b-100: #ececed; - --b-200: #dddde0; - --b-300: #c6c7ca; - --b-400: #abacaf; + --b-50: #F7F7F7; + --b-100: #ECECED; + --b-200: #DDDDE0; + --b-300: #C6C7CA; + --b-400: #ABACAF; --b-500: #96979C; --b-600: #6E6F73; --b-700: #5A5B5F; diff --git a/tailwind.config.js b/tailwind.config.js index e839d00ec..90408a779 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -14,16 +14,18 @@ module.exports = { colors: { white: colors.white, woot: { - 50: '#E3F2FF', - 100: '#BBDDFF', - 200: '#8FC9FF', - 300: '#61B3FF', - 400: '#3FA3FF', + 25: '#F5FAFF', + 50: '#EBF5FF', + 75: '#D6EBFF', + 100: '#C2E1FF', + 200: '#99CEFF', + 300: '#70BAFF', + 400: '#47A6FF', 500: '#1F93FF', - 600: '#2284F0', - 700: '#2272DC', - 800: '#2161CA', - 900: '#1F41AB', + 600: '#1976CC', + 700: '#135899', + 800: '#0C3B66', + 900: '#061D33', }, green: { 50: '#E6F8E6', @@ -38,38 +40,40 @@ module.exports = { 900: '#007000', }, yellow: { - 50: '#FFFEE8', - 100: '#FFFAC5', - 200: '#FFF69E', - 300: '#FEF176', - 400: '#FCEC56', - 500: '#F9E736', - 600: '#FFDD3A', - 700: '#FFC532', - 800: '#FDAD2A', - 900: '#F9841B', + 50: '#FEFDE8', + 100: '#FDFCC4', + 200: '#FCF68C', + 300: '#F9E736', + 400: '#F6D819', + 500: '#E6C00C', + 600: '#C69608', + 700: '#9E6b0A', + 800: '#835510', + 900: '#6F4514', }, slate: { - 50: '#F4F6FB', - 100: '#C8D6E6', - 200: '#ABBACE', - 300: '#8C9EB6', - 400: '#7489A4', - 500: '#5D7592', - 600: '#506781', - 700: '#40546B', - 800: '#314155', - 900: '#1F2D3D', + 25: '#F8FAFC', + 50: '#F1F5F8', + 75: '#EBF0F5', + 100: ' #E4EBF1', + 200: ' #C9D7E3', + 300: ' #AEC3D5', + 400: ' #93AFC8', + 500: ' #779BBB', + 600: ' #446888', + 700: ' #37546D', + 800: ' #293F51', + 900: ' #1B2836', }, black: { - 50: '#F8F9FE', - 100: '#F2F3F7', - 200: '#E9EAEF', - 300: '#DADBDF', - 400: '#B6B7BB', + 50: '#F7F7F7', + 100: '#ECECED', + 200: '#DDDDE0', + 300: '#C6C7CA', + 400: '#ABACAF', 500: '#96979C', 600: '#6E6F73', - 700: '#3C4858', + 700: '#5A5B5F', 800: '#3C3D40', 900: '#1B1C1F', }, From 20b4a91122e9b5db5ccc0546c06783a6c08fa247 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Mon, 17 Oct 2022 14:59:44 -0700 Subject: [PATCH 48/54] chore: Add feature flags in the settings console (#5657) --- .../dashboard/components/layout/Sidebar.vue | 10 +++---- .../layout/config/sidebarItems/settings.js | 28 +++++++++++++------ .../layout/sidebarComponents/Secondary.vue | 17 ++++++++--- .../sidebarComponents/SecondaryNavItem.vue | 6 ++-- .../widgets/WootWriter/ReplyBottomPanel.vue | 15 ++++++++-- app/javascript/dashboard/featureFlags.js | 13 +++++++++ .../dashboard/commands/goToCommandHotKeys.js | 22 ++++++++++++++- .../settings/account/account.routes.js | 2 +- .../dashboard/settings/settings.routes.js | 2 +- .../FluentIcon/dashboard-icons.json | 1 + .../account_features_field/_form.html.erb | 1 + config/features.yml | 18 ++++++++++++ ...20221017201914_add_features_to_accounts.rb | 20 +++++++++++++ db/schema.rb | 2 +- 14 files changed, 130 insertions(+), 27 deletions(-) create mode 100644 app/javascript/dashboard/featureFlags.js create mode 100644 db/migrate/20221017201914_add_features_to_accounts.rb diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 314b594d2..b919be2cd 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -73,14 +73,14 @@ export default { computed: { ...mapGetters({ - currentUser: 'getCurrentUser', - globalConfig: 'globalConfig/get', - isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance', - isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', - inboxes: 'inboxes/getInboxes', accountId: 'getCurrentAccountId', currentRole: 'getCurrentRole', + currentUser: 'getCurrentUser', + globalConfig: 'globalConfig/get', + inboxes: 'inboxes/getInboxes', + isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance', isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', + isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', labels: 'labels/getLabelsOnSidebar', teams: 'teams/getMyTeams', }), diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index 83a0c2309..768e42ea5 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -1,3 +1,4 @@ +import { FEATURE_FLAGS } from '../../../../featureFlags'; import { frontendURL } from '../../../../helper/URLHelper'; const settings = accountId => ({ @@ -38,12 +39,20 @@ const settings = accountId => ({ 'settings_teams_new', ], menuItems: [ + { + icon: 'briefcase', + label: 'ACCOUNT_SETTINGS', + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/settings/general`), + toStateName: 'general_settings_index', + }, { icon: 'people', label: 'AGENTS', hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/agents/list`), toStateName: 'agent_list', + featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT, }, { icon: 'people-team', @@ -51,6 +60,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/teams/list`), toStateName: 'settings_teams_list', + featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT, }, { icon: 'mail-inbox-all', @@ -58,6 +68,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`), toStateName: 'settings_inbox_list', + featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT, }, { icon: 'tag', @@ -65,6 +76,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/labels/list`), toStateName: 'labels_list', + featureFlag: FEATURE_FLAGS.LABELS, }, { icon: 'code', @@ -74,6 +86,7 @@ const settings = accountId => ({ `accounts/${accountId}/settings/custom-attributes/list` ), toStateName: 'attributes_list', + featureFlag: FEATURE_FLAGS.CUSTOM_ATTRIBUTES, }, { icon: 'automation', @@ -82,6 +95,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/automation/list`), toStateName: 'automation_list', + featureFlag: FEATURE_FLAGS.AUTOMATIONS, }, { icon: 'bot', @@ -90,7 +104,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/agent-bots`), toStateName: 'agent_bots', - featureFlagKey: 'agent_bots', + featureFlag: FEATURE_FLAGS.AGENT_BOTS, }, { icon: 'flash-settings', @@ -99,7 +113,7 @@ const settings = accountId => ({ toState: frontendURL(`accounts/${accountId}/settings/macros`), toStateName: 'macros_wrapper', beta: true, - featureFlagKey: 'macros', + featureFlag: FEATURE_FLAGS.MACROS, }, { icon: 'chat-multiple', @@ -109,6 +123,7 @@ const settings = accountId => ({ `accounts/${accountId}/settings/canned-response/list` ), toStateName: 'canned_list', + featureFlag: FEATURE_FLAGS.CANNED_RESPONSES, }, { icon: 'flash-on', @@ -116,6 +131,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/integrations`), toStateName: 'settings_integrations', + featureFlag: FEATURE_FLAGS.INTEGRATIONS, }, { icon: 'star-emphasis', @@ -123,6 +139,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/applications`), toStateName: 'settings_applications', + featureFlag: FEATURE_FLAGS.INTEGRATIONS, }, { icon: 'credit-card-person', @@ -132,13 +149,6 @@ const settings = accountId => ({ toStateName: 'billing_settings_index', showOnlyOnCloud: true, }, - { - icon: 'settings', - label: 'ACCOUNT_SETTINGS', - hasSubMenu: false, - toState: frontendURL(`accounts/${accountId}/settings/general`), - toStateName: 'general_settings_index', - }, ], }); diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue index e68fcc3a4..b8aa21b30 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue @@ -20,6 +20,8 @@ import { frontendURL } from '../../../helper/URLHelper'; import SecondaryNavItem from './SecondaryNavItem.vue'; import AccountContext from './AccountContext.vue'; +import { mapGetters } from 'vuex'; +import { FEATURE_FLAGS } from '../../../featureFlags'; export default { components: { @@ -61,6 +63,10 @@ export default { }, }, computed: { + ...mapGetters({ + accountId: 'getCurrentAccountId', + isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', + }), hasSecondaryMenu() { return this.menuConfig.menuItems && this.menuConfig.menuItems.length; }, @@ -89,7 +95,7 @@ export default { icon: 'folder', label: 'INBOXES', hasSubMenu: true, - newLink: true, + newLink: this.showNewLink(FEATURE_FLAGS.INBOX_MANAGEMENT), newLinkTag: 'NEW_INBOX', key: 'inbox', toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`), @@ -117,7 +123,7 @@ export default { icon: 'number-symbol', label: 'LABELS', hasSubMenu: true, - newLink: true, + newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT), newLinkTag: 'NEW_LABEL', key: 'label', toState: frontendURL(`accounts/${this.accountId}/settings/labels`), @@ -141,7 +147,7 @@ export default { label: 'TAGGED_WITH', hasSubMenu: true, key: 'label', - newLink: true, + newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT), newLinkTag: 'NEW_LABEL', toState: frontendURL(`accounts/${this.accountId}/settings/labels`), toStateName: 'labels_list', @@ -163,7 +169,7 @@ export default { icon: 'people-team', label: 'TEAMS', hasSubMenu: true, - newLink: true, + newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT), newLinkTag: 'NEW_TEAM', key: 'team', toState: frontendURL(`accounts/${this.accountId}/settings/teams/new`), @@ -238,6 +244,9 @@ export default { toggleAccountModal() { this.$emit('toggle-accounts'); }, + showNewLink(featureFlag) { + return this.isFeatureEnabledonAccount(this.accountId, featureFlag); + }, }, }; diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index cd155bce8..8661a488f 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -123,12 +123,12 @@ export default { return !!this.menuItem.children; }, isMenuItemVisible() { - if (!this.menuItem.featureFlagKey) { + if (!this.menuItem.featureFlag) { return true; } return this.isFeatureEnabledonAccount( this.accountId, - this.menuItem.featureFlagKey + this.menuItem.featureFlag ); }, isInboxConversation() { @@ -217,7 +217,7 @@ export default { } }, showItem(item) { - return this.isAdmin && item.newLink !== undefined; + return this.isAdmin && !!item.newLink; }, onClickOpen() { this.$emit('open'); diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index 9ace1ceb2..d26264cc2 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -110,13 +110,15 @@ import { hasPressedAltAndAKey } from 'shared/helpers/KeyboardHelpers'; import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import inboxMixin from 'shared/mixins/inboxMixin'; - +import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { ALLOWED_FILE_TYPES, ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP, } from 'shared/constants/messages'; import { REPLY_EDITOR_MODES } from './constants'; +import { mapGetters } from 'vuex'; + export default { name: 'ReplyBottomPanel', components: { FileUpload }, @@ -200,6 +202,10 @@ export default { }, }, computed: { + ...mapGetters({ + accountId: 'getCurrentAccountId', + isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', + }), isNote() { return this.mode === REPLY_EDITOR_MODES.NOTE; }, @@ -217,7 +223,12 @@ export default { return this.showFileUpload || this.isNote; }, showAudioRecorderButton() { - return this.showAudioRecorder; + return ( + this.isFeatureEnabledonAccount( + this.accountId, + FEATURE_FLAGS.VOICE_RECORDER + ) && this.showAudioRecorder + ); }, showAudioPlayStopButton() { return this.showAudioRecorder && this.isRecordingAudio; diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js new file mode 100644 index 000000000..c6bc736cf --- /dev/null +++ b/app/javascript/dashboard/featureFlags.js @@ -0,0 +1,13 @@ +export const FEATURE_FLAGS = { + AGENT_BOTS: 'agent_bots', + AGENT_MANAGEMENT: 'agent_management', + AUTOMATIONS: 'automations', + CANNED_RESPONSES: 'canned_responses', + CUSTOM_ATTRIBUTES: 'custom_attributes', + INBOX_MANAGEMENT: 'inbox_management', + INTEGRATIONS: 'integrations', + LABELS: 'labels', + MACROS: 'macros', + TEAM_MANAGEMENT: 'team_management', + VOICE_RECORDER: 'voice_recorder', +}; diff --git a/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js index 9a5b09f2e..b1d17dcbb 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js +++ b/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js @@ -16,6 +16,8 @@ import { ICON_CONVERSATION_REPORTS, } from './CommandBarIcons'; import { frontendURL } from '../../../helper/URLHelper'; +import { mapGetters } from 'vuex'; +import { FEATURE_FLAGS } from '../../../featureFlags'; const GO_TO_COMMANDS = [ { @@ -86,6 +88,7 @@ const GO_TO_COMMANDS = [ id: 'open_agent_settings', section: 'COMMAND_BAR.SECTIONS.SETTINGS', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_AGENTS', + featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT, icon: ICON_AGENT_REPORTS, path: accountId => `accounts/${accountId}/settings/agents/list`, role: ['administrator'], @@ -93,6 +96,7 @@ const GO_TO_COMMANDS = [ { id: 'open_team_settings', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_TEAMS', + featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT, section: 'COMMAND_BAR.SECTIONS.SETTINGS', icon: ICON_TEAM_REPORTS, path: accountId => `accounts/${accountId}/settings/teams/list`, @@ -101,6 +105,7 @@ const GO_TO_COMMANDS = [ { id: 'open_inbox_settings', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_INBOXES', + featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT, section: 'COMMAND_BAR.SECTIONS.SETTINGS', icon: ICON_INBOXES, path: accountId => `accounts/${accountId}/settings/inboxes/list`, @@ -109,6 +114,7 @@ const GO_TO_COMMANDS = [ { id: 'open_label_settings', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_LABELS', + featureFlag: FEATURE_FLAGS.LABELS, section: 'COMMAND_BAR.SECTIONS.SETTINGS', icon: ICON_LABELS, path: accountId => `accounts/${accountId}/settings/labels/list`, @@ -117,6 +123,7 @@ const GO_TO_COMMANDS = [ { id: 'open_canned_response_settings', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_CANNED_RESPONSES', + featureFlag: FEATURE_FLAGS.CANNED_RESPONSES, section: 'COMMAND_BAR.SECTIONS.SETTINGS', icon: ICON_CANNED_RESPONSE, path: accountId => `accounts/${accountId}/settings/canned-response/list`, @@ -125,6 +132,7 @@ const GO_TO_COMMANDS = [ { id: 'open_applications_settings', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_APPLICATIONS', + featureFlag: FEATURE_FLAGS.INTEGRATIONS, section: 'COMMAND_BAR.SECTIONS.SETTINGS', icon: ICON_APPS, path: accountId => `accounts/${accountId}/settings/applications`, @@ -158,8 +166,20 @@ const GO_TO_COMMANDS = [ export default { computed: { + ...mapGetters({ + accountId: 'getCurrentAccountId', + isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', + }), goToCommandHotKeys() { - let commands = GO_TO_COMMANDS; + let commands = GO_TO_COMMANDS.filter(cmd => { + if (cmd.featureFlag) { + return this.isFeatureEnabledonAccount( + this.accountId, + cmd.featureFlag + ); + } + return true; + }); if (!this.isAdmin) { commands = commands.filter(command => command.role.includes('agent')); diff --git a/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js b/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js index c2ef17129..5e3df43b4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js @@ -10,7 +10,7 @@ export default { component: SettingsContent, props: { headerTitle: 'GENERAL_SETTINGS.TITLE', - icon: 'settings', + icon: 'briefcase', showNewButton: false, }, children: [ diff --git a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js index 7c8cc11fa..794451e79 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js @@ -25,7 +25,7 @@ export default { roles: ['administrator', 'agent'], redirect: () => { if (store.getters.getCurrentRole === 'administrator') { - return frontendURL('accounts/:accountId/settings/agents'); + return frontendURL('accounts/:accountId/settings/general'); } return frontendURL('accounts/:accountId/settings/canned-response'); }, diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index 9a5eaa893..bf6f4127d 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -31,6 +31,7 @@ "book-open-globe-outline": "M3.5 5.75a.25.25 0 0 1 .25-.25H10c.69 0 1.25.56 1.25 1.25v8.959a6.49 6.49 0 0 1 1.5-2.646V6.75c0-.69.56-1.25 1.25-1.25h6.25a.25.25 0 0 1 .25.25v5.982A6.518 6.518 0 0 1 22 12.81V5.75A1.75 1.75 0 0 0 20.25 4H14c-.788 0-1.499.331-2 .863A2.742 2.742 0 0 0 10 4H3.75A1.75 1.75 0 0 0 2 5.75v12.5c0 .966.784 1.75 1.75 1.75H10c.495 0 .96-.13 1.36-.36a6.473 6.473 0 0 1-.343-1.663A1.248 1.248 0 0 1 10 18.5H3.75a.25.25 0 0 1-.25-.25V5.75ZM16.007 17c.04-1.415.248-2.669.553-3.585.171-.513.364-.893.554-1.134.195-.247.329-.281.386-.281.057 0 .192.034.386.281.19.241.383.62.554 1.134.305.916.513 2.17.553 3.585h-2.986Zm-.396-3.9c.108-.323.23-.622.368-.887A5.504 5.504 0 0 0 12.023 17h2.984c.04-1.5.26-2.866.604-3.9Zm3.778 0a6.133 6.133 0 0 0-.368-.887A5.504 5.504 0 0 1 22.978 17h-2.985c-.04-1.5-.26-2.866-.604-3.9Zm.604 4.9h2.985a5.504 5.504 0 0 1-3.957 4.787c.138-.265.26-.564.368-.886.345-1.035.564-2.4.604-3.901Zm-2.107 4.719c-.194.247-.329.281-.386.281-.057 0-.191-.034-.386-.281-.19-.241-.383-.62-.554-1.135-.305-.915-.513-2.17-.553-3.584h2.986c-.04 1.415-.248 2.669-.553 3.584-.171.514-.364.894-.554 1.135ZM12.023 18a5.504 5.504 0 0 0 3.956 4.787 6.133 6.133 0 0 1-.367-.886c-.346-1.035-.565-2.4-.605-3.901h-2.984Z", "bot-outline": "M17.753 14a2.25 2.25 0 0 1 2.25 2.25v.905a3.75 3.75 0 0 1-1.307 2.846C17.13 21.345 14.89 22 12 22c-2.89 0-5.128-.656-6.691-2a3.75 3.75 0 0 1-1.306-2.843v-.908A2.25 2.25 0 0 1 6.253 14h11.5Zm0 1.5h-11.5a.75.75 0 0 0-.75.75v.908c0 .655.286 1.278.784 1.706C7.545 19.945 9.44 20.502 12 20.502c2.56 0 4.458-.557 5.719-1.64a2.25 2.25 0 0 0 .784-1.706v-.906a.75.75 0 0 0-.75-.75ZM11.898 2.008 12 2a.75.75 0 0 1 .743.648l.007.102V3.5h3.5a2.25 2.25 0 0 1 2.25 2.25v4.505a2.25 2.25 0 0 1-2.25 2.25h-8.5a2.25 2.25 0 0 1-2.25-2.25V5.75A2.25 2.25 0 0 1 7.75 3.5h3.5v-.749a.75.75 0 0 1 .648-.743L12 2l-.102.007ZM16.25 5h-8.5a.75.75 0 0 0-.75.75v4.505c0 .414.336.75.75.75h8.5a.75.75 0 0 0 .75-.75V5.75a.75.75 0 0 0-.75-.75Zm-6.5 1.5a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Zm4.492 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z", "building-bank-outline": "M13.032 2.325a1.75 1.75 0 0 0-2.064 0L3.547 7.74c-.978.713-.473 2.26.736 2.26H4.5v5.8A2.75 2.75 0 0 0 3 18.25v1.5c0 .413.336.75.75.75h16.5a.75.75 0 0 0 .75-.75v-1.5a2.75 2.75 0 0 0-1.5-2.45V10h.217c1.21 0 1.713-1.547.736-2.26l-7.421-5.416Zm-1.18 1.211a.25.25 0 0 1 .295 0L18.95 8.5H5.05l6.803-4.964ZM18 10v5.5h-2V10h2Zm-3.5 0v5.5h-1.75V10h1.75Zm-3.25 0v5.5H9.5V10h1.75Zm-5.5 7h12.5c.69 0 1.25.56 1.25 1.25V19h-15v-.75c0-.69.56-1.25 1.25-1.25ZM6 15.5V10h2v5.5H6Z", + "briefcase-outline": "M8.75 3h6.5a.75.75 0 0 1 .743.648L16 3.75V7h1.75A3.25 3.25 0 0 1 21 10.25v6.5A3.25 3.25 0 0 1 17.75 20H6.25A3.25 3.25 0 0 1 3 16.75v-6.5A3.25 3.25 0 0 1 6.25 7H8V3.75a.75.75 0 0 1 .648-.743L8.75 3h6.5-6.5Zm9 5.5H6.25a1.75 1.75 0 0 0-1.75 1.75v6.5c0 .966.784 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-6.5a1.75 1.75 0 0 0-1.75-1.75Zm-3.25-4h-5V7h5V4.5Z", "calendar-clock-outline": [ "M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z", "M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z" diff --git a/app/views/fields/account_features_field/_form.html.erb b/app/views/fields/account_features_field/_form.html.erb index 823a043a6..c067e81c1 100644 --- a/app/views/fields/account_features_field/_form.html.erb +++ b/app/views/fields/account_features_field/_form.html.erb @@ -4,5 +4,6 @@
<% field.data.each do |key,val| %> <%= key %>: <%= check_box "enabled_features", "feature_#{key}", { checked: val }, true, false %> +
<% end %>
diff --git a/config/features.yml b/config/features.yml index ec779cd08..77d594fcf 100644 --- a/config/features.yml +++ b/config/features.yml @@ -19,3 +19,21 @@ enabled: false - name: macros enabled: false +- name: agent_management + enabled: true +- name: team_management + enabled: true +- name: inbox_management + enabled: true +- name: labels + enabled: true +- name: custom_attributes + enabled: true +- name: automations + enabled: true +- name: canned_responses + enabled: true +- name: integrations + enabled: true +- name: voice_recorder + enabled: true diff --git a/db/migrate/20221017201914_add_features_to_accounts.rb b/db/migrate/20221017201914_add_features_to_accounts.rb new file mode 100644 index 000000000..60f47f7d3 --- /dev/null +++ b/db/migrate/20221017201914_add_features_to_accounts.rb @@ -0,0 +1,20 @@ +class AddFeaturesToAccounts < ActiveRecord::Migration[6.1] + def change + Account.find_in_batches do |account_batch| + account_batch.each do |account| + account.enable_features( + 'agent_management', + 'automations', + 'canned_responses', + 'custom_attributes', + 'inbox_management', + 'integrations', + 'labels', + 'team_management', + 'voice_recorder' + ) + account.save! + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index d719bc9f7..d989b754b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_10_10_212946) do +ActiveRecord::Schema.define(version: 2022_10_17_201914) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" From 444809cc683b4b107569d5194440cc59c38b17a8 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 18 Oct 2022 03:30:02 +0530 Subject: [PATCH 49/54] fix: Added validation for check box in the pre-chat form (#5648) --- app/javascript/widget/components/PreChat/Form.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index 1649068c6..3da347966 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -245,6 +245,7 @@ export default { text: null, select: null, number: null, + checkbox: false, }; const validationKeys = Object.keys(validations); const validation = 'bail|required'; From 1c44e43c437ebc6dba1dab92a710c08ccf2594bd Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Tue, 18 Oct 2022 04:29:18 +0530 Subject: [PATCH 50/54] fix: Fix overflow issue for category name in article list (#5658) Co-authored-by: Pranav Raj S --- .../helpcenter/components/ArticleItem.vue | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue index b602baaa8..254302074 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue @@ -4,7 +4,7 @@
-
+
{{ title }}
@@ -20,7 +20,12 @@ class="fs-small button clear link secondary" :to="getCategoryRoute(category.slug)" > - {{ category.name }} + + {{ category.name }} + @@ -155,4 +160,9 @@ td { } } } + +.category-link-content { + max-width: 16rem; + line-height: 1.5; +} From 2423def8e8c62e885d72da9eb9812e8cdc624d64 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 17 Oct 2022 17:36:56 -0700 Subject: [PATCH 51/54] chore: Add attachments key to `message_created` webhook payload (#5659) - Add attachments key to `message_created` webhook payload --- app/listeners/webhook_listener.rb | 2 +- app/models/message.rb | 4 +++- app/models/webhook.rb | 4 ++-- spec/models/message_spec.rb | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/listeners/webhook_listener.rb b/app/listeners/webhook_listener.rb index 11a86e639..81d2c6b0a 100644 --- a/app/listeners/webhook_listener.rb +++ b/app/listeners/webhook_listener.rb @@ -54,7 +54,7 @@ class WebhookListener < BaseListener private def deliver_account_webhooks(payload, inbox) - inbox.account.webhooks.account.each do |webhook| + inbox.account.webhooks.account_type.each do |webhook| next unless webhook.subscriptions.include?(payload[:event]) WebhookJob.perform_later(webhook.url, payload) diff --git a/app/models/message.rb b/app/models/message.rb index 2930786f9..a3f63dbe9 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -116,7 +116,7 @@ class Message < ApplicationRecord end def webhook_data - { + data = { account: account.webhook_data, additional_attributes: additional_attributes, content_attributes: content_attributes, @@ -131,6 +131,8 @@ class Message < ApplicationRecord sender: sender.try(:webhook_data), source_id: source_id } + data.merge!(attachments: attachments.map(&:push_event_data)) if attachments.present? + data end def content diff --git a/app/models/webhook.rb b/app/models/webhook.rb index fe97fe583..5b0095093 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -5,7 +5,7 @@ # id :bigint not null, primary key # subscriptions :jsonb # url :string -# webhook_type :integer default("account") +# webhook_type :integer default("account_type") # created_at :datetime not null # updated_at :datetime not null # account_id :integer @@ -23,7 +23,7 @@ class Webhook < ApplicationRecord validates :account_id, presence: true validates :url, uniqueness: { scope: [:account_id] }, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]) validate :validate_webhook_subscriptions - enum webhook_type: { account: 0, inbox: 1 } + enum webhook_type: { account_type: 0, inbox_type: 1 } ALLOWED_WEBHOOK_EVENTS = %w[conversation_status_changed conversation_updated conversation_created message_created message_updated webwidget_triggered].freeze diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb index 4940d704a..aeff941da 100644 --- a/spec/models/message_spec.rb +++ b/spec/models/message_spec.rb @@ -33,6 +33,21 @@ RSpec.describe Message, type: :model do end end + context 'with webhook_data' do + it 'contains the message attachment when attachment is present' do + message = create(:message) + attachment = message.attachments.new(account_id: message.account_id, file_type: :image) + attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png') + attachment.save! + expect(message.webhook_data.key?(:attachments)).to be true + end + + it 'does not contain the message attachment when attachment is not present' do + message = create(:message) + expect(message.webhook_data.key?(:attachments)).to be false + end + end + context 'when message is created' do let(:message) { build(:message, account: create(:account)) } From e19c6d567137a9620801233854d2ade13c2d4017 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Mon, 17 Oct 2022 18:52:51 -0700 Subject: [PATCH 52/54] chore: Add editor toggle for API inbox (#5660) --- .../layout/sidebarComponents/Secondary.vue | 1 - .../widgets/WootWriter/ReplyBottomPanel.vue | 13 +++++++++++-- .../widgets/conversation/ReplyBox.vue | 17 +++++++++++++++-- .../dashboard/store/modules/inboxes.js | 4 ++-- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue index b8aa21b30..1d8ced445 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue @@ -64,7 +64,6 @@ export default { }, computed: { ...mapGetters({ - accountId: 'getCurrentAccountId', isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', }), hasSecondaryMenu() { diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index d26264cc2..a8c26201e 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -11,7 +11,6 @@ size="small" @click="toggleEmojiPicker" /> - + { return !template.components.some( i => i.format === 'IMAGE' || i.format === 'VIDEO' From 71ca530292738e460563f57dc18bb550a6e60768 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Mon, 17 Oct 2022 18:59:22 -0700 Subject: [PATCH 53/54] fix: Fix typo in help center (#5661) --- app/javascript/dashboard/i18n/locale/en/helpCenter.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/i18n/locale/en/helpCenter.json b/app/javascript/dashboard/i18n/locale/en/helpCenter.json index 84ba9b8c9..1f22ba5bd 100644 --- a/app/javascript/dashboard/i18n/locale/en/helpCenter.json +++ b/app/javascript/dashboard/i18n/locale/en/helpCenter.json @@ -307,7 +307,7 @@ "PUBLISH_ARTICLE": { "API": { "ERROR": "Error while publishing article", - "SUCCESS": "Article publishied successfully" + "SUCCESS": "Article published successfully" } }, "ARCHIVE_ARTICLE": { From 2f7a16ae16848449f30cbd9ecb2ff85d7c822371 Mon Sep 17 00:00:00 2001 From: Sojan Date: Mon, 17 Oct 2022 19:31:53 -0700 Subject: [PATCH 54/54] Bump version to 2.10.0 --- config/app.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/app.yml b/config/app.yml index d5b65c1fc..518c981ac 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '2.9.1' + version: '2.10.0' development: <<: *shared diff --git a/package.json b/package.json index a62526801..d3476875b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "2.9.1", + "version": "2.10.0", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}",