diff --git a/package.json b/package.json index 0538824e..4c45c0d4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@sentry/integrations": "^5.29.2", "@vue-stripe/vue-stripe": "^4.2.9", "@vue/web-component-wrapper": "^1.2.0", - "@weni/unnnic-system": "^1.12.4", + "@weni/unnnic-system": "^1.16.6", "axios": "^0.21.1", "core-js": "^3.6.5", "dotenv": "^9.0.2", diff --git a/src/api/projects.js b/src/api/projects.js index 4f2404ed..efbf0e37 100644 --- a/src/api/projects.js +++ b/src/api/projects.js @@ -1,4 +1,7 @@ +import axios from 'axios'; import request from './request.js'; +import getEnv from '../utils/env.js'; +import KCService from '../services/Keycloak.js'; export default { getProject({ uuid }) { @@ -106,4 +109,17 @@ export default { getTemplates() { return request.$http().get('v2/projects/template-type/'); }, + + async apiFlowsGetSuccessOrg({ flowUuid }) { + const { data } = await axios.get( + `${getEnv('URL_FLOWS')}/api/v2/success_orgs/${flowUuid}`, + { + headers: { + Authorization: `Bearer ${KCService.keycloak.token}`, + }, + }, + ); + + return data; + }, }; diff --git a/src/app.vue b/src/app.vue index d0902e09..430c2530 100644 --- a/src/app.vue +++ b/src/app.vue @@ -75,6 +75,7 @@ :routes="['chats']" class="page" dont-update-when-changes-language + name="chats" /> @@ -121,7 +122,6 @@ import sendAllIframes from './utils/plugins/sendAllIframes'; import iframessa from 'iframessa'; import KnowUserModal from './components/KnowUserModal/Index.vue'; import RightBar from './components/common/RightBar/Index.vue'; -import axios from 'axios'; import TrialPeriod from './modals/TrialPeriod.vue'; import { setUser } from '@sentry/browser'; @@ -271,22 +271,6 @@ export default { } } else if (event.data?.event === 'chats:update-unread-messages') { this.unreadMessages = event.data.unreadMessages; - } else if (event.data?.event === 'ia:get-flows-length') { - this.$refs['system-ia'].$refs.iframe.contentWindow.postMessage( - { - event: 'connect:set-flows-length', - flowsLength: this.$store.getters.currentProject?.flow_count || 0, - }, - '*', - ); - } else if (event.data?.event === 'flows:redirect') { - this.$router.push({ - name: 'push', - params: { - projectUuid: this.$route.params.projectUuid, - internal: event.data.path.split('/'), - }, - }); } if (content.startsWith(prefix)) { @@ -314,8 +298,39 @@ export default { } }); - iframessa.getter('flowsLength', () => { - return this.$store.getters.currentProject?.flow_count || 0; + iframessa.getter('hasFlows', async () => { + const { has_flows } = await this.$store.dispatch( + 'getSuccessOrgStatusByFlowUuid', + { + flowUuid: this.$store.getters.currentProject.flow_organization, + }, + ); + + return has_flows; + }); + + iframessa.on('redirectToFlows', ({ data }) => { + this.$router.push({ + name: 'push', + params: { + projectUuid: this.$route.params.projectUuid, + internal: data.path.split('/'), + }, + }); + }); + + iframessa.on('redirectToSettingsChats', ({ data }) => { + this.$router.push({ + name: 'settingsChats', + params: { + projectUuid: this.$route.params.projectUuid, + internal: data.path.split('/'), + }, + }); + }); + + iframessa.getter('isOpenHowToIntegrateChatsModal', () => { + return true; }); }, @@ -542,21 +557,36 @@ export default { } try { const flowUuid = this.$store.getters.currentProject.flow_organization; - const response = await axios.get( - `${getEnv('URL_FLOWS')}/api/v2/success_orgs/${flowUuid}`, - { - headers: { - Authorization: `Bearer ${this.$keycloak.token}`, - }, - }, - ); - const { has_ia, has_flows, has_channel, has_msg } = response.data; + + let oldValues = null; + + if (this.$store.state.Project.championChatbots[flowUuid]) { + oldValues = this.$store.state.Project.championChatbots[flowUuid]; + } + + const { has_ia, has_flows, has_channel, has_msg } = + await this.$store.dispatch('getSuccessOrgStatusByFlowUuid', { + flowUuid: this.$store.getters.currentProject.flow_organization, + force: true, + }); + + iframessa.modules.ai?.emit('update:hasFlows', has_flows); + const level = [has_flows, has_ia, has_channel, has_msg].lastIndexOf(true) + 1; - if (this.championChatbotsByProject[projectUuid] === undefined) { - this.$set(this.championChatbotsByProject, projectUuid, level); - } - if (this.championChatbotsByProject[projectUuid] <= 3 && level >= 4) { + + if ( + level >= 4 && + oldValues && + [ + oldValues.has_flows, + oldValues.has_ia, + oldValues.has_channel, + oldValues.has_msg, + ].lastIndexOf(true) + + 1 < + 4 + ) { this.$store.dispatch('openModal', { type: 'confirm', showClose: true, @@ -591,8 +621,7 @@ export default { }, }, }); - this.championChatbotsByProject[projectUuid] = level; - } else if (level <= 3) { + } else if (level < 4) { setTimeout(() => { this.verifyIfChampionChatbotStatusChanged({ projectUuid, diff --git a/src/assets/tutorial/sidebar-chats.gif b/src/assets/tutorial/sidebar-chats.gif new file mode 100644 index 00000000..1c33cf19 Binary files /dev/null and b/src/assets/tutorial/sidebar-chats.gif differ diff --git a/src/assets/tutorial/sidebar-integrations.gif b/src/assets/tutorial/sidebar-integrations.gif new file mode 100644 index 00000000..41e1526d Binary files /dev/null and b/src/assets/tutorial/sidebar-integrations.gif differ diff --git a/src/assets/tutorial/sidebar-intelligences.gif b/src/assets/tutorial/sidebar-intelligences.gif new file mode 100644 index 00000000..b0905e47 Binary files /dev/null and b/src/assets/tutorial/sidebar-intelligences.gif differ diff --git a/src/assets/tutorial/sidebar-studio.gif b/src/assets/tutorial/sidebar-studio.gif new file mode 100644 index 00000000..6a2279b8 Binary files /dev/null and b/src/assets/tutorial/sidebar-studio.gif differ diff --git a/src/components/SidebarModal.vue b/src/components/SidebarModal.vue new file mode 100644 index 00000000..fe232e98 --- /dev/null +++ b/src/components/SidebarModal.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/components/external/Sidebar.vue b/src/components/external/Sidebar.vue index e2677689..d6c35304 100644 --- a/src/components/external/Sidebar.vue +++ b/src/components/external/Sidebar.vue @@ -18,6 +18,36 @@ + + @@ -26,9 +56,19 @@ import { mapGetters, mapActions } from 'vuex'; import { get } from 'lodash'; import getEnv from '@/utils/env'; import { PROJECT_ROLE_CHATUSER } from '../users/permissionsObjects'; +import SidebarModal from '../SidebarModal.vue'; +import gifStudio from '../../assets/tutorial/sidebar-studio.gif'; +import gifIntelligences from '../../assets/tutorial/sidebar-intelligences.gif'; +import gifChats from '../../assets/tutorial/sidebar-chats.gif'; +import gifIntegrations from '../../assets/tutorial/sidebar-integrations.gif'; export default { name: 'Sidebar', + + components: { + SidebarModal, + }, + props: { unreadMessages: Number, }, @@ -38,6 +78,10 @@ export default { open: true, current: '', notifyAgents: false, + gifStudio, + gifIntelligences, + gifChats, + gifIntegrations, }; }, @@ -55,11 +99,15 @@ export default { }); }, - mounted() {}, - computed: { ...mapGetters(['currentProject']), + hasFlows() { + return this.$store.state.Project.championChatbots[ + this.$store.getters.currentProject?.flow_organization + ]?.has_flows; + }, + hideModulesButChats() { if ( !this.$store.getters.currentProject.menu.chat.length && @@ -124,6 +172,7 @@ export default { }, { label: 'SIDEBAR.STUDIO', + id: 'studio', icon: 'app-window-edit', viewUrl: `/projects/${get(project, 'uuid')}/studio/init`, show: () => { @@ -132,6 +181,7 @@ export default { }, { label: 'SIDEBAR.BH', + id: 'intelligences', icon: 'science-fiction-robot', viewUrl: `/projects/${get(project, 'uuid')}/bothub/init`, show: () => { @@ -140,6 +190,7 @@ export default { }, { label: 'SIDEBAR.RC', + id: 'chats', icon: 'messaging-we-chat', viewUrl: `/projects/${get(project, 'uuid')}/rocketchat`, show(project) { @@ -149,6 +200,7 @@ export default { }, { label: 'SIDEBAR.chats', + id: 'chats', icon: 'messaging-we-chat', viewUrl: `/projects/${get(project, 'uuid')}/chats/init`, show: (project) => { @@ -169,6 +221,7 @@ export default { items: [ { label: 'SIDEBAR.INTEGRATIONS', + id: 'integrations', icon: 'layout-dashboard', viewUrl: `/projects/${get(project, 'uuid')}/integrations/init`, show: (project) => { diff --git a/src/locales/translations.json b/src/locales/translations.json index 662416ef..a74596bf 100644 --- a/src/locales/translations.json +++ b/src/locales/translations.json @@ -74,6 +74,13 @@ "en": "Learn more", "es": "Saber más" }, + "docs_urls": { + "how_to_create_a_flow": { + "pt-br": "https://docs.weni.ai/l/pt/fluxos/como-criar-um-fluxo", + "en": "https://docs.weni.ai/l/en/flows-category/untitled-article-ld-3", + "es": "https://docs.weni.ai/l/en/flows-category/untitled-article-ld-3" + } + }, "weni_community": { "title": { "pt-br": "Comunidade", @@ -4914,6 +4921,56 @@ "pt-br": "Expandir", "en": "Show", "es": "Expandir" + }, + "modules": { + "studio": { + "title": { + "pt-br": "Estúdio", + "en": "Studio", + "es": "Estudio" + }, + "description": { + "pt-br": "Você pode criar grupos, ver seus contatos e programar o envio de mensagens. Para isso é importante ter um fluxo pronto.", + "en": "You can create groups, see your contacts and program the sending of messages. For this it is important to have a ready flow.", + "es": "Puede crear grupos, ver sus contactos y programar el envío de mensajes. Para esto es importante tener un flujo listo." + } + }, + "intelligences": { + "title": { + "pt-br": "Inteligência Artificial", + "en": "Artificial Intelligence", + "es": "Inteligencia Artificial" + }, + "description": { + "pt-br": "Treine ou conecte o seu chatbot a uma das nossas inteligências disponíveis. Para isso é importante ter um fluxo pronto.", + "en": "Train or connect your chatbot to one of our available intelligences. For this it is important to have a ready flow.", + "es": "Entrena o conecta tu chatbot a una de nuestras inteligencias disponibles. Para esto es importante tener un flujo listo." + } + }, + "chats": { + "title": { + "pt-br": "Chats", + "en": "Chats", + "es": "Chats" + }, + "description": { + "pt-br": "Realize atendimentos dentro da plataforma Weni para isso é importante ter um fluxo pronto para usar o Chats em seus projetos.", + "en": "Perform care within the Weni platform for this it is important to have a flow ready to use chats on your projects.", + "es": "Realice la atención dentro de la plataforma Weni para esto, es importante tener un flujo listo para usar los chats en sus proyectos." + } + }, + "integrations": { + "title": { + "pt-br": "Integrações", + "en": "Integrations", + "es": "Integraciones" + }, + "description": { + "pt-br": "Agilize seus atendimentos conectando seu chatbot a um canal de comunicação, para usar esta função você precisa de um fluxo.", + "en": "Agilize your calls connecting your chatbot to a communication channel to use this function you need a flow.", + "es": "Agilice sus llamadas conectando su chatbot a un canal de comunicación para usar esta función que necesita un flujo." + } + } } }, "NAVBAR": { diff --git a/src/store/project/actions.js b/src/store/project/actions.js index 4e2c8220..9af08ec0 100644 --- a/src/store/project/actions.js +++ b/src/store/project/actions.js @@ -164,4 +164,21 @@ export default { projectUuid, }); }, + + async getSuccessOrgStatusByFlowUuid({ state, commit }, { flowUuid, force }) { + if (!state.championChatbots[flowUuid] || force) { + const { has_ia, has_flows, has_channel, has_msg } = + await projects.apiFlowsGetSuccessOrg({ flowUuid }); + + commit('setChampionChatbot', { + flowUuid, + has_ia, + has_flows, + has_channel, + has_msg, + }); + } + + return state.championChatbots[flowUuid]; + }, }; diff --git a/src/store/project/index.js b/src/store/project/index.js index 72a52f71..0e9f7f76 100644 --- a/src/store/project/index.js +++ b/src/store/project/index.js @@ -9,6 +9,7 @@ const state = { status: null, data: [], }, + championChatbots: {}, }; const getters = { diff --git a/src/store/project/mutations.js b/src/store/project/mutations.js index 28707aa1..95e4252e 100644 --- a/src/store/project/mutations.js +++ b/src/store/project/mutations.js @@ -1,6 +1,15 @@ +import Vue from 'vue'; + export default { setCurrentProject: (state, project) => (state.currentProject = project), + setChampionChatbot: (state, value) => { + Vue.set(state, 'championChatbots', { + ...state.setChampionChatbots, + [value.flowUuid]: value, + }); + }, + PROJECT_CREATE_REQUEST: (state) => (state.loadingCreateProject = true), PROJECT_CREATE_SUCCESS: (state, project) => { state.currentProject = project; diff --git a/src/views/ProjectHomeBlank/ChampionChatbot.vue b/src/views/ProjectHomeBlank/ChampionChatbot.vue index c53abb3d..10618e36 100644 --- a/src/views/ProjectHomeBlank/ChampionChatbot.vue +++ b/src/views/ProjectHomeBlank/ChampionChatbot.vue @@ -99,18 +99,10 @@ export default { try { this.loading = true; - const flowUuid = this.$store.getters.currentProject.flow_organization; - - const response = await axios.get( - `${getEnv('URL_FLOWS')}/api/v2/success_orgs/${flowUuid}`, - { - headers: { - Authorization: `Bearer ${this.$keycloak.token}`, - }, - }, - ); - - const { has_ia, has_flows, has_channel, has_msg } = response.data; + const { has_ia, has_flows, has_channel, has_msg } = + await this.$store.dispatch('getSuccessOrgStatusByFlowUuid', { + flowUuid: this.$store.getters.currentProject.flow_organization, + }); this.level = [has_flows, has_ia, has_channel, has_msg].lastIndexOf(true) + 1; diff --git a/src/views/settings.vue b/src/views/settings.vue index 9d3a7971..b9fb8716 100644 --- a/src/views/settings.vue +++ b/src/views/settings.vue @@ -44,10 +44,6 @@ export default { ExternalSystem, }, - data() { - return {}; - }, - computed: { ...mapGetters(['currentProject']), @@ -133,7 +129,7 @@ export default { const { projectUuid } = this.$route.params; if (!projectUuid) { - return false; + return; } this.$refs['system-project'].reset(); diff --git a/tests/unit/views/settings.spec.js b/tests/unit/views/settings.spec.js new file mode 100644 index 00000000..1d822cac --- /dev/null +++ b/tests/unit/views/settings.spec.js @@ -0,0 +1,130 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import Settings from '@/views/settings.vue'; +import { PROJECT_ROLE_CHATUSER } from '@/components/users/permissionsObjects'; + +window.configs = { + MODULE_CHATS: 'https://chats', +}; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +const externalSystem = { + props: { + routes: Array, + }, + render(h) { + return h('span'); + }, + methods: { + init() { + return this.routes; + }, + + reset() {}, + }, +}; + +const spyExternalSystemInit = jest.spyOn(externalSystem.methods, 'init'); +const spyExternalSystemReset = jest.spyOn(externalSystem.methods, 'reset'); + +const $route = { + name: 'settingsProject', + fullPath: 'project', + params: { + projectUuid: '1', + }, +}; + +const wrapper = shallowMount(Settings, { + store: new Vuex.Store({ + state: { + currentProject: { + menu: { chat: [] }, + authorization: { role: 1 }, + }, + }, + getters: { + currentProject: (state) => state.currentProject, + }, + mutations: { + setCurrentProject(state, value) { + state.currentProject = value; + }, + }, + }), + + localVue, + + mocks: { + $route, + $router: { + replace(href) { + wrapper.vm.$route.name = href.name; + }, + }, + $t: () => 'text', + }, + + stubs: { + 'unnnic-card': true, + 'external-system': externalSystem, + }, +}); + +describe('settings.vue', () => { + it('it should call the project external system init method', async () => { + $route.name = 'settingsProject'; + $route.fullPath = 'project'; + + await wrapper.vm.$nextTick(); + + expect(spyExternalSystemInit).lastReturnedWith(['settingsProject']); + }); + + it('it should call the chats external system init method', async () => { + $route.name = 'settingsChats'; + $route.fullPath = 'chats'; + + await wrapper.vm.$nextTick(); + + expect(spyExternalSystemInit).lastReturnedWith(['settingsChats']); + }); + + it('it should not call system external reset method due projectUuid is not defined', async () => { + wrapper.vm.$route.params.projectUuid = ''; + + await wrapper.vm.$nextTick(); + + expect(spyExternalSystemReset).toHaveBeenCalledTimes(0); + }); + + it('it should call system external reset method two times, project and chats modules, due projectUuid is defined', async () => { + wrapper.vm.$route.params.projectUuid = '2'; + + await wrapper.vm.$nextTick(); + + expect(spyExternalSystemReset).toHaveBeenCalledTimes(2); + }); + + it('it should redirect user to the first valid tab if they try to enter in a non existent page', async () => { + wrapper.vm.$route.name = 'NonExistentPage'; + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.$route.name).toBe(wrapper.vm.pages[0].href.name); + }); + + it('it should hide modules but chats if user authorization role is chat user', async () => { + wrapper.vm.$store.commit('setCurrentProject', { + menu: { chat: [] }, + authorization: { + role: PROJECT_ROLE_CHATUSER, + }, + }); + + expect(wrapper.vm.hideModulesButChats).toBeTruthy(); + }); +}); diff --git a/yarn.lock b/yarn.lock index a5523d74..2a2fd855 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2005,10 +2005,10 @@ "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" -"@weni/unnnic-system@^1.12.4": - version "1.12.4" - resolved "https://registry.yarnpkg.com/@weni/unnnic-system/-/unnnic-system-1.12.4.tgz#d6d4c1d4049609694e892419cce4c331457cb0e4" - integrity sha512-9HrrjjvMx92dDUEKgKC1VSNH/cN33f4foHYpm1PHTFEAgVjr8O/4pWTdh0RwrHcUnFSRXAY8p7+x46+6wynTSg== +"@weni/unnnic-system@^1.16.6": + version "1.16.6" + resolved "https://registry.yarnpkg.com/@weni/unnnic-system/-/unnnic-system-1.16.6.tgz#ecc282b8436f76c9365449055f36cec27782e6bf" + integrity sha512-oMeO61rm+7h9UzV6TqIsUWSAWsjroCakcMVSnCUb2wRQxkTvT11p3LnYGYNGMkH6jTgNPuCqaZ5NdxyFgZBhRw== dependencies: core-js "^3.6.5" moment "^2.29.1"