diff --git a/app/controllers/api/v1/accounts/dashboard_apps_controller.rb b/app/controllers/api/v1/accounts/dashboard_apps_controller.rb
new file mode 100644
index 000000000..a8d7ebcb9
--- /dev/null
+++ b/app/controllers/api/v1/accounts/dashboard_apps_controller.rb
@@ -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
diff --git a/app/javascript/dashboard/api/dashboardApps.js b/app/javascript/dashboard/api/dashboardApps.js
new file mode 100644
index 000000000..b22ab698e
--- /dev/null
+++ b/app/javascript/dashboard/api/dashboardApps.js
@@ -0,0 +1,9 @@
+import ApiClient from './ApiClient';
+
+class DashboardAppsAPI extends ApiClient {
+ constructor() {
+ super('dashboard_apps', { accountScoped: true });
+ }
+}
+
+export default new DashboardAppsAPI();
diff --git a/app/javascript/dashboard/api/specs/dashboardApps.spec.js b/app/javascript/dashboard/api/specs/dashboardApps.spec.js
new file mode 100644
index 000000000..f3196ae2a
--- /dev/null
+++ b/app/javascript/dashboard/api/specs/dashboardApps.spec.js
@@ -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');
+ });
+});
diff --git a/app/javascript/dashboard/components/widgets/DashboardApp/Frame.vue b/app/javascript/dashboard/components/widgets/DashboardApp/Frame.vue
new file mode 100644
index 000000000..0c2c0e8ed
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/DashboardApp/Frame.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
index 4849c2e97..494bd5c82 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
@@ -6,7 +6,20 @@
:is-contact-panel-open="isContactPanelOpen"
@contact-panel-toggle="onToggleContactPanel"
/>
-
@@ -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);
diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/File.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/File.vue
index a8f2993d3..5861e0df1 100644
--- a/app/javascript/dashboard/components/widgets/conversation/bubble/File.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/bubble/File.vue
@@ -29,8 +29,11 @@ export default {
},
computed: {
fileName() {
- const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
- return filename;
+ if (this.url) {
+ const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
+ return filename || this.$t('CONVERSATION.UNKNOWN_FILE_TYPE');
+ }
+ return this.$t('CONVERSATION.UNKNOWN_FILE_TYPE');
},
},
methods: {
diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json
index ac25a5aed..c38fb16a4 100644
--- a/app/javascript/dashboard/i18n/locale/en/conversation.json
+++ b/app/javascript/dashboard/i18n/locale/en/conversation.json
@@ -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!",
@@ -30,6 +31,7 @@
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
+ "UNKNOWN_FILE_TYPE": "Unknown File",
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js
index 88c36cdf8..5cd1a0112 100755
--- a/app/javascript/dashboard/store/index.js
+++ b/app/javascript/dashboard/store/index.js
@@ -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,
},
});
diff --git a/app/javascript/dashboard/store/modules/dashboardApps.js b/app/javascript/dashboard/store/modules/dashboardApps.js
new file mode 100644
index 000000000..fda1f4de4
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/dashboardApps.js
@@ -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,
+};
diff --git a/app/javascript/dashboard/store/modules/specs/dashboardApps/actions.spec.js b/app/javascript/dashboard/store/modules/specs/dashboardApps/actions.spec.js
new file mode 100644
index 000000000..d24d879bf
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/dashboardApps/actions.spec.js
@@ -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 }],
+ ]);
+ });
+ });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/dashboardApps/getters.spec.js b/app/javascript/dashboard/store/modules/specs/dashboardApps/getters.spec.js
new file mode 100644
index 000000000..c27788581
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/dashboardApps/getters.spec.js
@@ -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,
+ });
+ });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/dashboardApps/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/dashboardApps/mutations.spec.js
new file mode 100644
index 000000000..aaf4da33e
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/dashboardApps/mutations.spec.js
@@ -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' }]);
+ });
+ });
+});
diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js
index f89a5205d..b78e468a1 100755
--- a/app/javascript/dashboard/store/mutation-types.js
+++ b/app/javascript/dashboard/store/mutation-types.js
@@ -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',
};
diff --git a/app/javascript/widget/store/modules/contacts.js b/app/javascript/widget/store/modules/contacts.js
index 587b84ac6..b82cda8ad 100644
--- a/app/javascript/widget/store/modules/contacts.js
+++ b/app/javascript/widget/store/modules/contacts.js
@@ -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,
diff --git a/app/models/account.rb b/app/models/account.rb
index b14887957..7dd1cc229 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -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)
diff --git a/app/models/dashboard_app.rb b/app/models/dashboard_app.rb
new file mode 100644
index 000000000..1f1b73d5c
--- /dev/null
+++ b/app/models/dashboard_app.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 6c3349473..534ebea21 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/views/api/v1/accounts/dashboard_apps/create.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/create.json.jbuilder
new file mode 100644
index 000000000..fd21053d3
--- /dev/null
+++ b/app/views/api/v1/accounts/dashboard_apps/create.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app
diff --git a/app/views/api/v1/accounts/dashboard_apps/index.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/index.json.jbuilder
new file mode 100644
index 000000000..d7e1f5a06
--- /dev/null
+++ b/app/views/api/v1/accounts/dashboard_apps/index.json.jbuilder
@@ -0,0 +1,3 @@
+json.array! @dashboard_apps do |dashboard_app|
+ json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: dashboard_app
+end
diff --git a/app/views/api/v1/accounts/dashboard_apps/show.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/show.json.jbuilder
new file mode 100644
index 000000000..fd21053d3
--- /dev/null
+++ b/app/views/api/v1/accounts/dashboard_apps/show.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app
diff --git a/app/views/api/v1/accounts/dashboard_apps/update.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/update.json.jbuilder
new file mode 100644
index 000000000..fd21053d3
--- /dev/null
+++ b/app/views/api/v1/accounts/dashboard_apps/update.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app
diff --git a/app/views/api/v1/models/_dashboard_app.json.jbuilder b/app/views/api/v1/models/_dashboard_app.json.jbuilder
new file mode 100644
index 000000000..f8632d28a
--- /dev/null
+++ b/app/views/api/v1/models/_dashboard_app.json.jbuilder
@@ -0,0 +1,4 @@
+json.id resource.id
+json.title resource.title
+json.content resource.content
+json.created_at resource.created_at
diff --git a/config/routes.rb b/config/routes.rb
index b2029a739..a09311b3e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/db/migrate/20220525141844_create_dashboard_apps.rb b/db/migrate/20220525141844_create_dashboard_apps.rb
new file mode 100644
index 000000000..0e4fee9e2
--- /dev/null
+++ b/db/migrate/20220525141844_create_dashboard_apps.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index e7382572e..4e2ba8095 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2022_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
diff --git a/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb b/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb
new file mode 100644
index 000000000..9a6b0fa67
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb
@@ -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
diff --git a/spec/factories/dashboard_app.rb b/spec/factories/dashboard_app.rb
new file mode 100644
index 000000000..559e966b6
--- /dev/null
+++ b/spec/factories/dashboard_app.rb
@@ -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
diff --git a/yarn.lock b/yarn.lock
index bd138a972..a4e62441d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6563,9 +6563,9 @@ events@^3.0.0:
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
eventsource@^1.0.7:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf"
- integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.1.tgz#4544a35a57d7120fba4fa4c86cb4023b2c09df2f"
+ integrity sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==
dependencies:
original "^1.0.0"