diff --git a/app/controllers/api/v1/accounts/dashboard_apps_controller.rb b/app/controllers/api/v1/accounts/dashboard_apps_controller.rb
index a8d7ebcb9..fce80036b 100644
--- a/app/controllers/api/v1/accounts/dashboard_apps_controller.rb
+++ b/app/controllers/api/v1/accounts/dashboard_apps_controller.rb
@@ -34,6 +34,7 @@ class Api::V1::Accounts::DashboardAppsController < Api::V1::Accounts::BaseContro
def permitted_payload
params.require(:dashboard_app).permit(
:title,
+ :show_on_sidebar,
content: [:url, :type]
)
end
diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
index cef4346e9..284b20237 100644
--- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
+++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
@@ -66,6 +66,7 @@ provideSidebarContext({
const inboxes = useMapGetter('inboxes/getInboxes');
const labels = useMapGetter('labels/getLabelsOnSidebar');
+const dashboardApps = useMapGetter('dashboardApps/getAppsOnSidebar');
const teams = useMapGetter('teams/getMyTeams');
const contactCustomViews = useMapGetter('customViews/getContactCustomViews');
const conversationCustomViews = useMapGetter(
@@ -80,6 +81,7 @@ onMounted(() => {
store.dispatch('attributes/get');
store.dispatch('customViews/get', 'conversation');
store.dispatch('customViews/get', 'contact');
+ store.dispatch('dashboardApps/get');
});
const sortedInboxes = computed(() =>
@@ -120,7 +122,7 @@ const newReportRoutes = () => [
const reportRoutes = computed(() => newReportRoutes());
const menuItems = computed(() => {
- return [
+ const items = [
{
name: 'Inbox',
label: t('SIDEBAR.INBOX'),
@@ -514,6 +516,23 @@ const menuItems = computed(() => {
],
},
];
+
+ if (dashboardApps.value.length > 0) {
+ const settingsIndex = items.findIndex(item => item.name === 'Settings');
+ items.splice(settingsIndex, 0, {
+ name: 'Apps',
+ label: t('SIDEBAR.APPS'),
+ icon: 'i-lucide-layout-grid',
+ children: dashboardApps.value.map(app => ({
+ name: `app-${app.id}`,
+ label: app.title,
+ to: accountScopedRoute('dashboard_app_view', { appId: app.id }),
+ activeOn: ['dashboard_app_view'],
+ })),
+ });
+ }
+
+ return items;
});
diff --git a/app/javascript/dashboard/components-next/switch/Switch.vue b/app/javascript/dashboard/components-next/switch/Switch.vue
index 41d0bf8af..643924193 100644
--- a/app/javascript/dashboard/components-next/switch/Switch.vue
+++ b/app/javascript/dashboard/components-next/switch/Switch.vue
@@ -1,6 +1,13 @@
+
+
+
+
+
+
+
+
+ {{ t('INTEGRATION_SETTINGS.DASHBOARD_APPS.VIEW.NOT_FOUND') }}
+
+
+
+
+
+ {{ dashboardApp.title }}
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/dashboardApps/dashboardApps.routes.js b/app/javascript/dashboard/routes/dashboard/dashboardApps/dashboardApps.routes.js
new file mode 100644
index 000000000..5505bf627
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/dashboardApps/dashboardApps.routes.js
@@ -0,0 +1,16 @@
+import { frontendURL } from '../../../helper/URLHelper';
+import DashboardAppView from './DashboardAppView.vue';
+
+export default {
+ routes: [
+ {
+ path: frontendURL('accounts/:accountId/dashboard-apps/:appId'),
+ name: 'dashboard_app_view',
+ meta: {
+ permissions: ['administrator', 'agent'],
+ },
+ component: DashboardAppView,
+ props: route => ({ appId: route.params.appId }),
+ },
+ ],
+};
diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/DashboardApps/DashboardAppModal.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/DashboardApps/DashboardAppModal.vue
index ddbeb30fc..a99143195 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/integrations/DashboardApps/DashboardAppModal.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/DashboardApps/DashboardAppModal.vue
@@ -4,10 +4,12 @@ import { required, url } from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
+import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue';
export default {
components: {
NextButton,
+ ToggleSwitch,
},
props: {
show: {
@@ -41,6 +43,7 @@ export default {
isLoading: false,
app: {
title: '',
+ show_on_sidebar: false,
content: {
type: 'frame',
url: '',
@@ -62,6 +65,7 @@ export default {
if (this.mode === 'UPDATE' && this.selectedAppData) {
this.app.title = this.selectedAppData.title;
this.app.content = this.selectedAppData.content[0];
+ this.app.show_on_sidebar = this.selectedAppData.show_on_sidebar ?? false;
}
},
methods: {
@@ -69,6 +73,7 @@ export default {
// Reset the data once closed
this.app = {
title: '',
+ show_on_sidebar: false,
content: { type: 'frame', url: '' },
};
this.$emit('close');
@@ -83,6 +88,7 @@ export default {
const action = this.mode.toLowerCase();
const payload = {
title: this.app.title,
+ show_on_sidebar: this.app.show_on_sidebar,
content: [this.app.content],
};
@@ -149,6 +155,19 @@ export default {
@input="v$.app.content.url.$touch"
@blur="v$.app.content.url.$touch"
/>
+
+
+
+
record.show_on_sidebar)
+ .sort((a, b) => a.title.localeCompare(b.title));
+ },
};
export const actions = {
diff --git a/app/javascript/dashboard/store/modules/specs/dashboardApps/getters.spec.js b/app/javascript/dashboard/store/modules/specs/dashboardApps/getters.spec.js
index c27788581..816a1c561 100644
--- a/app/javascript/dashboard/store/modules/specs/dashboardApps/getters.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/dashboardApps/getters.spec.js
@@ -26,4 +26,30 @@ describe('#getters', () => {
isDeleting: false,
});
});
+
+ it('getAppsOnSidebar', () => {
+ const state = {
+ records: [
+ {
+ id: 1,
+ title: 'Zebra App',
+ show_on_sidebar: true,
+ },
+ {
+ id: 2,
+ title: 'Alpha App',
+ show_on_sidebar: true,
+ },
+ {
+ id: 3,
+ title: 'Beta App',
+ show_on_sidebar: false,
+ },
+ ],
+ };
+ const result = getters.getAppsOnSidebar(state);
+ expect(result).toHaveLength(2);
+ expect(result[0].title).toEqual('Alpha App');
+ expect(result[1].title).toEqual('Zebra App');
+ });
});
diff --git a/app/models/dashboard_app.rb b/app/models/dashboard_app.rb
index e8a7edd4c..86016dd6c 100644
--- a/app/models/dashboard_app.rb
+++ b/app/models/dashboard_app.rb
@@ -2,13 +2,14 @@
#
# 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
+# id :bigint not null, primary key
+# content :jsonb
+# show_on_sidebar :boolean default(FALSE), not null
+# title :string not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :bigint not null
+# user_id :bigint
#
# Indexes
#
diff --git a/app/views/api/v1/models/_dashboard_app.json.jbuilder b/app/views/api/v1/models/_dashboard_app.json.jbuilder
index f8632d28a..e1ce976fb 100644
--- a/app/views/api/v1/models/_dashboard_app.json.jbuilder
+++ b/app/views/api/v1/models/_dashboard_app.json.jbuilder
@@ -1,4 +1,5 @@
json.id resource.id
json.title resource.title
json.content resource.content
+json.show_on_sidebar resource.show_on_sidebar
json.created_at resource.created_at
diff --git a/db/migrate/20251117003944_add_show_on_sidebar_to_dashboard_apps.rb b/db/migrate/20251117003944_add_show_on_sidebar_to_dashboard_apps.rb
new file mode 100644
index 000000000..bc97cac70
--- /dev/null
+++ b/db/migrate/20251117003944_add_show_on_sidebar_to_dashboard_apps.rb
@@ -0,0 +1,5 @@
+class AddShowOnSidebarToDashboardApps < ActiveRecord::Migration[7.1]
+ def change
+ add_column :dashboard_apps, :show_on_sidebar, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 492001621..469ac88bf 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2025_10_03_091242) do
+ActiveRecord::Schema[7.1].define(version: 2025_11_17_003944) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -773,6 +773,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_10_03_091242) do
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.boolean "show_on_sidebar", default: false, 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
diff --git a/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb b/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb
index 100f914bb..e6ca523ae 100644
--- a/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb
@@ -55,6 +55,9 @@ RSpec.describe 'DashboardAppsController', type: :request do
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(:payload_with_sidebar) do
+ { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'https://link.com' }], show_on_sidebar: true } }
+ end
let(:no_ssl_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'http://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' }] } } }
@@ -86,6 +89,17 @@ RSpec.describe 'DashboardAppsController', type: :request do
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
end
+ it 'creates the dashboard app with show_on_sidebar' do
+ expect do
+ post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
+ params: payload_with_sidebar
+ end.to change(DashboardApp, :count).by(1)
+
+ expect(response).to have_http_status(:success)
+ json_response = response.parsed_body
+ expect(json_response['show_on_sidebar']).to be true
+ end
+
it 'creates the dashboard app even if the URL does not have SSL' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
@@ -134,6 +148,7 @@ RSpec.describe 'DashboardAppsController', type: :request do
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(:payload_with_sidebar) { { dashboard_app: { show_on_sidebar: true } } }
let(:user) { create(:user, account: account) }
let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
@@ -159,6 +174,16 @@ RSpec.describe 'DashboardAppsController', type: :request do
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 'updates the show_on_sidebar attribute' do
+ patch "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
+ headers: user.create_new_auth_token,
+ params: payload_with_sidebar,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(dashboard_app.reload.show_on_sidebar).to be true
+ end
end
end