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