mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	feat: Allow users to create dashboard apps to give agents more context (#4761)
This commit is contained in:
		
							
								
								
									
										44
									
								
								app/controllers/api/v1/accounts/dashboard_apps_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/controllers/api/v1/accounts/dashboard_apps_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| class Api::V1::Accounts::DashboardAppsController < Api::V1::Accounts::BaseController | ||||
|   before_action :fetch_dashboard_apps, except: [:create] | ||||
|   before_action :fetch_dashboard_app, only: [:show, :update, :destroy] | ||||
|  | ||||
|   def index; end | ||||
|  | ||||
|   def show; end | ||||
|  | ||||
|   def create | ||||
|     @dashboard_app = Current.account.dashboard_apps.create!( | ||||
|       permitted_payload.merge(user_id: Current.user.id) | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def update | ||||
|     @dashboard_app.update!(permitted_payload) | ||||
|   end | ||||
|  | ||||
|   def destroy | ||||
|     @dashboard_app.destroy! | ||||
|     head :no_content | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def fetch_dashboard_apps | ||||
|     @dashboard_apps = Current.account.dashboard_apps | ||||
|   end | ||||
|  | ||||
|   def fetch_dashboard_app | ||||
|     @dashboard_app = @dashboard_apps.find(permitted_params[:id]) | ||||
|   end | ||||
|  | ||||
|   def permitted_payload | ||||
|     params.require(:dashboard_app).permit( | ||||
|       :title, | ||||
|       content: [:url, :type] | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def permitted_params | ||||
|     params.permit(:id) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										9
									
								
								app/javascript/dashboard/api/dashboardApps.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/javascript/dashboard/api/dashboardApps.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import ApiClient from './ApiClient'; | ||||
|  | ||||
| class DashboardAppsAPI extends ApiClient { | ||||
|   constructor() { | ||||
|     super('dashboard_apps', { accountScoped: true }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default new DashboardAppsAPI(); | ||||
							
								
								
									
										13
									
								
								app/javascript/dashboard/api/specs/dashboardApps.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/javascript/dashboard/api/specs/dashboardApps.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import dashboardAppsAPI from '../dashboardApps'; | ||||
| import ApiClient from '../ApiClient'; | ||||
|  | ||||
| describe('#dashboardAppsAPI', () => { | ||||
|   it('creates correct instance', () => { | ||||
|     expect(dashboardAppsAPI).toBeInstanceOf(ApiClient); | ||||
|     expect(dashboardAppsAPI).toHaveProperty('get'); | ||||
|     expect(dashboardAppsAPI).toHaveProperty('show'); | ||||
|     expect(dashboardAppsAPI).toHaveProperty('create'); | ||||
|     expect(dashboardAppsAPI).toHaveProperty('update'); | ||||
|     expect(dashboardAppsAPI).toHaveProperty('delete'); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,64 @@ | ||||
| <template> | ||||
|   <div class="dashboard-app--container"> | ||||
|     <div | ||||
|       v-for="(configItem, index) in config" | ||||
|       :key="index" | ||||
|       class="dashboard-app--list" | ||||
|     > | ||||
|       <iframe | ||||
|         v-if="configItem.type === 'frame' && configItem.url" | ||||
|         :id="`dashboard-app--frame-${index}`" | ||||
|         :src="configItem.url" | ||||
|         @load="() => onIframeLoad(index)" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     config: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|     currentChat: { | ||||
|       type: Object, | ||||
|       default: () => ({}), | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     dashboardAppContext() { | ||||
|       return { | ||||
|         conversation: this.currentChat, | ||||
|         contact: this.$store.getters['contacts/getContact'](this.contactId), | ||||
|       }; | ||||
|     }, | ||||
|     contactId() { | ||||
|       return this.currentChat?.meta?.sender?.id; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     onIframeLoad(index) { | ||||
|       const frameElement = document.getElementById( | ||||
|         `dashboard-app--frame-${index}` | ||||
|       ); | ||||
|       const eventData = { event: 'appContext', data: this.dashboardAppContext }; | ||||
|       frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*'); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .dashboard-app--container, | ||||
| .dashboard-app--list, | ||||
| .dashboard-app--list iframe { | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .dashboard-app--list iframe { | ||||
|   border: 0; | ||||
| } | ||||
| </style> | ||||
| @@ -6,7 +6,20 @@ | ||||
|       :is-contact-panel-open="isContactPanelOpen" | ||||
|       @contact-panel-toggle="onToggleContactPanel" | ||||
|     /> | ||||
|     <div class="messages-and-sidebar"> | ||||
|     <woot-tabs | ||||
|       v-if="dashboardApps.length && currentChat.id" | ||||
|       :index="activeIndex" | ||||
|       class="dashboard-app--tabs" | ||||
|       @change="onDashboardAppTabChange" | ||||
|     > | ||||
|       <woot-tabs-item | ||||
|         v-for="tab in dashboardAppTabs" | ||||
|         :key="tab.key" | ||||
|         :name="tab.name" | ||||
|         :show-badge="false" | ||||
|       /> | ||||
|     </woot-tabs> | ||||
|     <div v-if="!activeIndex" class="messages-and-sidebar"> | ||||
|       <messages-view | ||||
|         v-if="currentChat.id" | ||||
|         :inbox-id="inboxId" | ||||
| @@ -14,7 +27,6 @@ | ||||
|         @contact-panel-toggle="onToggleContactPanel" | ||||
|       /> | ||||
|       <empty-state v-else /> | ||||
|  | ||||
|       <div v-show="showContactPanel" class="conversation-sidebar-wrap"> | ||||
|         <contact-panel | ||||
|           v-if="showContactPanel" | ||||
| @@ -24,21 +36,29 @@ | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|     <dashboard-app-frame | ||||
|       v-else | ||||
|       :key="currentChat.id" | ||||
|       :config="dashboardApps[activeIndex - 1].content" | ||||
|       :current-chat="currentChat" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel'; | ||||
| import ConversationHeader from './ConversationHeader'; | ||||
| import DashboardAppFrame from '../DashboardApp/Frame.vue'; | ||||
| import EmptyState from './EmptyState'; | ||||
| import MessagesView from './MessagesView'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     EmptyState, | ||||
|     MessagesView, | ||||
|     ContactPanel, | ||||
|     ConversationHeader, | ||||
|     DashboardAppFrame, | ||||
|     EmptyState, | ||||
|     MessagesView, | ||||
|   }, | ||||
|  | ||||
|   props: { | ||||
| @@ -52,8 +72,26 @@ export default { | ||||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { activeIndex: 0 }; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ currentChat: 'getSelectedChat' }), | ||||
|     ...mapGetters({ | ||||
|       currentChat: 'getSelectedChat', | ||||
|       dashboardApps: 'dashboardApps/getRecords', | ||||
|     }), | ||||
|     dashboardAppTabs() { | ||||
|       return [ | ||||
|         { | ||||
|           key: 'messages', | ||||
|           name: this.$t('CONVERSATION.DASHBOARD_APP_TAB_MESSAGES'), | ||||
|         }, | ||||
|         ...this.dashboardApps.map(dashboardApp => ({ | ||||
|           key: `dashboard-${dashboardApp.id}`, | ||||
|           name: dashboardApp.title, | ||||
|         })), | ||||
|       ]; | ||||
|     }, | ||||
|     showContactPanel() { | ||||
|       return this.isContactPanelOpen && this.currentChat.id; | ||||
|     }, | ||||
| @@ -70,6 +108,7 @@ export default { | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.fetchLabels(); | ||||
|     this.$store.dispatch('dashboardApps/get'); | ||||
|   }, | ||||
|   methods: { | ||||
|     fetchLabels() { | ||||
| @@ -81,6 +120,9 @@ export default { | ||||
|     onToggleContactPanel() { | ||||
|       this.$emit('contact-panel-toggle'); | ||||
|     }, | ||||
|     onDashboardAppTabChange(index) { | ||||
|       this.activeIndex = index; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| @@ -96,6 +138,11 @@ export default { | ||||
|   background: var(--color-background-light); | ||||
| } | ||||
|  | ||||
| .dashboard-app--tabs { | ||||
|   background: var(--white); | ||||
|   margin-top: -1px; | ||||
| } | ||||
|  | ||||
| .messages-and-sidebar { | ||||
|   display: flex; | ||||
|   background: var(--color-background-light); | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| { | ||||
|   "CONVERSATION": { | ||||
|     "404": "Please select a conversation from left pane", | ||||
|     "DASHBOARD_APP_TAB_MESSAGES": "Messages", | ||||
|     "UNVERIFIED_SESSION": "The identity of this user is not verified", | ||||
|     "NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.", | ||||
|     "NO_MESSAGE_2": " to send a message to your page!", | ||||
|   | ||||
| @@ -3,7 +3,9 @@ import Vuex from 'vuex'; | ||||
|  | ||||
| import accounts from './modules/accounts'; | ||||
| import agents from './modules/agents'; | ||||
| import attributes from './modules/attributes'; | ||||
| import auth from './modules/auth'; | ||||
| import automations from './modules/automations'; | ||||
| import campaigns from './modules/campaigns'; | ||||
| import cannedResponse from './modules/cannedResponse'; | ||||
| import contactConversations from './modules/contactConversations'; | ||||
| @@ -18,6 +20,8 @@ import conversationSearch from './modules/conversationSearch'; | ||||
| import conversationStats from './modules/conversationStats'; | ||||
| import conversationTypingStatus from './modules/conversationTypingStatus'; | ||||
| import csat from './modules/csat'; | ||||
| import customViews from './modules/customViews'; | ||||
| import dashboardApps from './modules/dashboardApps'; | ||||
| import globalConfig from 'shared/store/globalConfig'; | ||||
| import inboxAssignableAgents from './modules/inboxAssignableAgents'; | ||||
| import inboxes from './modules/inboxes'; | ||||
| @@ -30,16 +34,15 @@ import teamMembers from './modules/teamMembers'; | ||||
| import teams from './modules/teams'; | ||||
| import userNotificationSettings from './modules/userNotificationSettings'; | ||||
| import webhooks from './modules/webhooks'; | ||||
| import attributes from './modules/attributes'; | ||||
| import automations from './modules/automations'; | ||||
| import customViews from './modules/customViews'; | ||||
|  | ||||
| Vue.use(Vuex); | ||||
| export default new Vuex.Store({ | ||||
|   modules: { | ||||
|     accounts, | ||||
|     agents, | ||||
|     attributes, | ||||
|     auth, | ||||
|     automations, | ||||
|     campaigns, | ||||
|     cannedResponse, | ||||
|     contactConversations, | ||||
| @@ -54,6 +57,8 @@ export default new Vuex.Store({ | ||||
|     conversationStats, | ||||
|     conversationTypingStatus, | ||||
|     csat, | ||||
|     customViews, | ||||
|     dashboardApps, | ||||
|     globalConfig, | ||||
|     inboxAssignableAgents, | ||||
|     inboxes, | ||||
| @@ -66,8 +71,5 @@ export default new Vuex.Store({ | ||||
|     teams, | ||||
|     userNotificationSettings, | ||||
|     webhooks, | ||||
|     attributes, | ||||
|     automations, | ||||
|     customViews, | ||||
|   }, | ||||
| }); | ||||
|   | ||||
							
								
								
									
										54
									
								
								app/javascript/dashboard/store/modules/dashboardApps.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/javascript/dashboard/store/modules/dashboardApps.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; | ||||
| import types from '../mutation-types'; | ||||
| import DashboardAppsAPI from '../../api/dashboardApps'; | ||||
|  | ||||
| export const state = { | ||||
|   records: [], | ||||
|   uiFlags: { | ||||
|     isFetching: false, | ||||
|     isCreating: false, | ||||
|     isDeleting: false, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const getters = { | ||||
|   getUIFlags(_state) { | ||||
|     return _state.uiFlags; | ||||
|   }, | ||||
|   getRecords(_state) { | ||||
|     return _state.records; | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const actions = { | ||||
|   get: async function getDashboardApps({ commit }) { | ||||
|     commit(types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: true }); | ||||
|     try { | ||||
|       const response = await DashboardAppsAPI.get(); | ||||
|       commit(types.SET_DASHBOARD_APPS, response.data); | ||||
|     } catch (error) { | ||||
|       // Ignore error | ||||
|     } finally { | ||||
|       commit(types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: false }); | ||||
|     } | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const mutations = { | ||||
|   [types.SET_DASHBOARD_APPS_UI_FLAG](_state, data) { | ||||
|     _state.uiFlags = { | ||||
|       ..._state.uiFlags, | ||||
|       ...data, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
|   [types.SET_DASHBOARD_APPS]: MutationHelpers.set, | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|   namespaced: true, | ||||
|   actions, | ||||
|   state, | ||||
|   getters, | ||||
|   mutations, | ||||
| }; | ||||
| @@ -0,0 +1,21 @@ | ||||
| import axios from 'axios'; | ||||
| import { actions } from '../../dashboardApps'; | ||||
| import types from '../../../mutation-types'; | ||||
|  | ||||
| const commit = jest.fn(); | ||||
| global.axios = axios; | ||||
| jest.mock('axios'); | ||||
|  | ||||
| describe('#actions', () => { | ||||
|   describe('#get', () => { | ||||
|     it('sends correct actions if API is success', async () => { | ||||
|       axios.get.mockResolvedValue({ data: [{ title: 'Title 1' }] }); | ||||
|       await actions.get({ commit }); | ||||
|       expect(commit.mock.calls).toEqual([ | ||||
|         [types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: true }], | ||||
|         [types.SET_DASHBOARD_APPS, [{ title: 'Title 1' }]], | ||||
|         [types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: false }], | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,29 @@ | ||||
| import { getters } from '../../dashboardApps'; | ||||
|  | ||||
| describe('#getters', () => { | ||||
|   it('getRecords', () => { | ||||
|     const state = { | ||||
|       records: [ | ||||
|         { | ||||
|           title: '1', | ||||
|           content: [{ link: 'https://google.com', type: 'frame' }], | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|     expect(getters.getRecords(state)).toEqual(state.records); | ||||
|   }); | ||||
|   it('getUIFlags', () => { | ||||
|     const state = { | ||||
|       uiFlags: { | ||||
|         isFetching: true, | ||||
|         isCreating: false, | ||||
|         isDeleting: false, | ||||
|       }, | ||||
|     }; | ||||
|     expect(getters.getUIFlags(state)).toEqual({ | ||||
|       isFetching: true, | ||||
|       isCreating: false, | ||||
|       isDeleting: false, | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,20 @@ | ||||
| import types from '../../../mutation-types'; | ||||
| import { mutations } from '../../dashboardApps'; | ||||
|  | ||||
| describe('#mutations', () => { | ||||
|   describe('#SET_DASHBOARD_APPS_UI_FLAG', () => { | ||||
|     it('set dashboard app ui flags', () => { | ||||
|       const state = { uiFlags: { isCreating: false, isUpdating: false } }; | ||||
|       mutations[types.SET_DASHBOARD_APPS_UI_FLAG](state, { isUpdating: true }); | ||||
|       expect(state.uiFlags).toEqual({ isCreating: false, isUpdating: true }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('#SET_DASHBOARD_APPS', () => { | ||||
|     it('set dashboard records', () => { | ||||
|       const state = { records: [{ title: 'Title 0' }] }; | ||||
|       mutations[types.SET_DASHBOARD_APPS](state, [{ title: 'Title 1' }]); | ||||
|       expect(state.records).toEqual([{ title: 'Title 1' }]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -210,4 +210,8 @@ export default { | ||||
|   SET_CUSTOM_VIEW: 'SET_CUSTOM_VIEW', | ||||
|   ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW', | ||||
|   DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW', | ||||
|  | ||||
|   // Dashboard Apps | ||||
|   SET_DASHBOARD_APPS_UI_FLAG: 'SET_DASHBOARD_APPS_UI_FLAG', | ||||
|   SET_DASHBOARD_APPS: 'SET_DASHBOARD_APPS', | ||||
| }; | ||||
|   | ||||
| @@ -61,7 +61,10 @@ export const actions = { | ||||
|         dispatch('conversationAttributes/getAttributes', {}, { root: true }); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       const data = error && error.response && error.response.data ? error.response.data : error | ||||
|       const data = | ||||
|         error && error.response && error.response.data | ||||
|           ? error.response.data | ||||
|           : error; | ||||
|       IFrameHelper.sendMessage({ | ||||
|         event: 'error', | ||||
|         errorType: SET_USER_ERROR, | ||||
|   | ||||
| @@ -39,27 +39,31 @@ class Account < ApplicationRecord | ||||
|   has_many :agent_bot_inboxes, dependent: :destroy_async | ||||
|   has_many :agent_bots, dependent: :destroy_async | ||||
|   has_many :api_channels, dependent: :destroy_async, class_name: '::Channel::Api' | ||||
|   has_many :articles, dependent: :destroy_async, class_name: '::Article' | ||||
|   has_many :automation_rules, dependent: :destroy | ||||
|   has_many :campaigns, dependent: :destroy_async | ||||
|   has_many :canned_responses, dependent: :destroy_async | ||||
|   has_many :categories, dependent: :destroy_async, class_name: '::Category' | ||||
|   has_many :contacts, dependent: :destroy_async | ||||
|   has_many :conversations, dependent: :destroy_async | ||||
|   has_many :csat_survey_responses, dependent: :destroy_async | ||||
|   has_many :custom_attribute_definitions, dependent: :destroy_async | ||||
|   has_many :custom_filters, dependent: :destroy_async | ||||
|   has_many :dashboard_apps, dependent: :destroy | ||||
|   has_many :data_imports, dependent: :destroy_async | ||||
|   has_many :email_channels, dependent: :destroy_async, class_name: '::Channel::Email' | ||||
|   has_many :facebook_pages, dependent: :destroy_async, class_name: '::Channel::FacebookPage' | ||||
|   has_many :hooks, dependent: :destroy_async, class_name: 'Integrations::Hook' | ||||
|   has_many :inboxes, dependent: :destroy_async | ||||
|   has_many :articles, dependent: :destroy_async, class_name: '::Article' | ||||
|   has_many :categories, dependent: :destroy_async, class_name: '::Category' | ||||
|   has_many :portals, dependent: :destroy_async, class_name: '::Portal' | ||||
|   has_many :labels, dependent: :destroy_async | ||||
|   has_many :line_channels, dependent: :destroy_async, class_name: '::Channel::Line' | ||||
|   has_many :mentions, dependent: :destroy_async | ||||
|   has_many :messages, dependent: :destroy_async | ||||
|   has_many :notes, dependent: :destroy_async | ||||
|   has_many :notification_settings, dependent: :destroy_async | ||||
|   has_many :notifications, dependent: :destroy | ||||
|   has_many :portals, dependent: :destroy_async, class_name: '::Portal' | ||||
|   has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms' | ||||
|   has_many :teams, dependent: :destroy_async | ||||
|   has_many :telegram_bots, dependent: :destroy_async | ||||
|   has_many :telegram_channels, dependent: :destroy_async, class_name: '::Channel::Telegram' | ||||
| @@ -69,10 +73,7 @@ class Account < ApplicationRecord | ||||
|   has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget' | ||||
|   has_many :webhooks, dependent: :destroy_async | ||||
|   has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp' | ||||
|   has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms' | ||||
|   has_many :working_hours, dependent: :destroy_async | ||||
|   has_many :automation_rules, dependent: :destroy | ||||
|   has_many :notifications, dependent: :destroy | ||||
|  | ||||
|   has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING) | ||||
|  | ||||
|   | ||||
							
								
								
									
										49
									
								
								app/models/dashboard_app.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								app/models/dashboard_app.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: dashboard_apps | ||||
| # | ||||
| #  id         :bigint           not null, primary key | ||||
| #  content    :jsonb | ||||
| #  title      :string           not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  account_id :bigint           not null | ||||
| #  user_id    :bigint | ||||
| # | ||||
| # Indexes | ||||
| # | ||||
| #  index_dashboard_apps_on_account_id  (account_id) | ||||
| #  index_dashboard_apps_on_user_id     (user_id) | ||||
| # | ||||
| # Foreign Keys | ||||
| # | ||||
| #  fk_rails_...  (account_id => accounts.id) | ||||
| #  fk_rails_...  (user_id => users.id) | ||||
| # | ||||
| class DashboardApp < ApplicationRecord | ||||
|   belongs_to :user | ||||
|   belongs_to :account | ||||
|   validate :validate_content | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def validate_content | ||||
|     has_invalid_data = self[:content].blank? || !self[:content].is_a?(Array) | ||||
|     self[:content] = [] if has_invalid_data | ||||
|  | ||||
|     content_schema = { | ||||
|       'type' => 'array', | ||||
|       'items' => { | ||||
|         'type' => 'object', | ||||
|         'required' => %w[url type], | ||||
|         'properties' => { | ||||
|           'type' => { 'enum': ['frame'] }, | ||||
|           'url' => { 'type': 'string', 'format' => 'uri' } | ||||
|         } | ||||
|       }, | ||||
|       'additionalProperties' => false, | ||||
|       'minItems' => 1 | ||||
|     } | ||||
|     errors.add(:content, ': Invalid data') unless JSONSchemer.schema(content_schema.to_json).valid?(self[:content]) | ||||
|   end | ||||
| end | ||||
| @@ -83,14 +83,15 @@ class User < ApplicationRecord | ||||
|   has_many :invitees, through: :account_users, class_name: 'User', foreign_key: 'inviter_id', source: :inviter, dependent: :nullify | ||||
|  | ||||
|   has_many :custom_filters, dependent: :destroy_async | ||||
|   has_many :dashboard_apps, dependent: :nullify | ||||
|   has_many :mentions, dependent: :destroy_async | ||||
|   has_many :notes, dependent: :nullify | ||||
|   has_many :notification_settings, dependent: :destroy_async | ||||
|   has_many :notification_subscriptions, dependent: :destroy_async | ||||
|   has_many :notifications, dependent: :destroy_async | ||||
|   has_many :portals, through: :portals_members | ||||
|   has_many :team_members, dependent: :destroy_async | ||||
|   has_many :teams, through: :team_members | ||||
|   has_many :portals, through: :portals_members | ||||
|  | ||||
|   before_validation :set_password_and_uid, on: :create | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1 @@ | ||||
| json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app | ||||
| @@ -0,0 +1,3 @@ | ||||
| json.array! @dashboard_apps do |dashboard_app| | ||||
|   json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: dashboard_app | ||||
| end | ||||
| @@ -0,0 +1 @@ | ||||
| json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app | ||||
| @@ -0,0 +1 @@ | ||||
| json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app | ||||
							
								
								
									
										4
									
								
								app/views/api/v1/models/_dashboard_app.json.jbuilder
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/views/api/v1/models/_dashboard_app.json.jbuilder
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| json.id resource.id | ||||
| json.title resource.title | ||||
| json.content resource.content | ||||
| json.created_at resource.created_at | ||||
| @@ -58,7 +58,7 @@ Rails.application.routes.draw do | ||||
|             post :attach_file, on: :collection | ||||
|           end | ||||
|           resources :campaigns, only: [:index, :create, :show, :update, :destroy] | ||||
|  | ||||
|           resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy] | ||||
|           namespace :channels do | ||||
|             resource :twilio_channel, only: [:create] | ||||
|           end | ||||
|   | ||||
							
								
								
									
										11
									
								
								db/migrate/20220525141844_create_dashboard_apps.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								db/migrate/20220525141844_create_dashboard_apps.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| class CreateDashboardApps < ActiveRecord::Migration[6.1] | ||||
|   def change | ||||
|     create_table :dashboard_apps do |t| | ||||
|       t.string :title, null: false | ||||
|       t.jsonb :content, default: [] | ||||
|       t.references :account, null: false, foreign_key: true | ||||
|       t.references :user, foreign_key: true | ||||
|       t.timestamps | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										15
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								db/schema.rb
									
									
									
									
									
								
							| @@ -10,7 +10,7 @@ | ||||
| # | ||||
| # It's strongly recommended that you check this file into your version control system. | ||||
|  | ||||
| ActiveRecord::Schema.define(version: 2022_05_13_145010) do | ||||
| ActiveRecord::Schema.define(version: 2022_05_25_141844) do | ||||
|  | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "pg_stat_statements" | ||||
| @@ -441,6 +441,17 @@ ActiveRecord::Schema.define(version: 2022_05_13_145010) do | ||||
|     t.index ["user_id"], name: "index_custom_filters_on_user_id" | ||||
|   end | ||||
|  | ||||
|   create_table "dashboard_apps", force: :cascade do |t| | ||||
|     t.string "title", null: false | ||||
|     t.jsonb "content", default: [] | ||||
|     t.bigint "account_id", null: false | ||||
|     t.bigint "user_id" | ||||
|     t.datetime "created_at", precision: 6, null: false | ||||
|     t.datetime "updated_at", precision: 6, null: false | ||||
|     t.index ["account_id"], name: "index_dashboard_apps_on_account_id" | ||||
|     t.index ["user_id"], name: "index_dashboard_apps_on_user_id" | ||||
|   end | ||||
|  | ||||
|   create_table "data_imports", force: :cascade do |t| | ||||
|     t.bigint "account_id", null: false | ||||
|     t.string "data_type", null: false | ||||
| @@ -817,6 +828,8 @@ ActiveRecord::Schema.define(version: 2022_05_13_145010) do | ||||
|   add_foreign_key "csat_survey_responses", "conversations", on_delete: :cascade | ||||
|   add_foreign_key "csat_survey_responses", "messages", on_delete: :cascade | ||||
|   add_foreign_key "csat_survey_responses", "users", column: "assigned_agent_id", on_delete: :cascade | ||||
|   add_foreign_key "dashboard_apps", "accounts" | ||||
|   add_foreign_key "dashboard_apps", "users" | ||||
|   add_foreign_key "data_imports", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "mentions", "conversations", on_delete: :cascade | ||||
|   add_foreign_key "mentions", "users", on_delete: :cascade | ||||
|   | ||||
| @@ -0,0 +1,158 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe 'DashboardAppsController', type: :request do | ||||
|   let(:account) { create(:account) } | ||||
|  | ||||
|   describe 'GET /api/v1/accounts/{account.id}/dashboard_apps' do | ||||
|     context 'when it is an unauthenticated user' do | ||||
|       it 'returns unauthorized' do | ||||
|         get "/api/v1/accounts/#{account.id}/dashboard_apps" | ||||
|  | ||||
|         expect(response).to have_http_status(:unauthorized) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when it is an authenticated user' do | ||||
|       let(:user) { create(:user, account: account) } | ||||
|       let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) } | ||||
|  | ||||
|       it 'returns all dashboard_apps in the account' do | ||||
|         get "/api/v1/accounts/#{account.id}/dashboard_apps", | ||||
|             headers: user.create_new_auth_token, | ||||
|             as: :json | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|         response_body = JSON.parse(response.body) | ||||
|         expect(response_body.first['title']).to eq(dashboard_app.title) | ||||
|         expect(response_body.first['content']).to eq(dashboard_app.content) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'GET /api/v1/accounts/{account.id}/dashboard_apps/:id' do | ||||
|     let(:user) { create(:user, account: account) } | ||||
|     let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) } | ||||
|  | ||||
|     context 'when it is an unauthenticated user' do | ||||
|       it 'returns unauthorized' do | ||||
|         get "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}" | ||||
|  | ||||
|         expect(response).to have_http_status(:unauthorized) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when it is an authenticated user' do | ||||
|       it 'shows the dashboard app' do | ||||
|         get "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}", | ||||
|             headers: user.create_new_auth_token, | ||||
|             as: :json | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|         expect(response.body).to include(dashboard_app.title) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'POST /api/v1/accounts/{account.id}/dashboard_apps' do | ||||
|     let(:payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'https://link.com' }] } } } | ||||
|     let(:invalid_type_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'dda', url: 'https://link.com' }] } } } | ||||
|     let(:invalid_url_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'com' }] } } } | ||||
|  | ||||
|     context 'when it is an unauthenticated user' do | ||||
|       it 'returns unauthorized' do | ||||
|         expect { post "/api/v1/accounts/#{account.id}/dashboard_apps", params: payload }.to change(CustomFilter, :count).by(0) | ||||
|  | ||||
|         expect(response).to have_http_status(:unauthorized) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when it is an authenticated user' do | ||||
|       let(:user) { create(:user, account: account) } | ||||
|  | ||||
|       it 'creates the dashboard app' do | ||||
|         expect do | ||||
|           post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token, | ||||
|                                                                 params: payload | ||||
|         end.to change(DashboardApp, :count).by(1) | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|         json_response = JSON.parse(response.body) | ||||
|         expect(json_response['title']).to eq 'CRM Dashboard' | ||||
|         expect(json_response['content'][0]['link']).to eq payload[:dashboard_app][:content][0][:link] | ||||
|         expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type] | ||||
|       end | ||||
|  | ||||
|       it 'does not create the dashboard app if invalid URL' do | ||||
|         expect do | ||||
|           post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token, | ||||
|                                                                 params: invalid_url_payload | ||||
|         end.to change(DashboardApp, :count).by(0) | ||||
|  | ||||
|         expect(response).to have_http_status(:unprocessable_entity) | ||||
|         json_response = JSON.parse(response.body) | ||||
|         expect(json_response['message']).to eq 'Content : Invalid data' | ||||
|       end | ||||
|  | ||||
|       it 'does not create the dashboard app if invalid type' do | ||||
|         expect do | ||||
|           post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token, | ||||
|                                                                 params: invalid_type_payload | ||||
|         end.to change(DashboardApp, :count).by(0) | ||||
|  | ||||
|         expect(response).to have_http_status(:unprocessable_entity) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'PATCH /api/v1/accounts/{account.id}/dashboard_apps/:id' do | ||||
|     let(:payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'https://link.com' }] } } } | ||||
|     let(:user) { create(:user, account: account) } | ||||
|     let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) } | ||||
|  | ||||
|     context 'when it is an unauthenticated user' do | ||||
|       it 'returns unauthorized' do | ||||
|         put "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}", | ||||
|             params: payload | ||||
|  | ||||
|         expect(response).to have_http_status(:unauthorized) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when it is an authenticated user' do | ||||
|       it 'updates the dashboard app' do | ||||
|         patch "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}", | ||||
|               headers: user.create_new_auth_token, | ||||
|               params: payload, | ||||
|               as: :json | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|         json_response = JSON.parse(response.body) | ||||
|         expect(dashboard_app.reload.title).to eq('CRM Dashboard') | ||||
|         expect(json_response['content'][0]['link']).to eq payload[:dashboard_app][:content][0][:link] | ||||
|         expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type] | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'DELETE /api/v1/accounts/{account.id}/dashboard_apps/:id' do | ||||
|     let(:user) { create(:user, account: account) } | ||||
|     let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) } | ||||
|  | ||||
|     context 'when it is an unauthenticated user' do | ||||
|       it 'returns unauthorized' do | ||||
|         delete "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}" | ||||
|         expect(response).to have_http_status(:unauthorized) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when it is an authenticated admin user' do | ||||
|       it 'deletes dashboard app' do | ||||
|         delete "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}", | ||||
|                headers: user.create_new_auth_token, | ||||
|                as: :json | ||||
|         expect(response).to have_http_status(:no_content) | ||||
|         expect(user.dashboard_apps.count).to be 0 | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										10
									
								
								spec/factories/dashboard_app.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								spec/factories/dashboard_app.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| FactoryBot.define do | ||||
|   factory :dashboard_app do | ||||
|     sequence(:title) { |n| "Dashboard App #{n}" } | ||||
|     content { [{ type: 'frame', url: 'https://chatwoot.com' }] } | ||||
|     user | ||||
|     account | ||||
|   end | ||||
| end | ||||
		Reference in New Issue
	
	Block a user
	 Pranav Raj S
					Pranav Raj S