feat: Allow users to create dashboard apps to give agents more context (#4761)

This commit is contained in:
Pranav Raj S 2022-06-01 11:13:10 +05:30 committed by GitHub
parent 55f7be4ffc
commit b9aa4444b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 585 additions and 21 deletions

View 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

View file

@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class DashboardAppsAPI extends ApiClient {
constructor() {
super('dashboard_apps', { accountScoped: true });
}
}
export default new DashboardAppsAPI();

View 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');
});
});

View file

@ -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>

View file

@ -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);

View file

@ -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!",

View file

@ -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,
},
});

View 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,
};

View file

@ -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 }],
]);
});
});
});

View file

@ -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,
});
});
});

View file

@ -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' }]);
});
});
});

View file

@ -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',
};

View file

@ -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,

View file

@ -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)

View 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

View file

@ -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

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app

View file

@ -0,0 +1,3 @@
json.array! @dashboard_apps do |dashboard_app|
json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: dashboard_app
end

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app

View file

@ -0,0 +1,4 @@
json.id resource.id
json.title resource.title
json.content resource.content
json.created_at resource.created_at

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View 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