feat: dashboard apps on sidebar (#146)
* feat: dashboard apps on sidebar * fix: handle dashboard app not found * chore: minor refactoring
This commit is contained in:
parent
14f43a6bc5
commit
7f0748460e
@ -34,6 +34,7 @@ class Api::V1::Accounts::DashboardAppsController < Api::V1::Accounts::BaseContro
|
|||||||
def permitted_payload
|
def permitted_payload
|
||||||
params.require(:dashboard_app).permit(
|
params.require(:dashboard_app).permit(
|
||||||
:title,
|
:title,
|
||||||
|
:show_on_sidebar,
|
||||||
content: [:url, :type]
|
content: [:url, :type]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -66,6 +66,7 @@ provideSidebarContext({
|
|||||||
|
|
||||||
const inboxes = useMapGetter('inboxes/getInboxes');
|
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||||
const labels = useMapGetter('labels/getLabelsOnSidebar');
|
const labels = useMapGetter('labels/getLabelsOnSidebar');
|
||||||
|
const dashboardApps = useMapGetter('dashboardApps/getAppsOnSidebar');
|
||||||
const teams = useMapGetter('teams/getMyTeams');
|
const teams = useMapGetter('teams/getMyTeams');
|
||||||
const contactCustomViews = useMapGetter('customViews/getContactCustomViews');
|
const contactCustomViews = useMapGetter('customViews/getContactCustomViews');
|
||||||
const conversationCustomViews = useMapGetter(
|
const conversationCustomViews = useMapGetter(
|
||||||
@ -80,6 +81,7 @@ onMounted(() => {
|
|||||||
store.dispatch('attributes/get');
|
store.dispatch('attributes/get');
|
||||||
store.dispatch('customViews/get', 'conversation');
|
store.dispatch('customViews/get', 'conversation');
|
||||||
store.dispatch('customViews/get', 'contact');
|
store.dispatch('customViews/get', 'contact');
|
||||||
|
store.dispatch('dashboardApps/get');
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedInboxes = computed(() =>
|
const sortedInboxes = computed(() =>
|
||||||
@ -120,7 +122,7 @@ const newReportRoutes = () => [
|
|||||||
const reportRoutes = computed(() => newReportRoutes());
|
const reportRoutes = computed(() => newReportRoutes());
|
||||||
|
|
||||||
const menuItems = computed(() => {
|
const menuItems = computed(() => {
|
||||||
return [
|
const items = [
|
||||||
{
|
{
|
||||||
name: 'Inbox',
|
name: 'Inbox',
|
||||||
label: t('SIDEBAR.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;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['change']);
|
const emit = defineEmits(['change']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@ -18,6 +25,7 @@ const updateValue = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
|
:id="props.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="relative h-4 transition-colors duration-200 ease-in-out rounded-full w-7 focus:outline-none focus:ring-1 focus:ring-n-brand focus:ring-offset-n-slate-2 focus:ring-offset-2 flex-shrink-0"
|
class="relative h-4 transition-colors duration-200 ease-in-out rounded-full w-7 focus:outline-none focus:ring-1 focus:ring-n-brand focus:ring-offset-n-slate-2 focus:ring-offset-2 flex-shrink-0"
|
||||||
:class="modelValue ? 'bg-n-brand' : 'bg-n-slate-6 disabled:bg-n-slate-6/60'"
|
:class="modelValue ? 'bg-n-brand' : 'bg-n-slate-6 disabled:bg-n-slate-6/60'"
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasOpenedAtleastOnce: false,
|
hasOpenedAtleastOnce: this.isVisible,
|
||||||
iframeLoading: true,
|
iframeLoading: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -46,8 +46,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
isVisible() {
|
isVisible(value) {
|
||||||
if (this.isVisible) {
|
if (value) {
|
||||||
this.hasOpenedAtleastOnce = true;
|
this.hasOpenedAtleastOnce = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -39,6 +39,9 @@ export default {
|
|||||||
currentChat: 'getSelectedChat',
|
currentChat: 'getSelectedChat',
|
||||||
dashboardApps: 'dashboardApps/getRecords',
|
dashboardApps: 'dashboardApps/getRecords',
|
||||||
}),
|
}),
|
||||||
|
conversationDashboardApps() {
|
||||||
|
return this.dashboardApps.filter(app => !app.show_on_sidebar);
|
||||||
|
},
|
||||||
dashboardAppTabs() {
|
dashboardAppTabs() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -46,7 +49,7 @@ export default {
|
|||||||
index: 0,
|
index: 0,
|
||||||
name: this.$t('CONVERSATION.DASHBOARD_APP_TAB_MESSAGES'),
|
name: this.$t('CONVERSATION.DASHBOARD_APP_TAB_MESSAGES'),
|
||||||
},
|
},
|
||||||
...this.dashboardApps.map((dashboardApp, index) => ({
|
...this.conversationDashboardApps.map((dashboardApp, index) => ({
|
||||||
key: `dashboard-${dashboardApp.id}`,
|
key: `dashboard-${dashboardApp.id}`,
|
||||||
index: index + 1,
|
index: index + 1,
|
||||||
name: dashboardApp.title,
|
name: dashboardApp.title,
|
||||||
@ -102,7 +105,7 @@ export default {
|
|||||||
:show-back-button="isOnExpandedLayout && !isInboxView"
|
:show-back-button="isOnExpandedLayout && !isInboxView"
|
||||||
/>
|
/>
|
||||||
<woot-tabs
|
<woot-tabs
|
||||||
v-if="dashboardApps.length && currentChat.id"
|
v-if="conversationDashboardApps.length && currentChat.id"
|
||||||
:index="activeIndex"
|
:index="activeIndex"
|
||||||
class="-mt-px border-t border-t-n-background"
|
class="-mt-px border-t border-t-n-background"
|
||||||
@change="onDashboardAppTabChange"
|
@change="onDashboardAppTabChange"
|
||||||
@ -130,11 +133,11 @@ export default {
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<DashboardAppFrame
|
<DashboardAppFrame
|
||||||
v-for="(dashboardApp, index) in dashboardApps"
|
v-for="(dashboardApp, index) in conversationDashboardApps"
|
||||||
v-show="activeIndex - 1 === index"
|
v-show="activeIndex - 1 === index"
|
||||||
:key="currentChat.id + '-' + dashboardApp.id"
|
:key="currentChat.id + '-' + dashboardApp.id"
|
||||||
:is-visible="activeIndex - 1 === index"
|
:is-visible="activeIndex - 1 === index"
|
||||||
:config="dashboardApps[index].content"
|
:config="conversationDashboardApps[index].content"
|
||||||
:position="index"
|
:position="index"
|
||||||
:current-chat="currentChat"
|
:current-chat="currentChat"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -216,13 +216,17 @@
|
|||||||
"EDIT_TOOLTIP": "Edit app",
|
"EDIT_TOOLTIP": "Edit app",
|
||||||
"DELETE_TOOLTIP": "Delete app"
|
"DELETE_TOOLTIP": "Delete app"
|
||||||
},
|
},
|
||||||
|
"VIEW": {
|
||||||
|
"NOT_FOUND": "We couldn't find that dashboard app."
|
||||||
|
},
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"TITLE_LABEL": "Name",
|
"TITLE_LABEL": "Name",
|
||||||
"TITLE_PLACEHOLDER": "Enter a name for your dashboard app",
|
"TITLE_PLACEHOLDER": "Enter a name for your dashboard app",
|
||||||
"TITLE_ERROR": "A name for the dashboard app is required",
|
"TITLE_ERROR": "A name for the dashboard app is required",
|
||||||
"URL_LABEL": "Endpoint",
|
"URL_LABEL": "Endpoint",
|
||||||
"URL_PLACEHOLDER": "Enter the endpoint URL where your app is hosted",
|
"URL_PLACEHOLDER": "Enter the endpoint URL where your app is hosted",
|
||||||
"URL_ERROR": "A valid URL is required"
|
"URL_ERROR": "A valid URL is required",
|
||||||
|
"SHOW_ON_SIDEBAR_LABEL": "Show on sidebar"
|
||||||
},
|
},
|
||||||
"CREATE": {
|
"CREATE": {
|
||||||
"HEADER": "Add a new dashboard app",
|
"HEADER": "Add a new dashboard app",
|
||||||
|
|||||||
@ -328,6 +328,7 @@
|
|||||||
"HOME": "Home",
|
"HOME": "Home",
|
||||||
"AGENTS": "Agents",
|
"AGENTS": "Agents",
|
||||||
"AGENT_BOTS": "Bots",
|
"AGENT_BOTS": "Bots",
|
||||||
|
"APPS": "Apps",
|
||||||
"AUDIT_LOGS": "Audit Logs",
|
"AUDIT_LOGS": "Audit Logs",
|
||||||
"INBOXES": "Inboxes",
|
"INBOXES": "Inboxes",
|
||||||
"NOTIFICATIONS": "Notifications",
|
"NOTIFICATIONS": "Notifications",
|
||||||
|
|||||||
@ -219,13 +219,17 @@
|
|||||||
"EDIT_TOOLTIP": "Alterar aplicativo",
|
"EDIT_TOOLTIP": "Alterar aplicativo",
|
||||||
"DELETE_TOOLTIP": "Excluir aplicativo"
|
"DELETE_TOOLTIP": "Excluir aplicativo"
|
||||||
},
|
},
|
||||||
|
"VIEW": {
|
||||||
|
"NOT_FOUND": "Não encontramos este aplicativo do painel."
|
||||||
|
},
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"TITLE_LABEL": "Nome",
|
"TITLE_LABEL": "Nome",
|
||||||
"TITLE_PLACEHOLDER": "Digite um nome para o aplicativo",
|
"TITLE_PLACEHOLDER": "Digite um nome para o aplicativo",
|
||||||
"TITLE_ERROR": "É necessário um nome para o aplicativo",
|
"TITLE_ERROR": "É necessário um nome para o aplicativo",
|
||||||
"URL_LABEL": "Endpoint",
|
"URL_LABEL": "Endpoint",
|
||||||
"URL_PLACEHOLDER": "Digite a URL do endpoint onde seu aplicativo está hospedado",
|
"URL_PLACEHOLDER": "Digite a URL do endpoint onde seu aplicativo está hospedado",
|
||||||
"URL_ERROR": "É necessário uma URL válida"
|
"URL_ERROR": "É necessário uma URL válida",
|
||||||
|
"SHOW_ON_SIDEBAR_LABEL": "Mostrar na barra lateral"
|
||||||
},
|
},
|
||||||
"CREATE": {
|
"CREATE": {
|
||||||
"HEADER": "Adicionar um novo aplicativo",
|
"HEADER": "Adicionar um novo aplicativo",
|
||||||
|
|||||||
@ -328,6 +328,7 @@
|
|||||||
"HOME": "Principal",
|
"HOME": "Principal",
|
||||||
"AGENTS": "Agentes",
|
"AGENTS": "Agentes",
|
||||||
"AGENT_BOTS": "Robôs",
|
"AGENT_BOTS": "Robôs",
|
||||||
|
"APPS": "Apps",
|
||||||
"AUDIT_LOGS": "Auditoria",
|
"AUDIT_LOGS": "Auditoria",
|
||||||
"INBOXES": "Caixas de Entrada",
|
"INBOXES": "Caixas de Entrada",
|
||||||
"NOTIFICATIONS": "Notificações",
|
"NOTIFICATIONS": "Notificações",
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { frontendURL } from '../../helper/URLHelper';
|
|||||||
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
||||||
import campaignsRoutes from './campaigns/campaigns.routes';
|
import campaignsRoutes from './campaigns/campaigns.routes';
|
||||||
import { routes as captainRoutes } from './captain/captain.routes';
|
import { routes as captainRoutes } from './captain/captain.routes';
|
||||||
|
import dashboardAppsRoutes from './dashboardApps/dashboardApps.routes';
|
||||||
import AppContainer from './Dashboard.vue';
|
import AppContainer from './Dashboard.vue';
|
||||||
import Suspended from './suspended/Index.vue';
|
import Suspended from './suspended/Index.vue';
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ export default {
|
|||||||
...notificationRoutes,
|
...notificationRoutes,
|
||||||
...helpcenterRoutes.routes,
|
...helpcenterRoutes.routes,
|
||||||
...campaignsRoutes.routes,
|
...campaignsRoutes.routes,
|
||||||
|
...dashboardAppsRoutes.routes,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -0,0 +1,72 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||||
|
import DashboardAppFrame from 'dashboard/components/widgets/DashboardApp/Frame.vue';
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const dashboardApps = useMapGetter('dashboardApps/getRecords');
|
||||||
|
const isLoadingApps = ref(true);
|
||||||
|
|
||||||
|
const appId = computed(() => Number(route.params.appId));
|
||||||
|
|
||||||
|
const dashboardApp = computed(() => {
|
||||||
|
return dashboardApps.value.find(app => app.id === appId.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const notFound = computed(() => !isLoadingApps.value && !dashboardApp.value);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
if (!dashboardApps.value.length) {
|
||||||
|
await store.dispatch('dashboardApps/get');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to fetch dashboard apps', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingApps.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col w-full h-full bg-n-background">
|
||||||
|
<div
|
||||||
|
v-if="isLoadingApps"
|
||||||
|
class="flex items-center justify-center w-full h-full"
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="notFound"
|
||||||
|
class="flex items-center justify-center w-full h-full px-4 text-center"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-n-slate-11">
|
||||||
|
{{ t('INTEGRATION_SETTINGS.DASHBOARD_APPS.VIEW.NOT_FOUND') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col w-full h-full">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 px-4 py-3 border-b border-n-weak bg-n-background"
|
||||||
|
>
|
||||||
|
<h1 class="text-lg font-semibold text-n-slate-12">
|
||||||
|
{{ dashboardApp.title }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-h-0">
|
||||||
|
<DashboardAppFrame
|
||||||
|
v-if="dashboardApp"
|
||||||
|
is-visible
|
||||||
|
:config="dashboardApp.content"
|
||||||
|
:position="0"
|
||||||
|
:current-chat="null"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -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 }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -4,10 +4,12 @@ import { required, url } from '@vuelidate/validators';
|
|||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NextButton,
|
NextButton,
|
||||||
|
ToggleSwitch,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
show: {
|
show: {
|
||||||
@ -41,6 +43,7 @@ export default {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
app: {
|
app: {
|
||||||
title: '',
|
title: '',
|
||||||
|
show_on_sidebar: false,
|
||||||
content: {
|
content: {
|
||||||
type: 'frame',
|
type: 'frame',
|
||||||
url: '',
|
url: '',
|
||||||
@ -62,6 +65,7 @@ export default {
|
|||||||
if (this.mode === 'UPDATE' && this.selectedAppData) {
|
if (this.mode === 'UPDATE' && this.selectedAppData) {
|
||||||
this.app.title = this.selectedAppData.title;
|
this.app.title = this.selectedAppData.title;
|
||||||
this.app.content = this.selectedAppData.content[0];
|
this.app.content = this.selectedAppData.content[0];
|
||||||
|
this.app.show_on_sidebar = this.selectedAppData.show_on_sidebar ?? false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -69,6 +73,7 @@ export default {
|
|||||||
// Reset the data once closed
|
// Reset the data once closed
|
||||||
this.app = {
|
this.app = {
|
||||||
title: '',
|
title: '',
|
||||||
|
show_on_sidebar: false,
|
||||||
content: { type: 'frame', url: '' },
|
content: { type: 'frame', url: '' },
|
||||||
};
|
};
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
@ -83,6 +88,7 @@ export default {
|
|||||||
const action = this.mode.toLowerCase();
|
const action = this.mode.toLowerCase();
|
||||||
const payload = {
|
const payload = {
|
||||||
title: this.app.title,
|
title: this.app.title,
|
||||||
|
show_on_sidebar: this.app.show_on_sidebar,
|
||||||
content: [this.app.content],
|
content: [this.app.content],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -149,6 +155,19 @@ export default {
|
|||||||
@input="v$.app.content.url.$touch"
|
@input="v$.app.content.url.$touch"
|
||||||
@blur="v$.app.content.url.$touch"
|
@blur="v$.app.content.url.$touch"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex items-center w-full gap-3 py-2">
|
||||||
|
<label
|
||||||
|
class="text-sm text-n-slate-12 cursor-pointer"
|
||||||
|
for="show-on-sidebar"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'INTEGRATION_SETTINGS.DASHBOARD_APPS.FORM.SHOW_ON_SIDEBAR_LABEL'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
<ToggleSwitch id="show-on-sidebar" v-model="app.show_on_sidebar" />
|
||||||
|
</div>
|
||||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||||
<NextButton
|
<NextButton
|
||||||
faded
|
faded
|
||||||
|
|||||||
@ -18,6 +18,11 @@ export const getters = {
|
|||||||
getRecords(_state) {
|
getRecords(_state) {
|
||||||
return _state.records;
|
return _state.records;
|
||||||
},
|
},
|
||||||
|
getAppsOnSidebar(_state) {
|
||||||
|
return _state.records
|
||||||
|
.filter(record => record.show_on_sidebar)
|
||||||
|
.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
|||||||
@ -26,4 +26,30 @@ describe('#getters', () => {
|
|||||||
isDeleting: false,
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,13 +2,14 @@
|
|||||||
#
|
#
|
||||||
# Table name: dashboard_apps
|
# Table name: dashboard_apps
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# content :jsonb
|
# content :jsonb
|
||||||
# title :string not null
|
# show_on_sidebar :boolean default(FALSE), not null
|
||||||
# created_at :datetime not null
|
# title :string not null
|
||||||
# updated_at :datetime not null
|
# created_at :datetime not null
|
||||||
# account_id :bigint not null
|
# updated_at :datetime not null
|
||||||
# user_id :bigint
|
# account_id :bigint not null
|
||||||
|
# user_id :bigint
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
json.id resource.id
|
json.id resource.id
|
||||||
json.title resource.title
|
json.title resource.title
|
||||||
json.content resource.content
|
json.content resource.content
|
||||||
|
json.show_on_sidebar resource.show_on_sidebar
|
||||||
json.created_at resource.created_at
|
json.created_at resource.created_at
|
||||||
|
|||||||
@ -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
|
||||||
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These extensions should be enabled to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
enable_extension "pg_trgm"
|
enable_extension "pg_trgm"
|
||||||
@ -773,6 +773,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_10_03_091242) do
|
|||||||
t.bigint "user_id"
|
t.bigint "user_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_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 ["account_id"], name: "index_dashboard_apps_on_account_id"
|
||||||
t.index ["user_id"], name: "index_dashboard_apps_on_user_id"
|
t.index ["user_id"], name: "index_dashboard_apps_on_user_id"
|
||||||
end
|
end
|
||||||
|
|||||||
@ -55,6 +55,9 @@ RSpec.describe 'DashboardAppsController', type: :request do
|
|||||||
|
|
||||||
describe 'POST /api/v1/accounts/{account.id}/dashboard_apps' 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) { { 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(: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_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' }] } } }
|
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]
|
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
|
||||||
end
|
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
|
it 'creates the dashboard app even if the URL does not have SSL' do
|
||||||
expect do
|
expect do
|
||||||
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
|
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
|
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) { { 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(:user) { create(:user, account: account) }
|
||||||
let!(:dashboard_app) { create(:dashboard_app, user: 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]['link']).to eq payload[:dashboard_app][:content][0][:link]
|
||||||
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
|
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user