From 4233c0432c8c76067528cc2f32380f9df4504de4 Mon Sep 17 00:00:00 2001 From: Debora Crescenzo Date: Mon, 11 May 2026 14:31:59 +0100 Subject: [PATCH] MT#65003 Use composable helpers instead of direct Vuex store access - Replace direct `useStore` usage with composable helpers (useActions, useState, useGetters) - Add `setPopupShow` action to wrap `popupShow` mutation and eliminate mutations in favor of `setPopupShow` action - Remove `usePbx` and `useUser` composables - Update documentation This prepares the codebase for eventual Pinia migration by: - Avoiding mutations (Pinia has no mutations) - Centralizing store access through helpers - Enabling future migration with minimal component changes Change-Id: Iddc63b8c3c39fe1a06bb6a78ef322f01fe77a9af (cherry picked from commit e64887e492e6d107fa4f2082c9d111e6fe4978b4) --- doc/architectural-overview.md | 9 +- doc/composables.md | 219 ++++++++++++++---- doc/data-layer.md | 50 ++-- doc/migration-guide.md | 75 +++++- doc/router-navigation-guard.md | 14 +- src/components/AuiMobileAppBadges.vue | 4 +- src/components/CscPopupMenu.vue | 6 +- src/components/CscPopupMenuRingTimeout.vue | 5 +- .../call-forwarding/CscCfAddForm.vue | 8 +- .../CscCfDestinationCustomAnnouncement.vue | 4 +- .../CscCfDestinationNumber.vue | 6 +- .../call-forwarding/CscCfGroupItem.vue | 5 +- src/composables/usePbx.js | 52 ----- src/composables/useUser.js | 61 ----- src/pages/CscPagePbxSettingsConference.vue | 18 +- src/store/call-forwarding/actions.js | 4 + 16 files changed, 312 insertions(+), 228 deletions(-) delete mode 100644 src/composables/usePbx.js delete mode 100644 src/composables/useUser.js diff --git a/doc/architectural-overview.md b/doc/architectural-overview.md index e8933930..a2b47f2e 100644 --- a/doc/architectural-overview.md +++ b/doc/architectural-overview.md @@ -33,8 +33,8 @@ This document describes the architecture of the ngcp-csc-ui project (Customer Se - `router/`: route definitions and meta configuration in `src/router/routes.js`. - `components/`, `pages/`, `layouts/`: UI building blocks and pages organized by feature. - `assets/`, `helpers/`, `mixins/`, `validators/`: supporting libraries and utilities. - - `composables/`: Reusable composition functions that encapsulate state management logic and provide reactive access to Vuex store. Notable files: - - `src/composables/useUser.js` — provides reactive access to user state, authentication actions, and permission checks (capabilities, platform features, profile attributes). + - `composables/`: Reusable composition functions that encapsulate logic and provide reactive access to Vuex store. Notable files: + - `src/composables/useStore.js` — provides helpers (`useState`, `useGetters`, `useActions`) for accessing Vuex store modules in Composition API components. **Note: `useMutations` is available but should be avoided. Use `useActions` instead for Pinia migration compatibility.** ## HTTP API layer (src/api/common.js) @@ -98,10 +98,11 @@ VoIP notes and constraints: ## Composables Layer -Starting with Vue 3 migration, the application introduces composables (`src/composables/`) as an optional layer for accessing Vuex store in Composition API components: +Starting with Vue 3 migration, the application uses composables (`src/composables/`) for accessing Vuex store in Composition API components: - **Purpose**: Provide reactive, encapsulated access to store state and actions -- **Available composables**: `useStore.js` (generic), `useUser.js` (authentication), `usePbx.js` (PBX features), `useGlobals.js` (global properties), `useWait.js` (loading states) +- **Available composables**: `useStore.js` (generic helpers), `useWait.js` (loading states) +- **Recommended pattern**: Use `useState`, `useGetters`, and `useActions` helpers. Avoid `useMutations` and instead wrap mutations in actions for Pinia migration compatibility (Pinia has no mutations). - **Coexistence**: Options API components continue using `mapState`/`mapGetters`/`mapActions`; composables are used when converting to Composition API - **Details**: See `migration-guide.md` for usage patterns and `composables.md` for API reference diff --git a/doc/composables.md b/doc/composables.md index 4c4779b4..67ea998d 100644 --- a/doc/composables.md +++ b/doc/composables.md @@ -16,6 +16,8 @@ Generic helpers for accessing Vuex store. It's a wrapper around Vuex that makes *Problem it solves*: In Composition API, you can't use `mapState`, `mapGetters`, `mapActions` like in Options API. You need a different approach to access store state, getters, and actions reactively. +> ⚠️ Use `useState`, `useGetters`, and `useActions` helpers. **Avoid `useMutations` if possible** — wrap mutations in actions instead. Pinia has no mutations, so using `useActions` now makes future migration easier. + ### Functions #### `useStore()` @@ -85,10 +87,11 @@ await login({ username: 'test', password: 'pass' }) ``` -#### `useMutations(moduleName, keys)` +#### `useMutations(moduleName, keys)` **Avoid unless it's absolutely necessary** Map mutations from a module to functions. -*Use when*: You need to commit store mutations (rarely needed - prefer actions). + +Remember, Pinia has no mutations. Create an action in your store that calls the mutation, then use `useActions` to access it.This helper remains in case the mutation needs to be accessed directly for some reason. ```vue ``` + +### Accessing Store Within a Composable + +If your composable needs global store state, use `useGetters` or `useActions`: + +```javascript +import { ref } from 'vue' +import { useGetters } from 'src/composables/useStore' +import { getCustomerPhonebook } from 'src/api/subscriber' + +export function useCustomerPhonebook() { + // Access global state + const { getCustomerId } = useGetters('user', ['getCustomerId']) + + // Local state + const phonebook = ref([]) + const loading = ref(false) + + const loadPhonebook = async () => { + loading.value = true + try { + const list = await getCustomerPhonebook({ + customerId: getCustomerId.value + }) + phonebook.value = list.data + } finally { + loading.value = false + } + } + + return { + phonebook, + loading, + loadPhonebook + } +} +``` diff --git a/doc/data-layer.md b/doc/data-layer.md index f7d53eef..c23f479e 100644 --- a/doc/data-layer.md +++ b/doc/data-layer.md @@ -152,27 +152,25 @@ if (apiIsCanceledRequest(err)) { ## Composables for State Access -Starting with the migration to Vue 3 Composition API, the application introduces composables as a layer between components and Vuex store: +Starting with the migration to Vue 3 Composition API, the application uses composables for accessing Vuex store: -1. **Purpose**: Composables provide reactive access to store state and actions without direct store coupling in components. +1. **Purpose**: Composables provide reactive access to store state, getters, and actions in Composition API components. -2. **Pattern** (based on `src/composables/useUser.js`): - - Import store and computed from Vue - - Define reactive refs using `computed(() => store.state/getters.xxx)` - - Wrap store actions in simple functions - - Return an object with all reactive properties and methods +2. **Pattern** (using `src/composables/useStore.js` helpers): + - Import the store helpers (`useState`, `useGetters`, `useActions`) + - Map state, getters, or actions from specific store modules + - All returned values are reactive computed refs 3. **Example**: ```javascript - import { computed } from 'vue' - import { store } from 'src/boot/store' + import { useGetters, useActions } from 'src/composables/useStore' - export function useUser() { - const isAdmin = computed(() => store.getters['user/isAdmin']) - const login = (credentials) => store.dispatch('user/login', credentials) + // In your component setup + const { isAdmin, isLogged } = useGetters('user', ['isAdmin', 'isLogged']) + const { login, logout } = useActions('user', ['login', 'logout']) - return { isAdmin, login } - } + // isAdmin and isLogged are computed refs + // login and logout are action functions ``` ## Store Access Patterns @@ -195,19 +193,15 @@ export default { ### Composition API with ` ``` -**Note**: When using composables, getters return `computed` refs, so access the value with `.value` when needed in computed expressions. +**Note**: When using store helpers, getters return `computed` refs, so access the value with `.value` when needed in computed expressions. ### Menu Hierarchy and Nested Items diff --git a/src/components/AuiMobileAppBadges.vue b/src/components/AuiMobileAppBadges.vue index 2bfc71d4..9d73a2f4 100644 --- a/src/components/AuiMobileAppBadges.vue +++ b/src/components/AuiMobileAppBadges.vue @@ -54,12 +54,12 @@ diff --git a/src/components/CscPopupMenuRingTimeout.vue b/src/components/CscPopupMenuRingTimeout.vue index b0817527..d01c242d 100644 --- a/src/components/CscPopupMenuRingTimeout.vue +++ b/src/components/CscPopupMenuRingTimeout.vue @@ -20,7 +20,7 @@ v-model="timeout" buttons data-cy="csc-forwarding-ring-timeout-global-editbox" - @before-show="$store.commit('callForwarding/popupShow','after-ring-timeout')" + @before-show="setPopupShow('after-ring-timeout')" @save="updateRingTimeoutEvent($event)" > store.state.callForwarding.announcements) +const { announcements } = useState('callForwarding', ['announcements']) -const { isSpCe, isPbxEnabled } = useUser() +const { isSpCe, isPbxEnabled } = useGetters('user', ['isSpCe', 'isPbxEnabled']) const isDestinationNumber = computed(() => destinationType.value === 'number') const isDestinationCustomAnnouncement = computed(() => destinationType.value === 'customhours') diff --git a/src/components/call-forwarding/CscCfDestinationCustomAnnouncement.vue b/src/components/call-forwarding/CscCfDestinationCustomAnnouncement.vue index 733e396a..398ad325 100644 --- a/src/components/call-forwarding/CscCfDestinationCustomAnnouncement.vue +++ b/src/components/call-forwarding/CscCfDestinationCustomAnnouncement.vue @@ -9,7 +9,7 @@ v-model="announcement" buttons anchor="top left" - @before-show="$store.commit('callForwarding/popupShow', null)" + @before-show="setPopupShow(null)" @save="$emit('input', $event)" > import CscCfDestination from 'components/call-forwarding/CscCfDestination' import { showGlobalError } from 'src/helpers/ui' +import { mapActions } from 'vuex' export default { name: 'CscCfDestinationCustomAnnouncement', components: { CscCfDestination }, @@ -51,6 +52,7 @@ export default { } }, methods: { + ...mapActions('callForwarding', ['setPopupShow']), checkAnnouncement () { const fieldFilled = this.announcement if (!fieldFilled) { diff --git a/src/components/call-forwarding/CscCfDestinationNumber.vue b/src/components/call-forwarding/CscCfDestinationNumber.vue index 9dea256a..f983aa76 100644 --- a/src/components/call-forwarding/CscCfDestinationNumber.vue +++ b/src/components/call-forwarding/CscCfDestinationNumber.vue @@ -8,7 +8,7 @@ v-slot="scope" v-model="number" buttons - @before-show="$store.commit('callForwarding/popupShow', null)" + @before-show="setPopupShow(null)" @save="$emit('input', $event)" > import CscCfDestination from 'components/call-forwarding/CscCfDestination' import CscInput from 'components/form/CscInput' +import { mapActions } from 'vuex' export default { name: 'CscCfDestinationNumber', components: { CscInput, CscCfDestination }, @@ -45,6 +46,9 @@ export default { number: this.$attrs.value } }, + methods: { + ...mapActions('callForwarding', ['setPopupShow']) + }, watch: { '$attrs.value' (value) { this.number = value diff --git a/src/components/call-forwarding/CscCfGroupItem.vue b/src/components/call-forwarding/CscCfGroupItem.vue index c5748577..6aab6ad1 100644 --- a/src/components/call-forwarding/CscCfGroupItem.vue +++ b/src/components/call-forwarding/CscCfGroupItem.vue @@ -54,7 +54,7 @@ v-slot="scope" v-model="changedDestinationTimeout" buttons - @before-show="$store.commit('callForwarding/popupShow', null)" + @before-show="setPopupShow(null)" @save="updateDestinationTimeoutEvent({ destinationTimeout: $event, destinationIndex: destinationIndex, @@ -222,7 +222,8 @@ export default { 'moveDestination', 'updateDestinationTimeout', 'rewriteDestination', - 'updateAnnouncement' + 'updateAnnouncement', + 'setPopupShow' ]), async moveDestinationEvent (direction) { const targetIndex = direction === 'up' ? this.destinationIndex - 1 : this.destinationIndex + 1 diff --git a/src/composables/usePbx.js b/src/composables/usePbx.js deleted file mode 100644 index 8d6e0023..00000000 --- a/src/composables/usePbx.js +++ /dev/null @@ -1,52 +0,0 @@ -import { useStore } from 'src/composables/useStore' -import { computed } from 'vue' - -export function usePbx () { - const store = useStore() - - // State - const numbers = computed(() => store.getters['pbx/numbers']) - const seatList = computed(() => store.state.pbx.seatList) - const groupList = computed(() => store.state.pbx.groupList) - const deviceProfileList = computed(() => store.state.pbx.deviceProfileList) - - // Getters - const numberOptions = computed(() => store.getters['pbx/getNumberOptions']) - const seatOptions = computed(() => store.getters['pbx/getSeatOptions']) - const groupOptions = computed(() => store.getters['pbx/getGroupOptions']) - const subscriberOptions = computed(() => store.getters['pbx/getSubscriberOptions']) - - // Loading states - const isNumbersRequesting = computed(() => store.getters['pbx/isNumbersRequesting']) - const isSubscribersRequesting = computed(() => store.getters['pbx/isSubscribersRequesting']) - - // Actions - const loadNumbers = () => store.dispatch('pbx/loadNumbers') - const loadSubscribers = () => store.dispatch('pbx/loadSubscribers') - const loadProfiles = () => store.dispatch('pbx/loadProfiles') - const loadDeviceModel = (payload) => store.dispatch('pbx/loadDeviceModel', payload) - - return { - // State - numbers, - seatList, - groupList, - deviceProfileList, - - // Getters - numberOptions, - seatOptions, - groupOptions, - subscriberOptions, - - // Loading states - isNumbersRequesting, - isSubscribersRequesting, - - // Actions - loadNumbers, - loadSubscribers, - loadProfiles, - loadDeviceModel - } -} diff --git a/src/composables/useUser.js b/src/composables/useUser.js deleted file mode 100644 index 060284bb..00000000 --- a/src/composables/useUser.js +++ /dev/null @@ -1,61 +0,0 @@ -import { useStore } from 'src/composables/useStore' -import { computed } from 'vue' - -export function useUser () { - const store = useStore() - - // State - const subscriber = computed(() => store.state.user.subscriber) - const platformInfo = computed(() => store.state.user.platformInfo) - - // Getters - const isLogged = computed(() => store.getters['user/isLogged']) - const username = computed(() => store.getters['user/getUsername']) - const isAdmin = computed(() => store.getters['user/isAdmin']) - const isPbxAdmin = computed(() => store.getters['user/isPbxAdmin']) - const isPbxEnabled = computed(() => store.getters['user/isPbxEnabled']) - const isSpCe = computed(() => store.getters['user/isSpCe']) - const loginRequesting = computed(() => store.getters['user/loginRequesting']) - const isPasswordChanging = computed(() => store.getters['user/isPasswordChanging']) - - // Actions - const login = (credentials) => store.dispatch('user/login', credentials) - const logout = () => store.dispatch('user/logout') - const changePassword = (newPassword) => store.dispatch('user/changePassword', newPassword) - const changeSIPPassword = (newPassword) => store.dispatch('user/changeSIPPassword', newPassword) - const initUser = () => store.dispatch('user/initUser') - - // Getters with parameters - const hasCapability = (capability) => store.getters['user/hasCapability'](capability) - const hasPlatformFeature = (feature) => store.getters['user/hasPlatformFeature'](feature) - const hasSubscriberProfileAttribute = (attribute) => - store.getters['user/hasSubscriberProfileAttribute'](attribute) - - return { - // State - subscriber, - platformInfo, - - // Getters - isLogged, - username, - isAdmin, - isPbxAdmin, - isPbxEnabled, - isSpCe, - loginRequesting, - isPasswordChanging, - - // Actions - login, - logout, - changePassword, - changeSIPPassword, - initUser, - - // Getters with parameters - hasCapability, - hasPlatformFeature, - hasSubscriberProfileAttribute - } -} diff --git a/src/pages/CscPagePbxSettingsConference.vue b/src/pages/CscPagePbxSettingsConference.vue index 4c11ebbb..d168fc2a 100644 --- a/src/pages/CscPagePbxSettingsConference.vue +++ b/src/pages/CscPagePbxSettingsConference.vue @@ -55,6 +55,7 @@ import { useVuelidate } from '@vuelidate/core' import { helpers, numeric } from '@vuelidate/validators' import CscPage from 'components/CscPage' import CscInputSaveable from 'components/form/CscInputSaveable' +import { useActions, useGetters, useState } from 'src/composables/useStore' import { useWait } from 'src/composables/useWait' import { PROFILE_ATTRIBUTE_MAP } from 'src/constants' import { showToast } from 'src/helpers/ui' @@ -65,7 +66,6 @@ import { ref } from 'vue' import { useI18n } from 'vue-i18n' -import { useStore } from 'vuex' defineOptions({ name: 'CscPagePbxSettingsConference' }) @@ -79,7 +79,12 @@ const WAITERS = { updateConferencePin: 'csc-pbx-call-settings-update-conference-pin' } -const store = useStore() +const { subscriberPreferences } = useState('callSettings', ['subscriberPreferences']) +const { hasSubscriberProfileAttribute } = useGetters('user', ['hasSubscriberProfileAttribute']) +const { loadSubscriberPreferencesAction, fieldUpdateAction } = useActions('callSettings', [ + 'loadSubscriberPreferencesAction', + 'fieldUpdateAction' +]) const wait = useWait() const { t } = useI18n() @@ -89,9 +94,6 @@ const changes = reactive({ conferencePin: '' }) -const subscriberPreferences = computed(() => store.state.callSettings.subscriberPreferences) -const hasSubscriberProfileAttribute = computed(() => store.getters['user/hasSubscriberProfileAttribute']) - const rules = computed(() => ({ conferenceMaxParticipants: { numeric: helpers.withMessage( @@ -138,7 +140,7 @@ async function saveConferenceMaxParticipants () { if (hasConferenceMaxParticipantsChanged.value && v$.value.conferenceMaxParticipants.$errors.length <= 0) { wait.start(WAITERS.updateConferenceMaxParticipants) try { - await store.dispatch('callSettings/fieldUpdateAction', { + await fieldUpdateAction({ field: 'conference_max_participants', value: changes.conferenceMaxParticipants }) @@ -156,7 +158,7 @@ async function saveConferencePin () { if (hasConferencePinChanged.value && v$.value.conferencePin.$errors.length <= 0) { wait.start(WAITERS.updateConferencePin) try { - await store.dispatch('callSettings/fieldUpdateAction', { + await fieldUpdateAction({ field: 'conference_pin', value: changes.conferencePin }) @@ -173,7 +175,7 @@ async function saveConferencePin () { onMounted(async () => { wait.start(WAITERS.loadPreferences) try { - await store.dispatch('callSettings/loadSubscriberPreferencesAction') + await loadSubscriberPreferencesAction() applyDefaultData() isInitialized.value = true } finally { diff --git a/src/store/call-forwarding/actions.js b/src/store/call-forwarding/actions.js index f247f1ac..ab06800e 100644 --- a/src/store/call-forwarding/actions.js +++ b/src/store/call-forwarding/actions.js @@ -901,3 +901,7 @@ export async function updateAnnouncement ({ dispatch, commit, state }, payload) export function resetCallForwardingState ({ commit }) { commit('resetState') } + +export function setPopupShow ({ commit }, popupId) { + commit('popupShow', popupId) +}