diff --git a/.codeclimate.yml b/.codeclimate.yml index 974681a09..d8b8d985b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -54,3 +54,5 @@ exclude_patterns: - 'app/javascript/widget/i18n/index.js' - 'app/javascript/survey/i18n/index.js' - 'app/javascript/shared/constants/locales.js' + - 'app/javascript/dashboard/helper/specs/macrosFixtures.js' + - 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js' diff --git a/.env.example b/.env.example index 12262576e..830ee7bce 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,11 @@ REDIS_SENTINELS= # You can find list of master using "SENTINEL masters" command REDIS_SENTINEL_MASTER_NAME= +# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels +# Use the following environment variable to customize passwords for sentinels. +# Use empty string if sentinels are configured with out passwords +# REDIS_SENTINEL_PASSWORD= + # Redis premium breakage in heroku fix # enable the following configuration # ref: https://github.com/chatwoot/chatwoot/issues/2420 diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index e9bf0802b..69ed786ce 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -35,7 +35,13 @@ class Messages::MessageBuilder file: uploaded_attachment ) - attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile) + attachment.file_type = if uploaded_attachment.is_a?(String) + file_type_by_signed_id( + uploaded_attachment + ) + else + file_type(uploaded_attachment&.content_type) + end end end diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb index d650b19d9..a7891ffd4 100644 --- a/app/controllers/api/v1/accounts/articles_controller.rb +++ b/app/controllers/api/v1/accounts/articles_controller.rb @@ -5,9 +5,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController before_action :set_current_page, only: [:index] def index - @articles_count = @portal.articles.count - @articles = @portal.articles - @articles = @articles.search(list_params) if list_params.present? + @portal_articles = @portal.articles + @all_articles = @portal_articles.search(list_params) + @articles_count = @all_articles.count + @articles = @all_articles.page(@current_page) end def create @@ -37,7 +38,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController end def portal - @portal ||= Current.account.portals.find_by(slug: params[:portal_id]) + @portal ||= Current.account.portals.find_by!(slug: params[:portal_id]) end def article_params diff --git a/app/controllers/api/v1/accounts/categories_controller.rb b/app/controllers/api/v1/accounts/categories_controller.rb index bc18b61b8..e28e601d5 100644 --- a/app/controllers/api/v1/accounts/categories_controller.rb +++ b/app/controllers/api/v1/accounts/categories_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle before_action :set_current_page, only: [:index] def index + @current_locale = params[:locale] @categories = @portal.categories.search(params) end diff --git a/app/controllers/api/v1/accounts/macros_controller.rb b/app/controllers/api/v1/accounts/macros_controller.rb index e7946a54e..3e812ed63 100644 --- a/app/controllers/api/v1/accounts/macros_controller.rb +++ b/app/controllers/api/v1/accounts/macros_controller.rb @@ -14,6 +14,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid? @macro.save! + process_attachments + @macro end def show @@ -25,10 +27,21 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController head :ok end + def attach_file + file_blob = ActiveStorage::Blob.create_and_upload!( + key: nil, + io: params[:attachment].tempfile, + filename: params[:attachment].original_filename, + content_type: params[:attachment].content_type + ) + render json: { blob_key: file_blob.key, blob_id: file_blob.id } + end + def update ActiveRecord::Base.transaction do @macro.update!(macros_with_user) @macro.set_visibility(current_user, permitted_params) + process_attachments @macro.save! rescue StandardError => e Rails.logger.error e @@ -42,6 +55,17 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController head :ok end + def process_attachments + actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' } + return if actions.blank? + + actions.each do |action| + blob_id = action['action_params'] + blob = ActiveStorage::Blob.find_by(id: blob_id) + @macro.files.attach(blob) + end + end + def permitted_params params.permit( :name, :account_id, :visibility, diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index 6ac000b58..62a98a872 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -14,7 +14,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController @portal.members << agents end - def show; end + def show + @all_articles = @portal.articles + @articles = @all_articles.search(locale: params[:locale]) + end def create @portal = Current.account.portals.build(portal_params) diff --git a/app/controllers/platform/api/v1/accounts_controller.rb b/app/controllers/platform/api/v1/accounts_controller.rb index 9c8f60f38..78143410f 100644 --- a/app/controllers/platform/api/v1/accounts_controller.rb +++ b/app/controllers/platform/api/v1/accounts_controller.rb @@ -3,16 +3,12 @@ class Platform::Api::V1::AccountsController < PlatformController @resource = Account.new(account_params) @resource.save! @platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource) - render json: @resource end - def show - render json: @resource - end + def show; end def update @resource.update!(account_params) - render json: @resource end def destroy @@ -27,6 +23,14 @@ class Platform::Api::V1::AccountsController < PlatformController end def account_params - params.permit(:name, :locale) + if permitted_params[:enabled_features] + return permitted_params.except(:enabled_features).merge(selected_feature_flags: permitted_params[:enabled_features].map(&:to_sym)) + end + + permitted_params + end + + def permitted_params + params.permit(:name, :locale, enabled_features: [], limits: {}) end end diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index 0dfa2ea9b..ae0fb2b6a 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -1,7 +1,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController before_action :ensure_custom_domain_request, only: [:show, :index] before_action :portal - before_action :set_category + before_action :set_category, except: [:index] before_action :set_article, only: [:show] layout 'portal' @@ -20,7 +20,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController end def set_category - @category = @portal.categories.find_by!(slug: params[:category_slug]) + @category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present? end def portal diff --git a/app/helpers/file_type_helper.rb b/app/helpers/file_type_helper.rb index db3f6249d..03b807aad 100644 --- a/app/helpers/file_type_helper.rb +++ b/app/helpers/file_type_helper.rb @@ -8,6 +8,12 @@ module FileTypeHelper :file end + # Used in case of DIRECT_UPLOADS_ENABLED=true + def file_type_by_signed_id(signed_id) + blob = ActiveStorage::Blob.find_signed(signed_id) + file_type(blob&.content_type) + end + def image_file?(content_type) [ 'image/jpeg', diff --git a/app/javascript/dashboard/api/helpCenter/categories.js b/app/javascript/dashboard/api/helpCenter/categories.js index b6eae3f32..01658497e 100644 --- a/app/javascript/dashboard/api/helpCenter/categories.js +++ b/app/javascript/dashboard/api/helpCenter/categories.js @@ -7,8 +7,8 @@ class CategoriesAPI extends PortalsAPI { super('categories', { accountScoped: true }); } - get({ portalSlug }) { - return axios.get(`${this.url}/${portalSlug}/categories`); + get({ portalSlug, locale }) { + return axios.get(`${this.url}/${portalSlug}/categories?locale=${locale}`); } create({ portalSlug, categoryObj }) { diff --git a/app/javascript/dashboard/api/helpCenter/portals.js b/app/javascript/dashboard/api/helpCenter/portals.js index 520453540..28220b0b5 100644 --- a/app/javascript/dashboard/api/helpCenter/portals.js +++ b/app/javascript/dashboard/api/helpCenter/portals.js @@ -6,6 +6,10 @@ class PortalsAPI extends ApiClient { super('portals', { accountScoped: true }); } + getPortal({ portalSlug, locale }) { + return axios.get(`${this.url}/${portalSlug}?locale=${locale}`); + } + updatePortal({ portalSlug, portalObj }) { return axios.patch(`${this.url}/${portalSlug}`, portalObj); } diff --git a/app/javascript/dashboard/components/index.js b/app/javascript/dashboard/components/index.js index 5e58aa049..f5ce5d856 100644 --- a/app/javascript/dashboard/components/index.js +++ b/app/javascript/dashboard/components/index.js @@ -5,9 +5,12 @@ import Button from './ui/WootButton'; import Code from './Code'; import ColorPicker from './widgets/ColorPicker'; import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue'; +import ConfirmModal from './widgets/modal/ConfirmationModal.vue'; +import ContextMenu from './ui/ContextMenu.vue'; import DeleteModal from './widgets/modal/DeleteModal.vue'; import DropdownItem from 'shared/components/ui/dropdown/DropdownItem'; import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; +import FeatureToggle from './widgets/FeatureToggle'; import HorizontalBar from './widgets/chart/HorizontalBarChart'; import Input from './widgets/forms/Input.vue'; import Label from './ui/Label'; @@ -21,8 +24,6 @@ import SubmitButton from './buttons/FormSubmitButton'; import Tabs from './ui/Tabs/Tabs'; import TabsItem from './ui/Tabs/TabsItem'; import Thumbnail from './widgets/Thumbnail.vue'; -import ConfirmModal from './widgets/modal/ConfirmationModal.vue'; -import ContextMenu from './ui/ContextMenu.vue'; const WootUIKit = { AvatarUploader, @@ -31,9 +32,12 @@ const WootUIKit = { Code, ColorPicker, ConfirmDeleteModal, + ConfirmModal, + ContextMenu, DeleteModal, DropdownItem, DropdownMenu, + FeatureToggle, HorizontalBar, Input, Label, @@ -47,8 +51,6 @@ const WootUIKit = { Tabs, TabsItem, Thumbnail, - ConfirmModal, - ContextMenu, install(Vue) { const keys = Object.keys(this); keys.pop(); // remove 'install' from keys diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue index de10dda4a..2dd23544f 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue @@ -26,7 +26,7 @@ :class="{ 'text-truncate': shouldTruncate }" > {{ label }} - + {{ childItemCount }} @@ -76,7 +76,7 @@ export default { type: String, default: '', }, - isHelpCenterSidebar: { + showChildCount: { type: Boolean, default: false, }, @@ -127,11 +127,16 @@ $label-badge-size: var(--space-slab); color: var(--w-500); border-color: var(--w-25); } + &.is-active .count-view { + background: var(--w-75); + color: var(--w-500); + } } .menu-label { flex-grow: 1; - line-height: var(--space-two); + display: inline-flex; + align-items: center; } .inbox-icon { @@ -175,10 +180,6 @@ $label-badge-size: var(--space-slab); font-weight: var(--font-weight-bold); margin-left: var(--space-smaller); padding: var(--space-zero) var(--space-smaller); - - &.is-active { - background: var(--w-50); - color: var(--w-500); - } + line-height: var(--font-size-small); } diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index 8661a488f..644258f50 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -4,16 +4,15 @@ -
- {{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }} -
@@ -104,14 +96,6 @@ export default { type: Object, default: () => ({}), }, - isHelpCenterSidebar: { - type: Boolean, - default: false, - }, - isCategoryEmpty: { - type: Boolean, - default: false, - }, }, computed: { ...mapGetters({ @@ -161,8 +145,8 @@ export default { this.menuItem.toStateName === 'settings_applications' ); }, - isArticlesView() { - return this.$store.state.route.name === this.menuItem.toStateName; + isCurrentRoute() { + return this.$store.state.route.name.includes(this.menuItem.toStateName); }, computedClass() { @@ -181,12 +165,11 @@ export default { } return ' '; } - if (this.isHelpCenterSidebar) { - if (this.isArticlesView) { - return 'is-active'; - } - return ' '; + + if (this.isCurrentRoute) { + return 'is-active'; } + return ''; }, }, @@ -222,6 +205,9 @@ export default { onClickOpen() { this.$emit('open'); }, + showChildCount(count) { + return Number.isInteger(count); + }, }, }; @@ -277,6 +263,11 @@ export default { color: var(--w-500); border-color: var(--w-25); } + + &.is-active .count-view { + background: var(--w-75); + color: var(--w-600); + } } .secondary-menu--icon { @@ -306,15 +297,12 @@ export default { top: -1px; } -.sidebar-item .button.menu-item--new { - display: inline-flex; - height: var(--space-medium); - margin: var(--space-smaller) 0; - padding: var(--space-smaller); - color: var(--s-500); +.sidebar-item .menu-item--new { + padding: var(--space-small) 0; - &:hover { - color: var(--w-500); + .button { + display: inline-flex; + color: var(--s-500); } } @@ -340,11 +328,6 @@ export default { font-weight: var(--font-weight-bold); margin-left: var(--space-smaller); padding: var(--space-zero) var(--space-smaller); - - &.is-active { - background: var(--w-50); - color: var(--w-500); - } } .submenu-icons { @@ -356,10 +339,4 @@ export default { margin-left: var(--space-small); } } - -.empty-text { - color: var(--s-500); - font-size: var(--font-size-small); - margin: var(--space-smaller); -} diff --git a/app/javascript/dashboard/components/ui/TimeAgo.vue b/app/javascript/dashboard/components/ui/TimeAgo.vue index c262a8aa0..c796ed0f6 100644 --- a/app/javascript/dashboard/components/ui/TimeAgo.vue +++ b/app/javascript/dashboard/components/ui/TimeAgo.vue @@ -1,17 +1,15 @@ - {{ timeAgo }} + {{ timeAgo }} diff --git a/app/javascript/dashboard/components/widgets/TableFooter.vue b/app/javascript/dashboard/components/widgets/TableFooter.vue index c89b37b01..e28099bff 100644 --- a/app/javascript/dashboard/components/widgets/TableFooter.vue +++ b/app/javascript/dashboard/components/widgets/TableFooter.vue @@ -83,75 +83,71 @@ export default { }, pageSize: { type: Number, - default: 15, + default: 25, }, totalCount: { type: Number, default: 0, }, - onPageChange: { - type: Function, - default: () => {}, - }, }, computed: { isFooterVisible() { return this.totalCount && !(this.firstIndex > this.totalCount); }, firstIndex() { - const firstIndex = this.pageSize * (this.currentPage - 1) + 1; - return firstIndex; + return this.pageSize * (this.currentPage - 1) + 1; }, lastIndex() { - const index = Math.min(this.totalCount, this.pageSize * this.currentPage); - return index; + return Math.min(this.totalCount, this.pageSize * this.currentPage); }, searchButtonClass() { return this.searchQuery !== '' ? 'show' : ''; }, hasLastPage() { - const isDisabled = - this.currentPage === Math.ceil(this.totalCount / this.pageSize); - return isDisabled; + return !!Math.ceil(this.totalCount / this.pageSize); }, hasFirstPage() { - const isDisabled = this.currentPage === 1; - return isDisabled; + return this.currentPage === 1; }, hasNextPage() { - const isDisabled = - this.currentPage === Math.ceil(this.totalCount / this.pageSize); - return isDisabled; + return this.currentPage === Math.ceil(this.totalCount / this.pageSize); }, hasPrevPage() { - const isDisabled = this.currentPage === 1; - return isDisabled; + return this.currentPage === 1; }, }, methods: { onNextPage() { - if (this.hasNextPage) return; + if (this.hasNextPage) { + return; + } const newPage = this.currentPage + 1; this.onPageChange(newPage); }, onPrevPage() { - if (this.hasPrevPage) return; - + if (this.hasPrevPage) { + return; + } const newPage = this.currentPage - 1; this.onPageChange(newPage); }, onFirstPage() { - if (this.hasFirstPage) return; - + if (this.hasFirstPage) { + return; + } const newPage = 1; this.onPageChange(newPage); }, onLastPage() { - if (this.hasLastPage) return; - + if (this.hasLastPage) { + return; + } const newPage = Math.ceil(this.totalCount / this.pageSize); this.onPageChange(newPage); }, + onPageChange(page) { + this.$emit('page-change', page); + }, }, }; diff --git a/app/javascript/dashboard/components/widgets/Thumbnail.vue b/app/javascript/dashboard/components/widgets/Thumbnail.vue index 4bfc729de..e0d9da24c 100644 --- a/app/javascript/dashboard/components/widgets/Thumbnail.vue +++ b/app/javascript/dashboard/components/widgets/Thumbnail.vue @@ -1,74 +1,23 @@Macros
A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions.
A macro can be helpful in 2 ways.
As an agent assist: If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.
As an option to onboard a team member: Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.
", + "ERROR": "Something went wrong. Please try again", + "ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.", + "ADD": { + "FORM": { + "NAME": { + "LABEL": "Macro name", + "PLACEHOLDER": "Enter a name for your macro", + "ERROR": "Name is required for creating a macro" + }, + "ACTIONS": { + "LABEL": "Actions" + } + }, + "API": { + "SUCCESS_MESSAGE": "Macro added successfully", + "ERROR_MESSAGE": "Unable to create macro, Please try again later" + } + }, + "LIST": { + "TABLE_HEADER": ["Name", "Created by", "Last updated by", "Visibility"], + "404": "No macros found" + }, + "DELETE": { + "TOOLTIP": "Delete macro", + "CONFIRM": { + "MESSAGE": "Are you sure to delete ", + "YES": "Yes, Delete", + "NO": "No" + }, + "API": { + "SUCCESS_MESSAGE": "Macro deleted successfully", + "ERROR_MESSAGE": "There was an error deleting the macro. Please try again later" + } + }, + "EDIT": { + "TOOLTIP": "Edit macro", + "API": { + "SUCCESS_MESSAGE": "Macro updated successfully", + "ERROR_MESSAGE": "Could not update Macro, Please try again later" + } + }, + "EDITOR": { + "START_FLOW": "Start Flow", + "END_FLOW": "End Flow", + "LOADING": "Fetching macro", + "ADD_BTN_TOOLTIP": "Add new action", + "DELETE_BTN_TOOLTIP": "Delete Action", + "VISIBILITY": { + "LABEL": "Macro Visibility", + "GLOBAL": { + "LABEL": "Public", + "DESCRIPTION": "This macro is available publicly for all agents in this account." + }, + "PERSONAL": { + "LABEL": "Private", + "DESCRIPTION": "This macro will be private to you and not be available to others." + } + } + }, + "EXECUTE": { + "BUTTON_TOOLTIP": "Execute", + "PREVIEW": "Preview Macro", + "EXECUTED_SUCCESSFULLY": "Macro executed successfully" + } } } diff --git a/app/javascript/dashboard/mixins/macrosMixin.js b/app/javascript/dashboard/mixins/macrosMixin.js new file mode 100644 index 000000000..d28f82ab5 --- /dev/null +++ b/app/javascript/dashboard/mixins/macrosMixin.js @@ -0,0 +1,20 @@ +export default { + methods: { + getDropdownValues(type) { + switch (type) { + case 'assign_team': + case 'send_email_to_team': + return this.teams; + case 'add_label': + return this.labels.map(i => { + return { + id: i.title, + name: i.title, + }; + }); + default: + return []; + } + }, + }, +}; diff --git a/app/javascript/dashboard/mixins/specs/macros.spec.js b/app/javascript/dashboard/mixins/specs/macros.spec.js new file mode 100644 index 000000000..5c9804ee9 --- /dev/null +++ b/app/javascript/dashboard/mixins/specs/macros.spec.js @@ -0,0 +1,41 @@ +import { createWrapper } from '@vue/test-utils'; +import macrosMixin from '../macrosMixin'; +import Vue from 'vue'; +import { teams, labels } from '../../helper/specs/macrosFixtures'; +describe('webhookMixin', () => { + describe('#getEventLabel', () => { + it('returns correct i18n translation:', () => { + const Component = { + render() {}, + title: 'MyComponent', + mixins: [macrosMixin], + data: () => { + return { + teams, + labels, + }; + }, + methods: { + $t(text) { + return text; + }, + }, + }; + + const resolvedLabels = labels.map(i => { + return { + id: i.title, + name: i.title, + }; + }); + + const Constructor = Vue.extend(Component); + const vm = new Constructor().$mount(); + const wrapper = createWrapper(vm); + expect(wrapper.vm.getDropdownValues('assign_team')).toEqual(teams); + expect(wrapper.vm.getDropdownValues('send_email_to_team')).toEqual(teams); + expect(wrapper.vm.getDropdownValues('add_label')).toEqual(resolvedLabels); + expect(wrapper.vm.getDropdownValues()).toEqual([]); + }); + }); +}); diff --git a/app/javascript/dashboard/mixins/specs/uiSettings.spec.js b/app/javascript/dashboard/mixins/specs/uiSettings.spec.js index 79c06939e..7959f8996 100644 --- a/app/javascript/dashboard/mixins/specs/uiSettings.spec.js +++ b/app/javascript/dashboard/mixins/specs/uiSettings.spec.js @@ -116,6 +116,7 @@ describe('uiSettingsMixin', () => { const wrapper = shallowMount(Component, { store, localVue }); expect(wrapper.vm.conversationSidebarItemsOrder).toEqual([ { name: 'conversation_actions' }, + { name: 'macros' }, { name: 'conversation_info' }, { name: 'contact_attributes' }, { name: 'previous_conversation' }, diff --git a/app/javascript/dashboard/mixins/uiSettings.js b/app/javascript/dashboard/mixins/uiSettings.js index 1711145a4..25a9a1903 100644 --- a/app/javascript/dashboard/mixins/uiSettings.js +++ b/app/javascript/dashboard/mixins/uiSettings.js @@ -2,6 +2,7 @@ import { mapGetters } from 'vuex'; export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = [ { name: 'conversation_actions' }, { name: 'conversation_participants' }, + { name: 'macros' }, { name: 'conversation_info' }, { name: 'contact_attributes' }, { name: 'previous_conversation' }, @@ -31,7 +32,17 @@ export default { ...mapGetters({ uiSettings: 'getUISettings' }), conversationSidebarItemsOrder() { const { conversation_sidebar_items_order: itemsOrder } = this.uiSettings; - return itemsOrder || DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER; + // If the sidebar order is not set, use the default order. + if (!itemsOrder) { + return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER; + } + // If the sidebar order doesn't have the new elements, then add them to the list. + DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => { + if (!itemsOrder.find(i => i.name === item.name)) { + itemsOrder.push(item); + } + }); + return itemsOrder; }, contactSidebarItemsOrder() { const { contact_sidebar_items_order: itemsOrder } = this.uiSettings; diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue index dbac2f0bc..bef3ceccb 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue @@ -24,9 +24,9 @@ @on-sort-change="onSortChange" />{{ macro.name }}
+{{ action.actionName }}
+{{ action.actionValue }}
++ {{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }} +
+ {{ thHeader }} + | + + +
---|
{{ $t('MACROS.EDITOR.VISIBILITY.LABEL') }}
++ {{ $t('MACROS.ORDER_INFO') }} +
+