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
-
-
-
-
{{ myData }}
+
Loading...
+
{{ error }}
+
+
+
```
+
+### 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)
+}