From 648c4caca188a4fdaa9c953950f01a8b66ffec8e Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 29 Jan 2024 20:09:17 +0400 Subject: [PATCH 01/35] chore: Move EE OpenAI spec to correct folder (#8805) - We previously had this spec inside the enterprise folder which wouldn't be picked up by our build process, so moving to the correct folder instead. Co-authored-by: Shivam Mishra --- .../openai/processor_service_spec.rb | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) rename {enterprise/spec => spec/enterprise/lib}/integrations/openai/processor_service_spec.rb (74%) diff --git a/enterprise/spec/integrations/openai/processor_service_spec.rb b/spec/enterprise/lib/integrations/openai/processor_service_spec.rb similarity index 74% rename from enterprise/spec/integrations/openai/processor_service_spec.rb rename to spec/enterprise/lib/integrations/openai/processor_service_spec.rb index 966062d37..2a98514cf 100644 --- a/enterprise/spec/integrations/openai/processor_service_spec.rb +++ b/spec/enterprise/lib/integrations/openai/processor_service_spec.rb @@ -44,6 +44,8 @@ RSpec.describe Integrations::Openai::ProcessorService do create(:label, account: account) create(:label, account: account) + + hook.settings['label_suggestion'] = 'true' end it 'returns the label suggestions' do @@ -81,5 +83,26 @@ RSpec.describe Integrations::Openai::ProcessorService do expect(subject.perform).to be_nil end end + + context 'when hook is not enabled' do + let(:event) { { 'name' => 'label_suggestion', 'data' => { 'conversation_display_id' => conversation.display_id } } } + + before do + create(:message, account: account, conversation: conversation, message_type: :incoming, content: 'hello agent') + create(:message, account: account, conversation: conversation, message_type: :outgoing, content: 'hello customer') + create(:message, account: account, conversation: conversation, message_type: :incoming, content: 'hello agent 2') + create(:message, account: account, conversation: conversation, message_type: :incoming, content: 'hello agent 3') + create(:message, account: account, conversation: conversation, message_type: :incoming, content: 'hello agent 4') + + create(:label, account: account) + create(:label, account: account) + + hook.settings['label_suggestion'] = nil + end + + it 'returns nil' do + expect(subject.perform).to be_nil + end + end end end From eeb0113dc5430af8b0edb8d9de34fb3138a4fb5c Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:38:33 +0530 Subject: [PATCH 02/35] feat: Inbox item header component (#8810) --- .../inbox/components/InboxItemHeader.vue | 53 ++++++++++++++ .../inbox/components/PaginationButton.vue | 73 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 app/javascript/dashboard/routes/dashboard/inbox/components/InboxItemHeader.vue create mode 100644 app/javascript/dashboard/routes/dashboard/inbox/components/PaginationButton.vue diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxItemHeader.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxItemHeader.vue new file mode 100644 index 000000000..7fe60702e --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxItemHeader.vue @@ -0,0 +1,53 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/PaginationButton.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/PaginationButton.vue new file mode 100644 index 000000000..d347ca3fa --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/PaginationButton.vue @@ -0,0 +1,73 @@ + + + From 0805f362d3e2272f5e45f2b77a1904fc518b17a1 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 30 Jan 2024 11:26:07 +0400 Subject: [PATCH 03/35] chore: Validation for purchased pricing_plan_quantity (#8809) - Add validations based on purchased pricing_plan_quantity - Modify the Dashboard to support the new error message --- .../dashboard/settings/agents/AddAgent.vue | 16 ++++++-- app/models/user.rb | 1 + .../app/models/enterprise/concerns/user.rb | 13 +++++++ spec/enterprise/models/user_spec.rb | 38 +++++++++++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 enterprise/app/models/enterprise/concerns/user.rb diff --git a/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue b/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue index 17cc94be0..f2037b080 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue @@ -130,15 +130,23 @@ export default { this.showAlert(this.$t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE')); this.onClose(); } catch (error) { - const { response: { data: { error: errorResponse = '' } = {} } = {} } = - error; + const { + response: { + data: { + error: errorResponse = '', + attributes: attributes = [], + message: attrError = '', + } = {}, + } = {}, + } = error; + let errorMessage = ''; - if (error.response.status === 422) { + if (error.response.status === 422 && !attributes.includes('base')) { errorMessage = this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE'); } else { errorMessage = this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE'); } - this.showAlert(errorResponse || errorMessage); + this.showAlert(errorResponse || attrError || errorMessage); } }, }, diff --git a/app/models/user.rb b/app/models/user.rb index 25ae2826a..2870983a4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -161,3 +161,4 @@ class User < ApplicationRecord end User.include_mod_with('Audit::User') +User.include_mod_with('Concerns::User') diff --git a/enterprise/app/models/enterprise/concerns/user.rb b/enterprise/app/models/enterprise/concerns/user.rb new file mode 100644 index 000000000..ccb677761 --- /dev/null +++ b/enterprise/app/models/enterprise/concerns/user.rb @@ -0,0 +1,13 @@ +module Enterprise::Concerns::User + extend ActiveSupport::Concern + + included do + before_validation :ensure_installation_pricing_plan_quantity + end + + def ensure_installation_pricing_plan_quantity + return unless ChatwootHub.pricing_plan == 'premium' + + errors.add(:base, 'User limit reached. Please purchase more licenses from super admin') if User.count >= ChatwootHub.pricing_plan_quantity + end +end diff --git a/spec/enterprise/models/user_spec.rb b/spec/enterprise/models/user_spec.rb index ba1890a78..f09ddefc1 100644 --- a/spec/enterprise/models/user_spec.rb +++ b/spec/enterprise/models/user_spec.rb @@ -5,6 +5,44 @@ require 'rails_helper' RSpec.describe User do let(:user) { create(:user) } + describe 'before validation for pricing plans' do + let(:new_user) { build(:user) } + + context 'when pricing plan is not premium' do + before do + allow(ChatwootHub).to receive(:pricing_plan).and_return('community') + allow(ChatwootHub).to receive(:pricing_plan_quantity).and_return(0) + end + + it 'does not add an error to the user' do + new_user.valid? + expect(new_user.errors[:base]).to be_empty + end + end + + context 'when pricing plan is premium' do + before do + allow(ChatwootHub).to receive(:pricing_plan).and_return('premium') + end + + context 'when the user limit is reached' do + it 'adds an error to the user' do + allow(ChatwootHub).to receive(:pricing_plan_quantity).and_return(1) + user.valid? + expect(user.errors[:base]).to include('User limit reached. Please purchase more licenses from super admin') + end + end + + context 'when the user limit is not reached' do + it 'does not add an error to the user' do + allow(ChatwootHub).to receive(:pricing_plan_quantity).and_return(2) + user.valid? + expect(user.errors[:base]).to be_empty + end + end + end + end + describe 'audit log' do context 'when user is created' do it 'has no associated audit log created' do From f2115b15f7bb6cf4b04eaf57eeccf29dc252c8dd Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 30 Jan 2024 13:55:20 +0530 Subject: [PATCH 04/35] feat: Add inbox view page (#8814) * feat: Add inbox view page * Update accounts.js * Update index.js --- app/javascript/dashboard/featureFlags.js | 1 + .../i18n/locale/en/generalSettings.json | 3 ++ .../routes/dashboard/dashboard.routes.js | 2 ++ .../dashboard/inbox/components/InboxView.vue | 5 +++ .../routes/dashboard/inbox/routes.js | 25 +++++++++++++++ app/javascript/dashboard/routes/index.js | 11 +++++++ .../dashboard/store/modules/accounts.js | 12 +++++-- .../modules/specs/account/getters.spec.js | 32 +++++++++++++++++++ config/features.yml | 2 ++ 9 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/inbox/components/InboxView.vue create mode 100644 app/javascript/dashboard/routes/dashboard/inbox/routes.js diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 4280e0f10..9d06f15c6 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -17,4 +17,5 @@ export const FEATURE_FLAGS = { VOICE_RECORDER: 'voice_recorder', AUDIT_LOGS: 'audit_logs', INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply', + INBOX_VIEW: 'inbox_view', }; diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json index d56a20c15..bb6363c23 100644 --- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json @@ -90,6 +90,9 @@ "conversation_mention": "Mention" } }, + "INBOX_PAGE": { + "HEADER": "Inbox" + }, "NETWORK": { "NOTIFICATION": { "OFFLINE": "Offline" diff --git a/app/javascript/dashboard/routes/dashboard/dashboard.routes.js b/app/javascript/dashboard/routes/dashboard/dashboard.routes.js index 712e05928..7bffc0f6c 100644 --- a/app/javascript/dashboard/routes/dashboard/dashboard.routes.js +++ b/app/javascript/dashboard/routes/dashboard/dashboard.routes.js @@ -5,6 +5,7 @@ import { routes as contactRoutes } from './contacts/routes'; import { routes as notificationRoutes } from './notifications/routes'; import { frontendURL } from '../../helper/URLHelper'; import helpcenterRoutes from './helpcenter/helpcenter.routes'; +import { routes as inboxRoutes } from './inbox/routes'; const AppContainer = () => import('./Dashboard.vue'); const Suspended = () => import('./suspended/Index.vue'); @@ -21,6 +22,7 @@ export default { ...contactRoutes, ...searchRoutes, ...notificationRoutes, + ...inboxRoutes, ], }, { diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxView.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxView.vue new file mode 100644 index 000000000..b64f56919 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxView.vue @@ -0,0 +1,5 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/inbox/routes.js b/app/javascript/dashboard/routes/dashboard/inbox/routes.js new file mode 100644 index 000000000..9ffa258f7 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/inbox/routes.js @@ -0,0 +1,25 @@ +/* eslint arrow-body-style: 0 */ +import { frontendURL } from '../../../helper/URLHelper'; +const SettingsWrapper = () => import('../settings/Wrapper.vue'); +const InboxView = () => import('./components/InboxView.vue'); + +export const routes = [ + { + path: frontendURL('accounts/:accountId/inbox'), + component: SettingsWrapper, + props: { + headerTitle: 'INBOX_PAGE.HEADER', + icon: 'alert', + showNewButton: false, + showSidemenuIcon: false, + }, + children: [ + { + path: '', + name: 'inbox_index', + component: InboxView, + roles: ['administrator', 'agent'], + }, + ], + }, +]; diff --git a/app/javascript/dashboard/routes/index.js b/app/javascript/dashboard/routes/index.js index e2245e20f..b7e16caf3 100644 --- a/app/javascript/dashboard/routes/index.js +++ b/app/javascript/dashboard/routes/index.js @@ -5,6 +5,7 @@ import dashboard from './dashboard/dashboard.routes'; import store from '../store'; import { validateLoggedInRoutes } from '../helper/routeHelpers'; import AnalyticsHelper from '../helper/AnalyticsHelper'; +import { FEATURE_FLAGS } from 'dashboard/featureFlags'; const routes = [...dashboard.routes]; @@ -41,6 +42,16 @@ export const validateAuthenticateRoutePermission = (to, next, { getters }) => { return '/app/login'; } + // Open inbox view if inbox view feature is enabled, else redirect to dashboard + // TODO: Remove this code once inbox view feature is enabled for all accounts + const isInboxViewEnabled = store.getters['accounts/isFeatureEnabledGlobally']( + user.account_id, + FEATURE_FLAGS.INBOX_VIEW + ); + if (to.name === 'inbox_index' && !isInboxViewEnabled) { + return next(frontendURL(`accounts/${user.account_id}/dashboard`)); + } + if (!to.name) { return next(frontendURL(`accounts/${user.account_id}/dashboard`)); } diff --git a/app/javascript/dashboard/store/modules/accounts.js b/app/javascript/dashboard/store/modules/accounts.js index 4731ec2a7..c11312914 100644 --- a/app/javascript/dashboard/store/modules/accounts.js +++ b/app/javascript/dashboard/store/modules/accounts.js @@ -4,6 +4,9 @@ import AccountAPI from '../../api/account'; import EnterpriseAccountAPI from '../../api/enterprise/account'; import { throwErrorMessage } from '../utils/api'; +const findRecordById = ($state, id) => + $state.records.find(record => record.id === Number(id)) || {}; + const state = { records: [], uiFlags: { @@ -30,10 +33,15 @@ export const getters = { return true; } - const { features = {} } = - $state.records.find(record => record.id === Number(id)) || {}; + const { features = {} } = findRecordById($state, id); + return features[featureName] || false; }, + // There are some features which can be enabled/disabled globally + isFeatureEnabledGlobally: $state => (id, featureName) => { + const { features = {} } = findRecordById($state, id); + return features[featureName] || false; + }, }; export const actions = { diff --git a/app/javascript/dashboard/store/modules/specs/account/getters.spec.js b/app/javascript/dashboard/store/modules/specs/account/getters.spec.js index 39706cbbd..c8613b706 100644 --- a/app/javascript/dashboard/store/modules/specs/account/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/account/getters.spec.js @@ -4,6 +4,10 @@ const accountData = { id: 1, name: 'Company one', locale: 'en', + features: { + auto_resolve_conversations: false, + agent_management: false, + }, }; describe('#getters', () => { @@ -29,4 +33,32 @@ describe('#getters', () => { isDeleting: false, }); }); + + it('isFeatureEnabledonAccount', () => { + const state = { + records: [accountData], + }; + const rootGetters = { + getCurrentUser: { + type: 'SuperAdmin', + }, + }; + expect( + getters.isFeatureEnabledonAccount( + state, + null, + null, + rootGetters + )(1, 'auto_resolve_conversations') + ).toEqual(true); + }); + + it('isFeatureEnabledGlobally', () => { + const state = { + records: [accountData], + }; + expect( + getters.isFeatureEnabledGlobally(state)(1, 'auto_resolve_conversations') + ).toEqual(false); + }); }); diff --git a/config/features.yml b/config/features.yml index 1bb355fc8..7eabf28d8 100644 --- a/config/features.yml +++ b/config/features.yml @@ -61,3 +61,5 @@ enabled: false - name: insert_article_in_reply enabled: false +- name: inbox_view + enabled: false From 17cb788193d151fcd446c90c8c672c74f0af452a Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 31 Jan 2024 00:41:38 +0530 Subject: [PATCH 05/35] fix: Upgrade gmail_xoauth gem to 0.4.3 (#8817) --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 96881eb30..6d2845410 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -277,7 +277,7 @@ GEM gli (2.21.1) globalid (1.2.1) activesupport (>= 6.1) - gmail_xoauth (0.4.2) + gmail_xoauth (0.4.3) oauth (>= 0.3.6) google-apis-core (0.11.0) addressable (~> 2.5, >= 2.5.1) @@ -794,7 +794,7 @@ GEM valid_email2 (4.0.6) activemodel (>= 3.2) mail (~> 2.5) - version_gem (1.1.2) + version_gem (1.1.3) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) From 53d42b15b8d47278413f169ab50130e5f70958b0 Mon Sep 17 00:00:00 2001 From: lkfdjaskl <46911188+gsemyong@users.noreply.github.com> Date: Wed, 31 Jan 2024 01:09:18 +0100 Subject: [PATCH 06/35] fix: Avoid oneOf usage in Open API 2.0 spec (#8819) Small fixes to make spec adhere to swagger 2.0 --- .../request/public/message/update_payload.yml | 18 +------- swagger/parameters/index.yml | 3 ++ swagger/paths/application/contacts/filter.yml | 2 +- swagger/swagger.json | 45 +++++++------------ 4 files changed, 21 insertions(+), 47 deletions(-) diff --git a/swagger/definitions/request/public/message/update_payload.yml b/swagger/definitions/request/public/message/update_payload.yml index b581d7171..10926faa1 100644 --- a/swagger/definitions/request/public/message/update_payload.yml +++ b/swagger/definitions/request/public/message/update_payload.yml @@ -2,19 +2,5 @@ type: object properties: submitted_values: - oneOf: - - type: object - description: Replies to the Bot Message Types - properties: - csat_survey_response: - type: object - description: CSAT Survey Response if original message is CSAT Survey - properties: - rating: - type: integer - description: CSAT Rating - example: 5 - feedback_message: - type: string - description: CSAT Comment - example: 'Great' + type: object + description: Replies to the Bot Message Types \ No newline at end of file diff --git a/swagger/parameters/index.yml b/swagger/parameters/index.yml index 5dcc1f418..d9cb6872c 100644 --- a/swagger/parameters/index.yml +++ b/swagger/parameters/index.yml @@ -51,3 +51,6 @@ public_inbox_identifier: public_contact_identifier: $ref: ./public/contact_identifier.yml + +portal_id: + $ref: ./portal_id.yml \ No newline at end of file diff --git a/swagger/paths/application/contacts/filter.yml b/swagger/paths/application/contacts/filter.yml index 639784ea5..2a1d53cf7 100644 --- a/swagger/paths/application/contacts/filter.yml +++ b/swagger/paths/application/contacts/filter.yml @@ -36,7 +36,7 @@ parameters: query_operator: type: string description: query operator name - enum: [ AND, OR ] + enum: [ AND, OR ] example: - attribute_key: 'name' filter_operator: 'equal_to' diff --git a/swagger/swagger.json b/swagger/swagger.json index e42a43efe..9bee2c952 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -2154,12 +2154,12 @@ }, "query_operator": { "type": "string", - "description": "query operator name" - }, - "enum": [ - "AND", - "OR" - ] + "description": "query operator name", + "enum": [ + "AND", + "OR" + ] + } } }, "example": [ @@ -6746,30 +6746,8 @@ "type": "object", "properties": { "submitted_values": { - "oneOf": [ - { - "type": "object", - "description": "Replies to the Bot Message Types", - "properties": { - "csat_survey_response": { - "type": "object", - "description": "CSAT Survey Response if original message is CSAT Survey", - "properties": { - "rating": { - "type": "integer", - "description": "CSAT Rating", - "example": 5 - }, - "feedback_message": { - "type": "string", - "description": "CSAT Comment", - "example": "Great" - } - } - } - } - } - ] + "type": "object", + "description": "Replies to the Bot Message Types" } } }, @@ -7280,6 +7258,13 @@ "type": "string", "required": true, "description": "The source id of contact obtained on contact create" + }, + "portal_id": { + "in": "path", + "name": "portal_id", + "type": "integer", + "required": true, + "description": "The numeric ID of the portal" } }, "x-tagGroups": [ From 905ca94f713945f16dd8467b9eb2ce34f8e850c1 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 30 Jan 2024 23:48:46 -0800 Subject: [PATCH 07/35] chore: Remove unused attribute imap_inbox_synced_at (#8822) - imap_inbox_synced_at is no longer used; this PR removes all the references to the same. --- .../routes/dashboard/settings/inbox/ImapSettings.vue | 3 --- app/models/channel/email.rb | 3 +-- ...31040316_remove_imap_inbox_syned_at_from_channel_email.rb | 5 +++++ db/schema.rb | 3 +-- spec/jobs/inboxes/fetch_imap_emails_job_spec.rb | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 db/migrate/20240131040316_remove_imap_inbox_syned_at_from_channel_email.rb diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue index 6ceb5faef..f6a7a5d8d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ImapSettings.vue @@ -143,9 +143,6 @@ export default { imap_login: this.login, imap_password: this.password, imap_enable_ssl: this.isSSLEnabled, - imap_inbox_synced_at: this.isIMAPEnabled - ? new Date().toISOString() - : undefined, }, }; diff --git a/app/models/channel/email.rb b/app/models/channel/email.rb index 0d519ec2d..9518599f2 100644 --- a/app/models/channel/email.rb +++ b/app/models/channel/email.rb @@ -8,7 +8,6 @@ # imap_address :string default("") # imap_enable_ssl :boolean default(TRUE) # imap_enabled :boolean default(FALSE) -# imap_inbox_synced_at :datetime # imap_login :string default("") # imap_password :string default("") # imap_port :integer default(0) @@ -41,7 +40,7 @@ class Channel::Email < ApplicationRecord AUTHORIZATION_ERROR_THRESHOLD = 10 self.table_name = 'channel_email' - EDITABLE_ATTRS = [:email, :imap_enabled, :imap_login, :imap_password, :imap_address, :imap_port, :imap_enable_ssl, :imap_inbox_synced_at, + EDITABLE_ATTRS = [:email, :imap_enabled, :imap_login, :imap_password, :imap_address, :imap_port, :imap_enable_ssl, :smtp_enabled, :smtp_login, :smtp_password, :smtp_address, :smtp_port, :smtp_domain, :smtp_enable_starttls_auto, :smtp_enable_ssl_tls, :smtp_openssl_verify_mode, :smtp_authentication, :provider].freeze diff --git a/db/migrate/20240131040316_remove_imap_inbox_syned_at_from_channel_email.rb b/db/migrate/20240131040316_remove_imap_inbox_syned_at_from_channel_email.rb new file mode 100644 index 000000000..20724f29c --- /dev/null +++ b/db/migrate/20240131040316_remove_imap_inbox_syned_at_from_channel_email.rb @@ -0,0 +1,5 @@ +class RemoveImapInboxSynedAtFromChannelEmail < ActiveRecord::Migration[7.0] + def change + remove_column :channel_email, :imap_inbox_synced_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index a5cff5fb3..3439a33da 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[7.0].define(version: 2024_01_29_080827) do +ActiveRecord::Schema[7.0].define(version: 2024_01_31_040316) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -276,7 +276,6 @@ ActiveRecord::Schema[7.0].define(version: 2024_01_29_080827) do t.string "imap_login", default: "" t.string "imap_password", default: "" t.boolean "imap_enable_ssl", default: true - t.datetime "imap_inbox_synced_at", precision: nil t.boolean "smtp_enabled", default: false t.string "smtp_address", default: "" t.integer "smtp_port", default: 0 diff --git a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb index 30a77c5ec..ce99a0065 100644 --- a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb +++ b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Inboxes::FetchImapEmailsJob do let(:account) { create(:account) } let(:imap_email_channel) do create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_login: 'imap@gmail.com', - imap_password: 'password', imap_inbox_synced_at: Time.now.utc, account: account) + imap_password: 'password', account: account) end let(:microsoft_imap_email_channel) do create(:channel_email, provider: 'microsoft', imap_enabled: true, imap_address: 'outlook.office365.com', From ee3f734b7bcde3e16385e848f13945c5a9d1fa7e Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Wed, 31 Jan 2024 00:24:12 -0800 Subject: [PATCH 08/35] chore: Refactor RefreshOauthTokenService to improve readability (#8820) - Added a trait called microsoft_email for the Channel::Email factory. - Rewrote the logic to make it simple to understand - Rewrote the specs for readability --- .../microsoft/refresh_oauth_token_service.rb | 69 +++++++----- spec/factories/channel/channel_email.rb | 17 +++ .../inboxes/fetch_imap_emails_job_spec.rb | 7 +- .../refresh_oauth_token_service_spec.rb | 106 ++++++++++++------ 4 files changed, 130 insertions(+), 69 deletions(-) diff --git a/app/services/microsoft/refresh_oauth_token_service.rb b/app/services/microsoft/refresh_oauth_token_service.rb index d9ad6aa50..a038dd68b 100644 --- a/app/services/microsoft/refresh_oauth_token_service.rb +++ b/app/services/microsoft/refresh_oauth_token_service.rb @@ -1,48 +1,39 @@ -# refer: https://gitlab.com/gitlab-org/ruby/gems/gitlab-mail_room/-/blob/master/lib/mail_room/microsoft_graph/connection.rb -# refer: https://github.com/microsoftgraph/msgraph-sample-rubyrailsapp/tree/b4a6869fe4a438cde42b161196484a929f1bee46 -# https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-configurable-token-lifetimes +# Refer: https://learn.microsoft.com/en-us/entra/identity-platform/configurable-token-lifetimes class Microsoft::RefreshOauthTokenService pattr_initialize [:channel!] - # if the token is not expired yet then skip the refresh token step + # Additional references: https://gitlab.com/gitlab-org/ruby/gems/gitlab-mail_room/-/blob/master/lib/mail_room/microsoft_graph/connection.rb def access_token - provider_config = channel.provider_config.with_indifferent_access - if Time.current.utc >= expires_on(provider_config['expires_on']) - # Token expired, refresh - new_hash = refresh_tokens - new_hash[:access_token] - else - provider_config[:access_token] - end + return provider_config[:access_token] unless access_token_expired? + + refreshed_tokens = refresh_tokens + refreshed_tokens[:access_token] end - def expires_on(expiry) - # we will give it a 5 minute gap for safety - expiry.presence ? DateTime.parse(expiry) - 5.minutes : Time.current.utc + def access_token_expired? + expiry = provider_config[:expires_on] + + return true if expiry.blank? + + # Adding a 5 minute window to expiry check to avoid any race + # conditions during the fetch operation. This would assure that the + # tokens are updated when we fetch the emails. + Time.current.utc >= DateTime.parse(expiry) - 5.minutes end - # + # Refresh the access tokens using the refresh token + # Refer: https://github.com/microsoftgraph/msgraph-sample-rubyrailsapp/tree/b4a6869fe4a438cde42b161196484a929f1bee46 def refresh_tokens - token_hash = channel.provider_config.with_indifferent_access - oauth_strategy = ::MicrosoftGraphAuth.new( - nil, ENV.fetch('AZURE_APP_ID', nil), ENV.fetch('AZURE_APP_SECRET', nil) - ) + oauth_strategy = build_oauth_strategy + token_service = build_token_service(oauth_strategy) - token_service = OAuth2::AccessToken.new( - oauth_strategy.client, token_hash['access_token'], - refresh_token: token_hash['refresh_token'] - ) - - # Refresh the tokens new_tokens = token_service.refresh!.to_hash.slice(:access_token, :refresh_token, :expires_at) update_channel_provider_config(new_tokens) - channel.provider_config + channel.reload.provider_config end - # def update_channel_provider_config(new_tokens) - new_tokens = new_tokens.with_indifferent_access channel.provider_config = { access_token: new_tokens[:access_token], refresh_token: new_tokens[:refresh_token], @@ -50,4 +41,24 @@ class Microsoft::RefreshOauthTokenService } channel.save! end + + private + + def provider_config + @provider_config ||= channel.provider_config.with_indifferent_access + end + + # Builds the OAuth strategy for Microsoft Graph + def build_oauth_strategy + ::MicrosoftGraphAuth.new(nil, ENV.fetch('AZURE_APP_ID'), ENV.fetch('AZURE_APP_SECRET')) + end + + # Builds the token service using OAuth2 + def build_token_service(oauth_strategy) + OAuth2::AccessToken.new( + oauth_strategy.client, + provider_config[:access_token], + refresh_token: provider_config[:refresh_token] + ) + end end diff --git a/spec/factories/channel/channel_email.rb b/spec/factories/channel/channel_email.rb index 6e01dffdd..3ec96aaa4 100644 --- a/spec/factories/channel/channel_email.rb +++ b/spec/factories/channel/channel_email.rb @@ -8,5 +8,22 @@ FactoryBot.define do after(:create) do |channel_email| create(:inbox, channel: channel_email, account: channel_email.account) end + + trait :microsoft_email do + imap_enabled { true } + imap_address { 'outlook.office365.com' } + imap_port { 993 } + imap_login { 'email@example.com' } + imap_password { '' } + imap_enable_ssl { true } + provider_config do + { + expires_on: Time.zone.now + 3600, + access_token: SecureRandom.hex, + refresh_token: SecureRandom.hex + } + end + provider { 'microsoft' } + end end end diff --git a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb index ce99a0065..48c2dde86 100644 --- a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb +++ b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb @@ -8,11 +8,8 @@ RSpec.describe Inboxes::FetchImapEmailsJob do create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_login: 'imap@gmail.com', imap_password: 'password', account: account) end - let(:microsoft_imap_email_channel) do - create(:channel_email, provider: 'microsoft', imap_enabled: true, imap_address: 'outlook.office365.com', - imap_port: 993, imap_login: 'imap@outlook.com', imap_password: 'password', account: account, - provider_config: { access_token: 'access_token' }) - end + + let(:microsoft_imap_email_channel) { create(:channel_email, :microsoft_email) } let(:ms_email_inbox) { create(:inbox, channel: microsoft_imap_email_channel, account: account) } let!(:conversation) { create(:conversation, inbox: imap_email_channel.inbox, account: account) } let(:inbound_mail) { create_inbound_email_from_mail(from: 'testemail@gmail.com', to: 'imap@outlook.com', subject: 'Hello!') } diff --git a/spec/services/microsoft/refresh_oauth_token_service_spec.rb b/spec/services/microsoft/refresh_oauth_token_service_spec.rb index 5218d388b..346db70d8 100644 --- a/spec/services/microsoft/refresh_oauth_token_service_spec.rb +++ b/spec/services/microsoft/refresh_oauth_token_service_spec.rb @@ -1,51 +1,87 @@ require 'rails_helper' RSpec.describe Microsoft::RefreshOauthTokenService do - let(:access_token) { SecureRandom.hex } - let(:refresh_token) { SecureRandom.hex } - let(:expires_on) { Time.zone.now + 3600 } - - let!(:microsoft_email_channel) do - create(:channel_email, provider_config: { access_token: access_token, refresh_token: refresh_token, expires_on: expires_on }) + let!(:microsoft_channel) { create(:channel_email, :microsoft_email) } + let!(:microsoft_channel_with_expired_token) do + create( + :channel_email, :microsoft_email, provider_config: { + expires_on: Time.zone.now - 3600, + access_token: SecureRandom.hex, + refresh_token: SecureRandom.hex + } + ) end - let(:new_tokens) { { access_token: access_token, refresh_token: refresh_token, expires_at: expires_on.to_i, token_type: 'bearer' } } - describe '#access_token' do - context 'when token is not expired' do - it 'returns the existing access token' do - expect(described_class.new(channel: microsoft_email_channel).access_token).to eq(access_token) - expect(microsoft_email_channel.reload.provider_config['refresh_token']).to eq(refresh_token) - end + let(:new_tokens) do + { + access_token: SecureRandom.hex, + refresh_token: SecureRandom.hex, + expires_at: (Time.zone.now + 3600).to_i, + token_type: 'bearer' + } + end + + context 'when token is not expired' do + it 'returns the existing access token' do + service = described_class.new(channel: microsoft_channel) + + expect(service.access_token).to eq(microsoft_channel.provider_config['access_token']) + expect(microsoft_channel.reload.provider_config['refresh_token']).to eq(microsoft_channel.provider_config['refresh_token']) + end + end + + describe 'on expired token or invalid expiry' do + before do + stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token').with( + body: { 'grant_type' => 'refresh_token', 'refresh_token' => microsoft_channel_with_expired_token.provider_config['refresh_token'] } + ).to_return(status: 200, body: new_tokens.to_json, headers: { 'Content-Type' => 'application/json' }) end - context 'when token is expired' do - let(:expires_on) { 1.minute.from_now } - - before do - stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token').with( - body: { 'grant_type' => 'refresh_token', 'refresh_token' => refresh_token } - ).to_return(status: 200, body: new_tokens.to_json, headers: { 'Content-Type' => 'application/json' }) - end - + context 'when token is invalid' do it 'fetches new access token and refresh tokens' do - microsoft_email_channel.provider_config['expires_on'] = Time.zone.now - 3600 - microsoft_email_channel.save! + with_modified_env AZURE_APP_ID: SecureRandom.uuid, AZURE_APP_SECRET: SecureRandom.hex do + provider_config = microsoft_channel_with_expired_token.provider_config + service = described_class.new(channel: microsoft_channel_with_expired_token) + expect(service.access_token).not_to eq(provider_config['access_token']) - expect(described_class.new(channel: microsoft_email_channel).access_token).not_to eq(access_token) - expect(microsoft_email_channel.reload.provider_config['access_token']).to eq(new_tokens[:access_token]) - expect(microsoft_email_channel.reload.provider_config['refresh_token']).to eq(new_tokens[:refresh_token]) - expect(microsoft_email_channel.reload.provider_config['expires_on']).to eq(Time.at(new_tokens[:expires_at]).utc.to_s) + new_provider_config = microsoft_channel_with_expired_token.reload.provider_config + expect(new_provider_config['access_token']).to eq(new_tokens[:access_token]) + expect(new_provider_config['refresh_token']).to eq(new_tokens[:refresh_token]) + expect(new_provider_config['expires_on']).to eq(Time.at(new_tokens[:expires_at]).utc.to_s) + end end end - context 'when refresh token is not present in provider config and access token is expired' do - it 'throws an error' do - microsoft_email_channel.update(provider_config: { - access_token: access_token, - expires_on: expires_on - 3600 - }) + context 'when expiry time is missing' do + it 'fetches new access token and refresh tokens' do + with_modified_env AZURE_APP_ID: SecureRandom.uuid, AZURE_APP_SECRET: SecureRandom.hex do + microsoft_channel_with_expired_token.provider_config['expires_on'] = nil + microsoft_channel_with_expired_token.save! + provider_config = microsoft_channel_with_expired_token.provider_config + service = described_class.new(channel: microsoft_channel_with_expired_token) + expect(service.access_token).not_to eq(provider_config['access_token']) + + new_provider_config = microsoft_channel_with_expired_token.reload.provider_config + expect(new_provider_config['access_token']).to eq(new_tokens[:access_token]) + expect(new_provider_config['refresh_token']).to eq(new_tokens[:refresh_token]) + expect(new_provider_config['expires_on']).to eq(Time.at(new_tokens[:expires_at]).utc.to_s) + end + end + end + end + + context 'when refresh token is not present in provider config and access token is expired' do + it 'throws an error' do + with_modified_env AZURE_APP_ID: SecureRandom.uuid, AZURE_APP_SECRET: SecureRandom.hex do + microsoft_channel.update( + provider_config: { + access_token: SecureRandom.hex, + expires_on: Time.zone.now - 3600 + } + ) + expect do - described_class.new(channel: microsoft_email_channel).access_token + described_class.new(channel: microsoft_channel).access_token end.to raise_error(RuntimeError, 'A refresh_token is not available') end end From 390cd756e829fccbc2e05acae1a0ca76bcd21cd3 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 31 Jan 2024 16:48:42 +0400 Subject: [PATCH 09/35] chore: Reorganize the installation config settings (#8794) - Reorganizing installation config settings to move more configurations into UI from environment variables - Changes to installation config to support premium plans in the enterprise edition - Fixes the broken premium indicator in account/show and accounts/edit page --- .../administrate/custom_styles.scss | 2 +- .../super_admin/app_configs_controller.rb | 15 +- .../super_admin/account_features_helper.rb | 9 ++ app/jobs/internal/check_new_versions_job.rb | 24 ++- .../account_features_field/_form.html.erb | 10 +- .../account_features_field/_show.html.erb | 6 +- .../super_admin/app_configs/show.html.erb | 2 +- app/views/super_admin/settings/show.html.erb | 11 +- config/features.yml | 3 + config/installation_config.yml | 138 ++++++++++++++---- .../app/helpers/super_admin/features.yml | 2 + .../internal/check_new_versions_job.rb | 30 ++++ .../internal/reconcile_plan_config_service.rb | 60 ++++++++ enterprise/config/premium_features.yml | 4 + .../config/premium_installation_config.yml | 22 +++ lib/redis/redis_keys.rb | 1 + .../super_admin/app_config_controller_spec.rb | 6 +- .../internal/check_new_versions_job_spec.rb | 32 ++++ .../reconcile_plan_config_service_spec.rb | 81 ++++++++++ .../internal/check_new_versions_job_spec.rb | 2 +- 20 files changed, 398 insertions(+), 62 deletions(-) create mode 100644 app/helpers/super_admin/account_features_helper.rb create mode 100644 enterprise/app/jobs/enterprise/internal/check_new_versions_job.rb create mode 100644 enterprise/app/services/internal/reconcile_plan_config_service.rb create mode 100644 enterprise/config/premium_features.yml create mode 100644 enterprise/config/premium_installation_config.yml create mode 100644 spec/enterprise/jobs/enterprise/internal/check_new_versions_job_spec.rb create mode 100644 spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb diff --git a/app/assets/stylesheets/administrate/custom_styles.scss b/app/assets/stylesheets/administrate/custom_styles.scss index 859a35689..5e6d803d8 100644 --- a/app/assets/stylesheets/administrate/custom_styles.scss +++ b/app/assets/stylesheets/administrate/custom_styles.scss @@ -9,7 +9,7 @@ padding: 4px 12px; .icon-container { - margin-right: 4px; + margin-right: 2px; } diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 97caad42c..a31d01675 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -22,19 +22,24 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController i.value = value i.save! end - # rubocop:disable Rails/I18nLocaleTexts - redirect_to super_admin_settings_path, notice: 'App Configs updated successfully' - # rubocop:enable Rails/I18nLocaleTexts + redirect_to super_admin_settings_path, notice: "App Configs - #{@config.titleize} updated successfully" end private def set_config - @config = params[:config] + @config = params[:config] || 'general' end def allowed_configs - @allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET] + @allowed_configs = case @config + when 'facebook' + %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] + when 'email' + ['MAILER_INBOUND_EMAIL_DOMAIN'] + else + %w[ENABLE_ACCOUNT_SIGNUP] + end end end diff --git a/app/helpers/super_admin/account_features_helper.rb b/app/helpers/super_admin/account_features_helper.rb new file mode 100644 index 000000000..759aec134 --- /dev/null +++ b/app/helpers/super_admin/account_features_helper.rb @@ -0,0 +1,9 @@ +module SuperAdmin::AccountFeaturesHelper + def self.account_features + YAML.safe_load(Rails.root.join('config/features.yml').read).freeze + end + + def self.account_premium_features + account_features.filter { |feature| feature['premium'] }.pluck('name') + end +end diff --git a/app/jobs/internal/check_new_versions_job.rb b/app/jobs/internal/check_new_versions_job.rb index be51e7530..a141385d0 100644 --- a/app/jobs/internal/check_new_versions_job.rb +++ b/app/jobs/internal/check_new_versions_job.rb @@ -4,21 +4,17 @@ class Internal::CheckNewVersionsJob < ApplicationJob def perform return unless Rails.env.production? - instance_info = ChatwootHub.sync_with_hub - return unless instance_info - - ::Redis::Alfred.set(::Redis::Alfred::LATEST_CHATWOOT_VERSION, instance_info['version']) - update_installation_config(key: 'INSTALLATION_PRICING_PLAN', value: instance_info['plan']) - update_installation_config(key: 'INSTALLATION_PRICING_PLAN_QUANTITY', value: instance_info['plan_quantity']) - update_installation_config(key: 'CHATWOOT_SUPPORT_WEBSITE_TOKEN', value: instance_info['chatwoot_support_website_token']) - update_installation_config(key: 'CHATWOOT_SUPPORT_IDENTIFIER_HASH', value: instance_info['chatwoot_support_identifier_hash']) - update_installation_config(key: 'CHATWOOT_SUPPORT_SCRIPT_URL', value: instance_info['chatwoot_support_script_url']) + @instance_info = ChatwootHub.sync_with_hub + update_version_info end - def update_installation_config(key:, value:) - config = InstallationConfig.find_or_initialize_by(name: key) - config.value = value - config.locked = true - config.save! + private + + def update_version_info + return if @instance_info['version'].blank? + + ::Redis::Alfred.set(::Redis::Alfred::LATEST_CHATWOOT_VERSION, @instance_info['version']) end end + +Internal::CheckNewVersionsJob.prepend_mod_with('Internal::CheckNewVersionsJob') diff --git a/app/views/fields/account_features_field/_form.html.erb b/app/views/fields/account_features_field/_form.html.erb index c0ef0f425..ebaa23803 100644 --- a/app/views/fields/account_features_field/_form.html.erb +++ b/app/views/fields/account_features_field/_form.html.erb @@ -4,11 +4,15 @@
<% field.data.each do |key,val| %>
- <% if ['audit_logs', 'response_bot'].include? key %> - + <% is_premium = SuperAdmin::AccountFeaturesHelper.account_premium_features.include? key %> + <% if is_premium %> + + + <% end %> <%= key %> - <%= check_box "enabled_features", "feature_#{key}", { checked: val }, true, false %> + <% should_disable = is_premium && ChatwootHub.pricing_plan == 'community' %> + <%= check_box "enabled_features", "feature_#{key}", { checked: val, disabled: should_disable }, true, false %>
<% end %>
diff --git a/app/views/fields/account_features_field/_show.html.erb b/app/views/fields/account_features_field/_show.html.erb index d67c0c860..c34665393 100644 --- a/app/views/fields/account_features_field/_show.html.erb +++ b/app/views/fields/account_features_field/_show.html.erb @@ -1,8 +1,10 @@
<% field.data.each do |key,val| %>
- <% if ['audit_logs', 'response_bot'].include? key %> - + <% if SuperAdmin::AccountFeaturesHelper.account_premium_features.include? key %> + + + <% end %> <%= key %> <%= val.present? ? '✅' : '❌' %> diff --git a/app/views/super_admin/app_configs/show.html.erb b/app/views/super_admin/app_configs/show.html.erb index 239723069..83f99f6bd 100644 --- a/app/views/super_admin/app_configs/show.html.erb +++ b/app/views/super_admin/app_configs/show.html.erb @@ -1,5 +1,5 @@ <% content_for(:title) do %> - Configure Settings + Configure Settings - <%= @config.titleize %> <% end %>
+ + <% if Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING) %> + + <% end %> +
@@ -44,7 +53,7 @@ <% if ChatwootHub.pricing_plan != 'community' && User.count > ChatwootHub.pricing_plan_quantity %>
-

You have <%= User.count %> agents. Please add more licenses.

+

You have <%= User.count %> agents. Please add more licenses to add more users.

<% end %> diff --git a/config/features.yml b/config/features.yml index 7eabf28d8..529ad37d9 100644 --- a/config/features.yml +++ b/config/features.yml @@ -11,6 +11,7 @@ enabled: false - name: disable_branding enabled: false + premium: true - name: email_continuity_on_api_channel enabled: false - name: help_center @@ -55,8 +56,10 @@ enabled: false - name: audit_logs enabled: false + premium: true - name: response_bot enabled: false + premium: true - name: message_reply_to enabled: false - name: insert_article_in_reply diff --git a/config/installation_config.yml b/config/installation_config.yml index 0778ab922..2643461e2 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -1,5 +1,20 @@ -# if you don't specify locked attribute, the default value will be true -# which means the particular config will be locked +# This file contains all the installation wide configuration which controls various settings in Chatwoot +# This is internal config and should not be modified by the user directly in database +# Chatwoot might override and modify these values during the upgrade process +# Configs which can be modified by the user are available in the dashboard under appropriate UI +# +# name: the name of the config referenced in the code +# value: the value of the config +# display_title: the title of the config displayed in the dashboard UI +# description: the description of the config displayed in the dashboard UI +# locked: if you don't specify locked attribute in yaml, the default value will be true, +# which means the particular config will be locked and won't be available in `super_admin/installation_configs` +# premium: These values get overwritten unless the user is on a premium plan +# type: The type of the config. Default is text, boolean is also supported + + + +# ------- Branding Related Config ------- # - name: INSTALLATION_NAME value: 'Chatwoot' display_title: 'Installation Name' @@ -41,32 +56,20 @@ display_title: 'Chatwoot Metadata' description: 'Display default Chatwoot metadata like favicons and upgrade warnings' type: boolean -- name: MAILER_INBOUND_EMAIL_DOMAIN - value: - locked: false -- name: MAILER_SUPPORT_EMAIL - value: +# ------- End of Branding Related Config ------- # + + + +# ------- Signup & Account Related Config ------- # +- name: ENABLE_ACCOUNT_SIGNUP + display_title: 'Enable Account Signup' + value: false + description: 'Allow users to signup for new accounts' locked: false + type: boolean - name: CREATE_NEW_ACCOUNT_FROM_DASHBOARD value: false - locked: false -- name: INSTALLATION_EVENTS_WEBHOOK_URL - value: - locked: false -- name: CHATWOOT_INBOX_TOKEN - value: - locked: false -- name: CHATWOOT_INBOX_HMAC_KEY - value: - locked: false -- name: API_CHANNEL_NAME - value: -- name: API_CHANNEL_THUMBNAIL - value: -- name: ANALYTICS_TOKEN - value: -- name: DIRECT_UPLOADS_ENABLED - value: false + description: 'Allow users to create new accounts from the dashboard' locked: false - name: HCAPTCHA_SITE_KEY value: @@ -74,34 +77,107 @@ - name: HCAPTCHA_SERVER_KEY value: locked: false -- name: LOGOUT_REDIRECT_LINK - value: /app/login +- name: INSTALLATION_EVENTS_WEBHOOK_URL + value: + display_title: 'System events Webhook URL' + description: 'The URL to which the system events like new accounts created will be sent' locked: false -- name: DISABLE_USER_PROFILE_UPDATE +- name: DIRECT_UPLOADS_ENABLED + type: boolean value: false + description: 'Enable direct uploads to cloud storage' + locked: false +# ------- End of Account Related Config ------- # + + + +# ------- Email Related Config ------- # +- name: MAILER_INBOUND_EMAIL_DOMAIN + value: + description: 'The domain name to be used for generating conversation continuity emails (reply+id@domain.com)' + locked: false +- name: MAILER_SUPPORT_EMAIL + value: + locked: false +# ------- End of Email Related Config ------- # + + +# ------- Facebook Channel Related Config ------- # +- name: FB_APP_ID + display_title: 'Facebook App ID' + locked: false +- name: FB_VERIFY_TOKEN + display_title: 'Facebook Verify Token' + description: 'The verify token used for Facebook Messenger Webhook' + locked: false +- name: FB_APP_SECRET + display_title: 'Facebook App Secret' + locked: false +- name: IG_VERIFY_TOKEN + display_title: 'Instagram Verify Token' + description: 'The verify token used for Instagram Webhook' locked: false - name: ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT + display_title: 'Enable human agent' value: false locked: false -- name: CSML_BOT_HOST + description: 'Enable human agent for messenger channel for longer message back period. Needs additional app approval: https://developers.facebook.com/docs/features-reference/human-agent/' + type: boolean +# ------- End of Facebook Channel Related Config ------- # + +# ------- Chatwoot Internal Config for Cloud ----# +- name: CHATWOOT_INBOX_TOKEN value: + description: 'The Chatwoot Inbox Token for Contact Support in Cloud' locked: false -- name: CSML_BOT_API_KEY +- name: CHATWOOT_INBOX_HMAC_KEY value: + description: 'The Chatwoot Inbox HMAC Key for Contact Support in Cloud' locked: false - name: CHATWOOT_CLOUD_PLANS value: + description: 'Config to store stripe plans for cloud' - name: DEPLOYMENT_ENV value: self-hosted -- name: CSML_EDITOR_HOST + description: 'The deployment environment of the installation, to differentiate between Chatwoot cloud and self-hosted' +- name: ANALYTICS_TOKEN value: + description: 'The June.so analytics token for Chatwoot cloud' +# ------- End of Chatwoot Internal Config for Cloud ----# + + +# ------- Chatwoot Internal Config for Self Hosted ----# - name: INSTALLATION_PRICING_PLAN value: 'community' + description: 'The pricing plan for the installation, retrieved from the billing API' - name: INSTALLATION_PRICING_PLAN_QUANTITY value: 0 + description: 'The number of licenses purchased for the installation, retrieved from the billing API' - name: CHATWOOT_SUPPORT_WEBSITE_TOKEN value: + description: 'The Chatwoot website token, used to identify the Chatwoot inbox and display the "Contact Support" option on the billing page' - name: CHATWOOT_SUPPORT_SCRIPT_URL value: + description: 'The Chatwoot script base URL, to display the "Contact Support" option on the billing page' - name: CHATWOOT_SUPPORT_IDENTIFIER_HASH value: + description: 'The Chatwoot identifier hash, to validate the contact in the live chat window.' +# ------- End of Chatwoot Internal Config for Self Hosted ----# + +## ------ Configs added for enterprise clients ------ ## +- name: API_CHANNEL_NAME + value: + description: 'Custom name for the API channel' +- name: API_CHANNEL_THUMBNAIL + value: + description: 'Custom thumbnail for the API channel' +- name: LOGOUT_REDIRECT_LINK + value: /app/login + locked: false + description: 'Redirect to a different link after logout' +- name: DISABLE_USER_PROFILE_UPDATE + value: false + locked: false + description: 'Disable rendering profile update page for users' + +## ------ End of Configs added for enterprise clients ------ ## \ No newline at end of file diff --git a/enterprise/app/helpers/super_admin/features.yml b/enterprise/app/helpers/super_admin/features.yml index 41e5af426..a3413a325 100644 --- a/enterprise/app/helpers/super_admin/features.yml +++ b/enterprise/app/helpers/super_admin/features.yml @@ -1,3 +1,5 @@ +# TODO: Move this values to features.yml itself +# No need to replicate the same values in two places custom_branding: name: 'Custom Branding' description: 'Apply your own branding to this installation.' diff --git a/enterprise/app/jobs/enterprise/internal/check_new_versions_job.rb b/enterprise/app/jobs/enterprise/internal/check_new_versions_job.rb new file mode 100644 index 000000000..b1f5d62ec --- /dev/null +++ b/enterprise/app/jobs/enterprise/internal/check_new_versions_job.rb @@ -0,0 +1,30 @@ +module Enterprise::Internal::CheckNewVersionsJob + def perform + super + update_plan_info + reconcile_premium_config_and_features + end + + private + + def update_plan_info + return if @instance_info.blank? + + update_installation_config(key: 'INSTALLATION_PRICING_PLAN', value: @instance_info['plan']) + update_installation_config(key: 'INSTALLATION_PRICING_PLAN_QUANTITY', value: @instance_info['plan_quantity']) + update_installation_config(key: 'CHATWOOT_SUPPORT_WEBSITE_TOKEN', value: @instance_info['chatwoot_support_website_token']) + update_installation_config(key: 'CHATWOOT_SUPPORT_IDENTIFIER_HASH', value: @instance_info['chatwoot_support_identifier_hash']) + update_installation_config(key: 'CHATWOOT_SUPPORT_SCRIPT_URL', value: @instance_info['chatwoot_support_script_url']) + end + + def update_installation_config(key:, value:) + config = InstallationConfig.find_or_initialize_by(name: key) + config.value = value + config.locked = true + config.save! + end + + def reconcile_premium_config_and_features + Internal::ReconcilePlanConfigService.new.perform + end +end diff --git a/enterprise/app/services/internal/reconcile_plan_config_service.rb b/enterprise/app/services/internal/reconcile_plan_config_service.rb new file mode 100644 index 000000000..04414ea68 --- /dev/null +++ b/enterprise/app/services/internal/reconcile_plan_config_service.rb @@ -0,0 +1,60 @@ +class Internal::ReconcilePlanConfigService + def perform + remove_premium_config_reset_warning + return if ChatwootHub.pricing_plan != 'community' + + create_premium_config_reset_warning if premium_config_reset_required? + + # We will have this enabled in the future + # reconcile_premium_config + reconcile_premium_features + end + + private + + def config_path + @config_path ||= Rails.root.join('enterprise/config') + end + + def premium_config + @premium_config ||= YAML.safe_load(File.read("#{config_path}/premium_installation_config.yml")).freeze + end + + def remove_premium_config_reset_warning + Redis::Alfred.delete(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING) + end + + def create_premium_config_reset_warning + Redis::Alfred.set(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING, true) + end + + def premium_config_reset_required? + premium_config.any? do |config| + config = config.with_indifferent_access + existing_config = InstallationConfig.find_by(name: config[:name]) + existing_config&.value != config[:value] if existing_config.present? + end + end + + def reconcile_premium_config + premium_config.each do |config| + new_config = config.with_indifferent_access + existing_config = InstallationConfig.find_by(name: new_config[:name]) + next if existing_config&.value == new_config[:value] + + existing_config&.update!(value: new_config[:value]) + end + end + + def premium_features + @premium_features ||= YAML.safe_load(File.read("#{config_path}/premium_features.yml")).freeze + end + + def reconcile_premium_features + Account.find_in_batches do |accounts| + accounts.each do |account| + account.disable_features!(*premium_features) + end + end + end +end diff --git a/enterprise/config/premium_features.yml b/enterprise/config/premium_features.yml new file mode 100644 index 000000000..9628e1da4 --- /dev/null +++ b/enterprise/config/premium_features.yml @@ -0,0 +1,4 @@ +# List of the premium features in EE edition +- disable_branding +- audit_logs +- response_bot diff --git a/enterprise/config/premium_installation_config.yml b/enterprise/config/premium_installation_config.yml new file mode 100644 index 000000000..0f102b083 --- /dev/null +++ b/enterprise/config/premium_installation_config.yml @@ -0,0 +1,22 @@ +# ------- Branding Related Config ------- # +- name: INSTALLATION_NAME + value: 'Chatwoot' +- name: LOGO_THUMBNAIL + value: '/brand-assets/logo_thumbnail.svg' +- name: LOGO + value: '/brand-assets/logo.svg' +- name: LOGO_DARK + value: '/brand-assets/logo_dark.svg' +- name: BRAND_URL + value: 'https://www.chatwoot.com' +- name: WIDGET_BRAND_URL + value: 'https://www.chatwoot.com' +- name: BRAND_NAME + value: 'Chatwoot' +- name: TERMS_URL + value: 'https://www.chatwoot.com/terms-of-service' +- name: PRIVACY_URL + value: 'https://www.chatwoot.com/privacy-policy' +- name: DISPLAY_MANIFEST + value: true +# ------- End of Branding Related Config ------- # diff --git a/lib/redis/redis_keys.rb b/lib/redis/redis_keys.rb index 535d3774d..f4a1520b1 100644 --- a/lib/redis/redis_keys.rb +++ b/lib/redis/redis_keys.rb @@ -28,6 +28,7 @@ module Redis::RedisKeys ## Internal Installation related keys CHATWOOT_INSTALLATION_ONBOARDING = 'CHATWOOT_INSTALLATION_ONBOARDING'.freeze + CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING = 'CHATWOOT_CONFIG_RESET_WARNING'.freeze LATEST_CHATWOOT_VERSION = 'LATEST_CHATWOOT_VERSION'.freeze # Check if a message create with same source-id is in progress? MESSAGE_SOURCE_KEY = 'MESSAGE_SOURCE_KEY::%s'.freeze diff --git a/spec/controllers/super_admin/app_config_controller_spec.rb b/spec/controllers/super_admin/app_config_controller_spec.rb index 338c3e611..f517009eb 100644 --- a/spec/controllers/super_admin/app_config_controller_spec.rb +++ b/spec/controllers/super_admin/app_config_controller_spec.rb @@ -16,9 +16,9 @@ RSpec.describe 'Super Admin Application Config API', type: :request do it 'shows the app_config page' do sign_in(super_admin, scope: :super_admin) - get '/super_admin/app_config' + get '/super_admin/app_config?config=facebook' expect(response).to have_http_status(:success) - expect(response.body).to include(config.name) + expect(response.body).to include(config.value) end end end @@ -34,7 +34,7 @@ RSpec.describe 'Super Admin Application Config API', type: :request do context 'when it is an aunthenticated super admin' do it 'shows the app_config page' do sign_in(super_admin, scope: :super_admin) - post '/super_admin/app_config', params: { app_config: { FB_APP_ID: 'FB_APP_ID' } } + post '/super_admin/app_config?config=facebook', params: { app_config: { FB_APP_ID: 'FB_APP_ID' } } expect(response).to have_http_status(:found) expect(response).to redirect_to(super_admin_settings_path) diff --git a/spec/enterprise/jobs/enterprise/internal/check_new_versions_job_spec.rb b/spec/enterprise/jobs/enterprise/internal/check_new_versions_job_spec.rb new file mode 100644 index 000000000..48664c904 --- /dev/null +++ b/spec/enterprise/jobs/enterprise/internal/check_new_versions_job_spec.rb @@ -0,0 +1,32 @@ +require 'rails_helper' + +RSpec.describe Internal::CheckNewVersionsJob do + subject(:job) { described_class.perform_now } + + let(:reconsile_premium_config_service) { instance_double(Internal::ReconcilePlanConfigService) } + + before do + allow(Internal::ReconcilePlanConfigService).to receive(:new).and_return(reconsile_premium_config_service) + allow(reconsile_premium_config_service).to receive(:perform) + allow(Rails.env).to receive(:production?).and_return(true) + end + + it 'updates the plan info' do + data = { 'version' => '1.2.3', 'plan' => 'enterprise', 'plan_quantity' => 1, 'chatwoot_support_website_token' => '123', + 'chatwoot_support_identifier_hash' => '123', 'chatwoot_support_script_url' => '123' } + allow(ChatwootHub).to receive(:sync_with_hub).and_return(data) + job + expect(InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN').value).to eq 'enterprise' + expect(InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN_QUANTITY').value).to eq 1 + expect(InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_WEBSITE_TOKEN').value).to eq '123' + expect(InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_IDENTIFIER_HASH').value).to eq '123' + expect(InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_SCRIPT_URL').value).to eq '123' + end + + it 'calls Internal::ReconcilePlanConfigService' do + data = { 'version' => '1.2.3' } + allow(ChatwootHub).to receive(:sync_with_hub).and_return(data) + job + expect(reconsile_premium_config_service).to have_received(:perform) + end +end diff --git a/spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb b/spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb new file mode 100644 index 000000000..ee8b25557 --- /dev/null +++ b/spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +RSpec.describe Internal::ReconcilePlanConfigService do + describe '#perform' do + let(:service) { described_class.new } + + context 'when pricing plan is community' do + before do + allow(ChatwootHub).to receive(:pricing_plan).and_return('community') + end + + it 'disables the premium features for accounts' do + account = create(:account) + account.enable_features!('disable_branding', 'audit_logs', 'response_bot') + response_bot_account = create(:account) + response_bot_account.enable_features!('response_bot') + disable_branding_account = create(:account) + disable_branding_account.enable_features!('disable_branding') + service.perform + expect(account.reload.enabled_features.keys).not_to include('response_bot', 'disable_branding', 'audit_logs') + expect(response_bot_account.reload.enabled_features.keys).not_to include('response_bot') + expect(disable_branding_account.reload.enabled_features.keys).not_to include('disable_branding') + end + + it 'creates a premium config reset warning if config was modified' do + create(:installation_config, name: 'INSTALLATION_NAME', value: 'custom-name') + service.perform + expect(Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING)).to eq('true') + end + + it 'will not create a premium config reset warning if config is not modified' do + create(:installation_config, name: 'INSTALLATION_NAME', value: 'Chatwoot') + service.perform + expect(Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING)).to be_nil + end + + # To be enabled in the future when method is uncommented + + # it 'updates the premium configs to default' do + # create(:installation_config, name: 'INSTALLATION_NAME', value: 'custom-name') + # create(:installation_config, name: 'LOGO', value: '/custom-path/logo.svg') + # service.perform + # expect(InstallationConfig.find_by(name: 'INSTALLATION_NAME').value).to eq('Chatwoot') + # expect(InstallationConfig.find_by(name: 'LOGO').value).to eq('/brand-assets/logo.svg') + # end + end + + context 'when pricing plan is not community' do + before do + allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise') + end + + it 'unset premium config warning on upgrade' do + Redis::Alfred.set(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING, true) + service.perform + expect(Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING)).to be_nil + end + + it 'does not disable the premium features for accounts' do + account = create(:account) + account.enable_features!('disable_branding', 'audit_logs', 'response_bot') + response_bot_account = create(:account) + response_bot_account.enable_features!('response_bot') + disable_branding_account = create(:account) + disable_branding_account.enable_features!('disable_branding') + service.perform + expect(account.reload.enabled_features.keys).to include('response_bot', 'disable_branding', 'audit_logs') + expect(response_bot_account.reload.enabled_features.keys).to include('response_bot') + expect(disable_branding_account.reload.enabled_features.keys).to include('disable_branding') + end + + it 'does not update the LOGO config' do + create(:installation_config, name: 'INSTALLATION_NAME', value: 'custom-name') + create(:installation_config, name: 'LOGO', value: '/custom-path/logo.svg') + service.perform + expect(InstallationConfig.find_by(name: 'INSTALLATION_NAME').value).to eq('custom-name') + expect(InstallationConfig.find_by(name: 'LOGO').value).to eq('/custom-path/logo.svg') + end + end + end +end diff --git a/spec/jobs/internal/check_new_versions_job_spec.rb b/spec/jobs/internal/check_new_versions_job_spec.rb index 3d36c4ba7..82424e202 100644 --- a/spec/jobs/internal/check_new_versions_job_spec.rb +++ b/spec/jobs/internal/check_new_versions_job_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Internal::CheckNewVersionsJob do subject(:job) { described_class.perform_now } it 'updates the latest chatwoot version in redis' do - data = { 'version' => '1.2.3' }.to_json + data = { 'version' => '1.2.3' } allow(Rails.env).to receive(:production?).and_return(true) allow(ChatwootHub).to receive(:sync_with_hub).and_return(data) job From d10525a7142776da7985972f664ec0d2c09e376f Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 31 Jan 2024 18:38:01 +0400 Subject: [PATCH 10/35] fix: Template sync issue for older Whatsapp channels (#8830) - This PR introduces a modification to the channel fetching logic, ensuring that channels with older message_template_last_updated timestamps are prioritized during synchronization. --- .../whatsapp/templates_sync_scheduler_job.rb | 6 ++++-- .../templates_sync_scheduler_job_spec.rb | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/jobs/channels/whatsapp/templates_sync_scheduler_job.rb b/app/jobs/channels/whatsapp/templates_sync_scheduler_job.rb index ade3eb082..6a5e6cb2b 100644 --- a/app/jobs/channels/whatsapp/templates_sync_scheduler_job.rb +++ b/app/jobs/channels/whatsapp/templates_sync_scheduler_job.rb @@ -2,8 +2,10 @@ class Channels::Whatsapp::TemplatesSyncSchedulerJob < ApplicationJob queue_as :low def perform - Channel::Whatsapp.where('message_templates_last_updated <= ? OR message_templates_last_updated IS NULL', - 3.hours.ago).limit(Limits::BULK_EXTERNAL_HTTP_CALLS_LIMIT).all.each do |channel| + Channel::Whatsapp.order(Arel.sql('message_templates_last_updated IS NULL DESC, message_templates_last_updated ASC')) + .where('message_templates_last_updated <= ? OR message_templates_last_updated IS NULL', 3.hours.ago) + .limit(Limits::BULK_EXTERNAL_HTTP_CALLS_LIMIT) + .each do |channel| Channels::Whatsapp::TemplatesSyncJob.perform_later(channel) end end diff --git a/spec/jobs/channels/whatsapp/templates_sync_scheduler_job_spec.rb b/spec/jobs/channels/whatsapp/templates_sync_scheduler_job_spec.rb index e6545bfe3..cf0b30842 100644 --- a/spec/jobs/channels/whatsapp/templates_sync_scheduler_job_spec.rb +++ b/spec/jobs/channels/whatsapp/templates_sync_scheduler_job_spec.rb @@ -23,5 +23,23 @@ RSpec.describe Channels::Whatsapp::TemplatesSyncSchedulerJob do have_been_enqueued.with(non_synced).on_queue('low') ) end + + it 'schedules templates_sync_job for oldest synced channels first' do + stub_const('Limits::BULK_EXTERNAL_HTTP_CALLS_LIMIT', 2) + stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook') + non_synced = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: nil) + synced_recently = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: 4.hours.ago) + synced_old = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: 6.hours.ago) + described_class.perform_now + expect(Channels::Whatsapp::TemplatesSyncJob).not_to( + have_been_enqueued.with(synced_recently).on_queue('low') + ) + expect(Channels::Whatsapp::TemplatesSyncJob).to( + have_been_enqueued.with(synced_old).on_queue('low') + ) + expect(Channels::Whatsapp::TemplatesSyncJob).to( + have_been_enqueued.with(non_synced).on_queue('low') + ) + end end end From b9c62b3fedb700650ed3d9d624bf1895c98f4446 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 1 Feb 2024 10:22:09 +0530 Subject: [PATCH 11/35] feat: Inbox list header (#8831) * feat: Inbox list header * fix: Border --- .../inbox/components/InboxListHeader.vue | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue new file mode 100644 index 000000000..24218744c --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue @@ -0,0 +1,61 @@ + + + + + From b7a7e5a0d393d76cb526aaeb15c019e365fa99eb Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 1 Feb 2024 12:10:58 +0530 Subject: [PATCH 12/35] feat: Inbox list API integration (#8825) * feat: Inbox view * feat: Bind real values * chore: code cleanup * feat: add observer * fix: Inbox icon * chore: more code cleanup * chore: Replace conversation id * chore: Minor fix * chore: Hide from side bar * chore: Fix eslint * chore: Minor fix * fix: dark mode color * chore: Minor fix * feat: Add description for each notification types * chore: remove commented code * Update InboxList.vue * Update InboxView.vue * chore: fix specs * fix: specs * Update InboxView.vue --------- Co-authored-by: iamsivin Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .../dashboard/components/ChatList.vue | 2 +- .../i18n/locale/en/generalSettings.json | 3 - .../dashboard/i18n/locale/en/inbox.json | 17 +++ .../dashboard/i18n/locale/en/index.js | 2 + .../dashboard/i18n/locale/en/settings.json | 1 + .../dashboard/mixins/specs/time.spec.js | 80 ++++++++++++-- app/javascript/dashboard/mixins/time.js | 45 +++++--- .../conversation/conversation.routes.js | 8 ++ .../routes/dashboard/dashboard.routes.js | 2 - .../routes/dashboard/inbox/InboxCard.vue | 70 ------------ .../routes/dashboard/inbox/InboxList.vue | 103 ++++++++++++++++++ .../routes/dashboard/inbox/InboxView.vue | 45 ++++++++ .../dashboard/inbox/components/InboxCard.vue | 102 +++++++++++++++++ .../inbox/components/InboxItemHeader.vue | 39 +++---- .../inbox/components/InboxNameAndId.vue | 44 +++++--- .../dashboard/inbox/components/InboxView.vue | 5 - .../inbox/components/PriorityIcon.vue | 29 +++-- .../dashboard/inbox/components/StatusIcon.vue | 38 ++++--- .../routes/dashboard/inbox/routes.js | 25 ----- app/javascript/dashboard/routes/index.js | 11 -- .../store/modules/notifications/actions.js | 21 ++++ .../store/modules/notifications/index.js | 1 + .../store/modules/notifications/mutations.js | 4 + .../specs/notifications/actions.spec.js | 39 +++++++ .../specs/notifications/mutations.spec.js | 13 ++- .../dashboard/store/mutation-types.js | 1 + .../FluentIcon/dashboard-icons.json | 1 + 27 files changed, 541 insertions(+), 210 deletions(-) create mode 100644 app/javascript/dashboard/i18n/locale/en/inbox.json delete mode 100644 app/javascript/dashboard/routes/dashboard/inbox/InboxCard.vue create mode 100644 app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue create mode 100644 app/javascript/dashboard/routes/dashboard/inbox/InboxView.vue create mode 100644 app/javascript/dashboard/routes/dashboard/inbox/components/InboxCard.vue delete mode 100644 app/javascript/dashboard/routes/dashboard/inbox/components/InboxView.vue delete mode 100644 app/javascript/dashboard/routes/dashboard/inbox/routes.js diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 31fe6110d..7fcc58127 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -1,6 +1,6 @@