feat: Add Pinia support and relocate store factory (#12854)

Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
Sivin Varghese
2025-11-28 16:31:59 +05:30
committed by GitHub
parent 1ef945de7b
commit f23d95e004
24 changed files with 1341 additions and 603 deletions

View File

@@ -1,11 +1,10 @@
<script setup>
import { ref, computed, onMounted, reactive } from 'vue';
import { useStore } from 'vuex';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useMapGetter } from 'dashboard/composables/store';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { debounce } from '@chatwoot/utils';
import { useCompaniesStore } from 'dashboard/stores/companies';
import CompaniesListLayout from 'dashboard/components-next/Companies/CompaniesListLayout.vue';
import CompaniesCard from 'dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue';
@@ -13,13 +12,18 @@ import CompaniesCard from 'dashboard/components-next/Companies/CompaniesCard/Com
const DEFAULT_SORT_FIELD = 'created_at';
const DEBOUNCE_DELAY = 300;
const store = useStore();
const companiesStore = useCompaniesStore();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { updateUISettings, uiSettings } = useUISettings();
const companies = computed(() => companiesStore.getCompaniesList);
const meta = computed(() => companiesStore.getMeta);
const uiFlags = computed(() => companiesStore.getUIFlags);
const searchQuery = computed(() => route.query?.search || '');
const searchValue = ref(searchQuery.value);
const pageNumber = computed(() => Number(route.query?.page) || 1);
@@ -46,10 +50,6 @@ const sortState = reactive({
const activeSort = computed(() => sortState.activeSort);
const activeOrdering = computed(() => sortState.activeOrdering);
const companies = useMapGetter('companies/getCompaniesList');
const meta = useMapGetter('companies/getMeta');
const uiFlags = useMapGetter('companies/getUIFlags');
const isFetchingList = computed(() => uiFlags.value.fetchingList);
const buildSortAttr = () =>
@@ -89,13 +89,13 @@ const fetchCompanies = async (page, search, sort) => {
}
if (currentSearch) {
await store.dispatch('companies/search', {
await companiesStore.search({
search: currentSearch,
page: currentPage,
sort: currentSort,
});
} else {
await store.dispatch('companies/get', {
await companiesStore.get({
page: currentPage,
sort: currentSort,
});

View File

@@ -1,5 +1,5 @@
import CaptainAssistantAPI from 'dashboard/api/captain/assistant';
import { createStore } from './storeFactory';
import { createStore } from '../storeFactory';
export default createStore({
name: 'CaptainAssistant',

View File

@@ -1,5 +1,5 @@
import CaptainBulkActionsAPI from 'dashboard/api/captain/bulkActions';
import { createStore } from './storeFactory';
import { createStore } from '../storeFactory';
import { throwErrorMessage } from 'dashboard/store/utils/api';
export default createStore({

View File

@@ -1,5 +1,5 @@
import CopilotMessagesAPI from 'dashboard/api/captain/copilotMessages';
import { createStore } from './storeFactory';
import { createStore } from '../storeFactory';
export default createStore({
name: 'CopilotMessages',

View File

@@ -1,5 +1,5 @@
import CopilotThreadsAPI from 'dashboard/api/captain/copilotThreads';
import { createStore } from './storeFactory';
import { createStore } from '../storeFactory';
export default createStore({
name: 'CopilotThreads',

View File

@@ -1,5 +1,5 @@
import CaptainCustomTools from 'dashboard/api/captain/customTools';
import { createStore } from './storeFactory';
import { createStore } from '../storeFactory';
import { throwErrorMessage } from 'dashboard/store/utils/api';
export default createStore({

View File

@@ -1,5 +1,5 @@
import CaptainDocumentAPI from 'dashboard/api/captain/document';
import { createStore } from './storeFactory';
import { createStore } from '../storeFactory';
export default createStore({
name: 'CaptainDocument',

View File

@@ -1,5 +1,5 @@
import CaptainInboxes from 'dashboard/api/captain/inboxes';
import { createStore } from './storeFactory';
import { createStore } from '../storeFactory';
import { throwErrorMessage } from 'dashboard/store/utils/api';
export default createStore({

View File

@@ -1,5 +1,5 @@
import CaptainResponseAPI from 'dashboard/api/captain/response';
import { createStore } from './storeFactory';
import { createStore } from '../storeFactory';
const SET_PENDING_COUNT = 'SET_PENDING_COUNT';

View File

@@ -1,5 +1,5 @@
import CaptainScenarios from 'dashboard/api/captain/scenarios';
import { createStore } from './storeFactory';
import { createStore } from '../storeFactory';
import { throwErrorMessage } from 'dashboard/store/utils/api';
export default createStore({

View File

@@ -1,94 +0,0 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import {
createRecord,
deleteRecord,
getRecords,
showRecord,
updateRecord,
} from './storeFactoryHelper';
export const generateMutationTypes = name => {
const capitalizedName = name.toUpperCase();
return {
SET_UI_FLAG: `SET_${capitalizedName}_UI_FLAG`,
SET: `SET_${capitalizedName}`,
ADD: `ADD_${capitalizedName}`,
EDIT: `EDIT_${capitalizedName}`,
DELETE: `DELETE_${capitalizedName}`,
SET_META: `SET_${capitalizedName}_META`,
UPSERT: `UPSERT_${capitalizedName}`,
};
};
export const createInitialState = () => ({
records: [],
meta: {},
uiFlags: {
fetchingList: false,
fetchingItem: false,
creatingItem: false,
updatingItem: false,
deletingItem: false,
},
});
export const createGetters = () => ({
getRecords: state => state.records.sort((r1, r2) => r2.id - r1.id),
getRecord: state => id =>
state.records.find(record => record.id === Number(id)) || {},
getUIFlags: state => state.uiFlags,
getMeta: state => state.meta,
});
export const createMutations = mutationTypes => ({
[mutationTypes.SET_UI_FLAG](state, data) {
state.uiFlags = {
...state.uiFlags,
...data,
};
},
[mutationTypes.SET_META](state, meta) {
state.meta = {
...state.meta,
totalCount: Number(meta.total_count),
page: Number(meta.page),
};
},
[mutationTypes.SET]: MutationHelpers.set,
[mutationTypes.ADD]: MutationHelpers.create,
[mutationTypes.EDIT]: MutationHelpers.update,
[mutationTypes.DELETE]: MutationHelpers.destroy,
[mutationTypes.UPSERT]: MutationHelpers.setSingleRecord,
});
export const createCrudActions = (API, mutationTypes) => ({
get: getRecords(mutationTypes, API),
show: showRecord(mutationTypes, API),
create: createRecord(mutationTypes, API),
update: updateRecord(mutationTypes, API),
delete: deleteRecord(mutationTypes, API),
});
export const createStore = options => {
const { name, API, actions, getters, mutations } = options;
const mutationTypes = generateMutationTypes(name);
const customActions = actions ? actions(mutationTypes) : {};
return {
namespaced: true,
state: createInitialState(),
getters: {
...createGetters(),
...(getters || {}),
},
mutations: {
...createMutations(mutationTypes),
...(mutations || {}),
},
actions: {
...createCrudActions(API, mutationTypes),
...customActions,
},
};
};

View File

@@ -1,380 +0,0 @@
import { throwErrorMessage } from 'dashboard/store/utils/api';
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import {
generateMutationTypes,
createInitialState,
createGetters,
createMutations,
createCrudActions,
createStore,
} from './storeFactory';
vi.mock('dashboard/store/utils/api', () => ({
throwErrorMessage: vi.fn(),
}));
vi.mock('shared/helpers/vuex/mutationHelpers', () => ({
set: vi.fn(),
create: vi.fn(),
update: vi.fn(),
destroy: vi.fn(),
setSingleRecord: vi.fn(),
}));
describe('storeFactory', () => {
describe('generateMutationTypes', () => {
it('generates correct mutation types with capitalized name', () => {
const result = generateMutationTypes('test');
expect(result).toEqual({
SET_UI_FLAG: 'SET_TEST_UI_FLAG',
SET: 'SET_TEST',
ADD: 'ADD_TEST',
EDIT: 'EDIT_TEST',
DELETE: 'DELETE_TEST',
SET_META: 'SET_TEST_META',
UPSERT: 'UPSERT_TEST',
});
});
});
describe('createInitialState', () => {
it('returns the correct initial state structure', () => {
const result = createInitialState();
expect(result).toEqual({
records: [],
meta: {},
uiFlags: {
fetchingList: false,
fetchingItem: false,
creatingItem: false,
updatingItem: false,
deletingItem: false,
},
});
});
});
describe('createGetters', () => {
it('returns getters with correct implementations', () => {
const getters = createGetters();
const state = {
records: [{ id: 2 }, { id: 1 }, { id: 3 }],
uiFlags: { fetchingList: true },
meta: { totalCount: 10, page: 1 },
};
expect(getters.getRecords(state)).toEqual([
{ id: 3 },
{ id: 2 },
{ id: 1 },
]);
expect(getters.getRecord(state)(2)).toEqual({ id: 2 });
expect(getters.getRecord(state)(4)).toEqual({});
expect(getters.getUIFlags(state)).toEqual({
fetchingList: true,
});
expect(getters.getMeta(state)).toEqual({
totalCount: 10,
page: 1,
});
});
});
describe('createMutations', () => {
it('creates mutations with correct implementations', () => {
const mutationTypes = generateMutationTypes('test');
const mutations = createMutations(mutationTypes);
const state = { uiFlags: { fetchingList: false } };
mutations[mutationTypes.SET_UI_FLAG](state, { fetchingList: true });
expect(state.uiFlags).toEqual({ fetchingList: true });
const metaState = { meta: {} };
mutations[mutationTypes.SET_META](metaState, {
total_count: '10',
page: '2',
});
expect(metaState.meta).toEqual({ totalCount: 10, page: 2 });
expect(mutations[mutationTypes.SET]).toBe(MutationHelpers.set);
expect(mutations[mutationTypes.ADD]).toBe(MutationHelpers.create);
expect(mutations[mutationTypes.EDIT]).toBe(MutationHelpers.update);
expect(mutations[mutationTypes.DELETE]).toBe(MutationHelpers.destroy);
expect(mutations[mutationTypes.UPSERT]).toBe(
MutationHelpers.setSingleRecord
);
});
});
describe('createCrudActions', () => {
let API;
let commit;
let mutationTypes;
let actions;
beforeEach(() => {
API = {
get: vi.fn(),
show: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
commit = vi.fn();
mutationTypes = generateMutationTypes('test');
actions = createCrudActions(API, mutationTypes);
});
describe('get action', () => {
it('handles successful API response', async () => {
const payload = [{ id: 1 }];
const meta = { total_count: 10, page: 1 };
API.get.mockResolvedValue({ data: { payload, meta } });
const result = await actions.get({ commit });
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingList: true,
});
expect(commit).toHaveBeenCalledWith(mutationTypes.SET, payload);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_META, meta);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingList: false,
});
expect(result).toEqual(payload);
});
it('handles API error', async () => {
const error = new Error('API Error');
API.get.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const result = await actions.get({ commit });
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingList: true,
});
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingList: false,
});
expect(result).toEqual('Error thrown');
});
});
describe('show action', () => {
it('handles successful API response', async () => {
const data = { id: 1, name: 'Test' };
API.show.mockResolvedValue({ data });
const result = await actions.show({ commit }, 1);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingItem: true,
});
expect(commit).toHaveBeenCalledWith(mutationTypes.UPSERT, data);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingItem: false,
});
expect(result).toEqual(data);
});
it('handles API error', async () => {
const error = new Error('API Error');
API.show.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const result = await actions.show({ commit }, 1);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingItem: true,
});
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingItem: false,
});
expect(result).toEqual('Error thrown');
});
});
describe('create action', () => {
it('handles successful API response', async () => {
const data = { id: 1, name: 'Test' };
API.create.mockResolvedValue({ data });
const result = await actions.create({ commit }, { name: 'Test' });
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
creatingItem: true,
});
expect(commit).toHaveBeenCalledWith(mutationTypes.UPSERT, data);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
creatingItem: false,
});
expect(result).toEqual(data);
});
it('handles API error', async () => {
const error = new Error('API Error');
API.create.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const result = await actions.create({ commit }, { name: 'Test' });
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
creatingItem: true,
});
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
creatingItem: false,
});
expect(result).toEqual('Error thrown');
});
});
describe('update action', () => {
it('handles successful API response', async () => {
const data = { id: 1, name: 'Updated' };
API.update.mockResolvedValue({ data });
const result = await actions.update(
{ commit },
{ id: 1, name: 'Updated' }
);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
updatingItem: true,
});
expect(API.update).toHaveBeenCalledWith(1, { name: 'Updated' });
expect(commit).toHaveBeenCalledWith(mutationTypes.EDIT, data);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
updatingItem: false,
});
expect(result).toEqual(data);
});
it('handles API error', async () => {
const error = new Error('API Error');
API.update.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const result = await actions.update(
{ commit },
{ id: 1, name: 'Updated' }
);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
updatingItem: true,
});
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
updatingItem: false,
});
expect(result).toEqual('Error thrown');
});
});
describe('delete action', () => {
it('handles successful API response', async () => {
API.delete.mockResolvedValue({});
const result = await actions.delete({ commit }, 1);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
deletingItem: true,
});
expect(API.delete).toHaveBeenCalledWith(1);
expect(commit).toHaveBeenCalledWith(mutationTypes.DELETE, 1);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
deletingItem: false,
});
expect(result).toEqual(1);
});
it('handles API error', async () => {
const error = new Error('API Error');
API.delete.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const result = await actions.delete({ commit }, 1);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
deletingItem: true,
});
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
deletingItem: false,
});
expect(result).toEqual('Error thrown');
});
});
});
describe('createStore', () => {
it('creates a complete store with default options', () => {
const API = {};
const store = createStore({ name: 'test', API });
expect(store.namespaced).toBe(true);
expect(store.state).toEqual(createInitialState());
expect(Object.keys(store.getters)).toEqual([
'getRecords',
'getRecord',
'getUIFlags',
'getMeta',
]);
expect(Object.keys(store.mutations)).toEqual([
'SET_TEST_UI_FLAG',
'SET_TEST_META',
'SET_TEST',
'ADD_TEST',
'EDIT_TEST',
'DELETE_TEST',
'UPSERT_TEST',
]);
expect(Object.keys(store.actions)).toEqual([
'get',
'show',
'create',
'update',
'delete',
]);
});
it('creates a store with custom actions and getters', () => {
const API = {};
const customGetters = { customGetter: () => 'custom' };
const customActions = () => ({
customAction: () => 'custom',
});
const store = createStore({
name: 'test',
API,
getters: customGetters,
actions: customActions,
});
expect(store.getters).toHaveProperty('customGetter');
expect(store.actions).toHaveProperty('customAction');
expect(Object.keys(store.getters)).toEqual([
'getRecords',
'getRecord',
'getUIFlags',
'getMeta',
'customGetter',
]);
expect(Object.keys(store.actions)).toEqual([
'get',
'show',
'create',
'update',
'delete',
'customAction',
]);
});
});
});

View File

@@ -1,77 +0,0 @@
import { throwErrorMessage } from 'dashboard/store/utils/api';
export const getRecords =
(mutationTypes, API) =>
async ({ commit }, params = {}) => {
commit(mutationTypes.SET_UI_FLAG, { fetchingList: true });
try {
const response = await API.get(params);
commit(mutationTypes.SET, response.data.payload);
commit(mutationTypes.SET_META, response.data.meta);
return response.data.payload;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(mutationTypes.SET_UI_FLAG, { fetchingList: false });
}
};
export const showRecord =
(mutationTypes, API) =>
async ({ commit }, id) => {
commit(mutationTypes.SET_UI_FLAG, { fetchingItem: true });
try {
const response = await API.show(id);
commit(mutationTypes.UPSERT, response.data);
return response.data;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(mutationTypes.SET_UI_FLAG, { fetchingItem: false });
}
};
export const createRecord =
(mutationTypes, API) =>
async ({ commit }, dataObj) => {
commit(mutationTypes.SET_UI_FLAG, { creatingItem: true });
try {
const response = await API.create(dataObj);
commit(mutationTypes.UPSERT, response.data);
return response.data;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(mutationTypes.SET_UI_FLAG, { creatingItem: false });
}
};
export const updateRecord =
(mutationTypes, API) =>
async ({ commit }, { id, ...updateObj }) => {
commit(mutationTypes.SET_UI_FLAG, { updatingItem: true });
try {
const response = await API.update(id, updateObj);
commit(mutationTypes.EDIT, response.data);
return response.data;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(mutationTypes.SET_UI_FLAG, { updatingItem: false });
}
};
export const deleteRecord =
(mutationTypes, API) =>
async ({ commit }, id) => {
commit(mutationTypes.SET_UI_FLAG, { deletingItem: true });
try {
await API.delete(id);
commit(mutationTypes.DELETE, id);
return id;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(mutationTypes.SET_UI_FLAG, { deletingItem: false });
}
};

View File

@@ -1,4 +1,4 @@
import { createStore } from './storeFactory';
import { createStore } from '../storeFactory';
import CaptainToolsAPI from '../../api/captain/tools';
import { throwErrorMessage } from 'dashboard/store/utils/api';

View File

@@ -14,7 +14,6 @@ import bulkActions from './modules/bulkActions';
import campaigns from './modules/campaigns';
import cannedResponse from './modules/cannedResponse';
import categories from './modules/helpCenterCategories';
import companies from './modules/companies';
import contactConversations from './modules/contactConversations';
import contactLabels from './modules/contactLabels';
import contactNotes from './modules/contactNotes';
@@ -78,7 +77,6 @@ export default createStore({
campaigns,
cannedResponse,
categories,
companies,
contactConversations,
contactLabels,
contactNotes,

View File

@@ -1,29 +0,0 @@
import CompanyAPI from 'dashboard/api/companies';
import { createStore } from 'dashboard/store/captain/storeFactory';
import camelcaseKeys from 'camelcase-keys';
export default createStore({
name: 'Company',
API: CompanyAPI,
getters: {
getCompaniesList: state => {
return camelcaseKeys(state.records, { deep: true });
},
},
actions: mutationTypes => ({
search: async ({ commit }, { search, page, sort }) => {
commit(mutationTypes.SET_UI_FLAG, { fetchingList: true });
try {
const {
data: { payload, meta },
} = await CompanyAPI.search(search, page, sort);
commit(mutationTypes.SET, payload);
commit(mutationTypes.SET_META, meta);
} catch (error) {
// Error
} finally {
commit(mutationTypes.SET_UI_FLAG, { fetchingList: false });
}
},
}),
});

View File

@@ -0,0 +1,226 @@
/**
* Universal Store Factory
*
* This factory creates stores for both Vuex and Pinia, allowing gradual
* migration from Vuex to Pinia without breaking existing functionality.
*
* @module storeFactory
* @see https://pinia.vuejs.org/ - Pinia documentation
* @see https://vuex.vuejs.org/ - Vuex documentation
*/
import { defineStore } from 'pinia';
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import {
// Vuex helpers
createRecord,
deleteRecord,
getRecords,
showRecord,
updateRecord,
// Pinia helpers
piniaGetRecords,
piniaShowRecord,
piniaCreateRecord,
piniaUpdateRecord,
piniaDeleteRecord,
} from './storeFactoryHelper';
export const generateMutationTypes = name => {
const capitalizedName = name.toUpperCase();
return {
SET_UI_FLAG: `SET_${capitalizedName}_UI_FLAG`,
SET: `SET_${capitalizedName}`,
ADD: `ADD_${capitalizedName}`,
EDIT: `EDIT_${capitalizedName}`,
DELETE: `DELETE_${capitalizedName}`,
SET_META: `SET_${capitalizedName}_META`,
UPSERT: `UPSERT_${capitalizedName}`,
};
};
export const createInitialState = () => ({
records: [],
meta: {},
uiFlags: {
fetchingList: false,
fetchingItem: false,
creatingItem: false,
updatingItem: false,
deletingItem: false,
},
});
export const createGetters = () => ({
getRecords: state => state.records.sort((r1, r2) => r2.id - r1.id),
getRecord: state => id =>
state.records.find(record => record.id === Number(id)) || {},
getUIFlags: state => state.uiFlags,
getMeta: state => state.meta,
});
export const createMutations = mutationTypes => ({
[mutationTypes.SET_UI_FLAG](state, data) {
state.uiFlags = {
...state.uiFlags,
...data,
};
},
[mutationTypes.SET_META](state, meta) {
state.meta = {
...state.meta,
totalCount: Number(meta.total_count),
page: Number(meta.page),
};
},
[mutationTypes.SET]: MutationHelpers.set,
[mutationTypes.ADD]: MutationHelpers.create,
[mutationTypes.EDIT]: MutationHelpers.update,
[mutationTypes.DELETE]: MutationHelpers.destroy,
[mutationTypes.UPSERT]: MutationHelpers.setSingleRecord,
});
export const createCrudActions = (API, mutationTypes) => ({
get: getRecords(mutationTypes, API),
show: showRecord(mutationTypes, API),
create: createRecord(mutationTypes, API),
update: updateRecord(mutationTypes, API),
delete: deleteRecord(mutationTypes, API),
});
/**
* Create Vuex store with standard CRUD operations
*
* @param {Object} options - Store configuration
* @param {string} options.name - Store name
* @param {Object} options.API - API client
* @param {Object} [options.getters] - Custom getters
* @param {Function} [options.actions] - Custom actions function
* @param {Object} [options.mutations] - Custom mutations
* @returns {Object} Vuex module configuration
*/
export const createVuexStore = options => {
const { name, API, actions, getters, mutations } = options;
const mutationTypes = generateMutationTypes(name);
const customActions = actions ? actions(mutationTypes) : {};
return {
namespaced: true,
state: createInitialState(),
getters: {
...createGetters(),
...(getters || {}),
},
mutations: {
...createMutations(mutationTypes),
...(mutations || {}),
},
actions: {
...createCrudActions(API, mutationTypes),
...customActions,
},
};
};
/**
* Create Pinia store with standard CRUD operations
*
* @param {Object} options - Store configuration
* @param {string} options.name - Store name
* @param {Object} options.API - API client
* @param {Object} [options.getters] - Custom getters
* @param {Function} [options.actions] - Custom actions function
* @returns {Function} Pinia store composable
*/
export const createPiniaStore = options => {
const { name, API, actions, getters } = options;
return defineStore(name.toLowerCase(), {
state: createInitialState,
getters: {
...createGetters(),
...(getters || {}),
},
actions: {
setUIFlag(data) {
this.uiFlags = {
...this.uiFlags,
...data,
};
},
setMeta(meta) {
this.meta = {
...this.meta,
totalCount: Number(meta.total_count || meta.totalCount || 0),
page: Number(meta.page || 1),
};
},
async get(params) {
return piniaGetRecords(this, API, params);
},
async show(id) {
return piniaShowRecord(this, API, id);
},
async create(obj) {
return piniaCreateRecord(this, API, obj);
},
async update(payload) {
return piniaUpdateRecord(this, API, payload);
},
async delete(id) {
return piniaDeleteRecord(this, API, id);
},
...(actions ? actions() : {}),
},
});
};
/**
* Universal Store Factory - Main Entry Point
*
* Creates either a Vuex or Pinia store based on the 'type' parameter.
* Defaults to Vuex for backward compatibility.
*
* @param {Object} options - Store configuration
* @param {string} options.name - Store name
* @param {Object} options.API - API client for CRUD operations
* @param {string} [options.type='vuex'] - Store type: 'vuex' or 'pinia'
* @param {Object} [options.getters] - Custom getters
* @param {Function} [options.actions] - Custom actions function
* @param {Object} [options.mutations] - Custom mutations (Vuex only)
*
* @returns {Object|Function} Vuex module or Pinia store composable
*
* @example
* Create Vuex store (default)
* export default createStore({
* name: 'Company',
* API: CompanyAPI,
* });
*
* @example
* Create Pinia store
* export const useCompaniesStore = createStore({
* name: 'Company',
* type: 'pinia',
* API: CompanyAPI,
* });
*/
export const createStore = options => {
const { type = 'vuex' } = options;
if (type === 'pinia') {
return createPiniaStore(options);
}
return createVuexStore(options);
};

View File

@@ -0,0 +1,761 @@
import { setActivePinia, createPinia } from 'pinia';
import { throwErrorMessage } from 'dashboard/store/utils/api';
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import {
generateMutationTypes,
createInitialState,
createGetters,
createMutations,
createCrudActions,
createStore,
} from './storeFactory';
vi.mock('dashboard/store/utils/api', () => ({
throwErrorMessage: vi.fn(),
}));
vi.mock('shared/helpers/vuex/mutationHelpers', () => ({
set: vi.fn(),
create: vi.fn(),
update: vi.fn(),
destroy: vi.fn(),
setSingleRecord: vi.fn(),
}));
describe('storeFactory', () => {
describe('createStore with type parameter', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it('creates Vuex store by default', () => {
const API = {};
const store = createStore({ name: 'test', API });
expect(store.namespaced).toBe(true);
expect(store.state).toBeDefined();
expect(store.mutations).toBeDefined();
});
it('creates Pinia store when type is "pinia"', () => {
const API = {
get: vi.fn(),
show: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
const useTestStore = createStore({
name: 'test',
API,
type: 'pinia',
});
const store = useTestStore();
expect(store.records).toBeDefined();
expect(store.get).toBeTypeOf('function');
expect(store.setUIFlag).toBeTypeOf('function');
});
it('creates Vuex store when type is "vuex"', () => {
const API = {};
const store = createStore({
name: 'test',
API,
type: 'vuex',
});
expect(store.namespaced).toBe(true);
expect(store.state).toBeDefined();
});
});
describe('generateMutationTypes', () => {
it('generates correct mutation types with capitalized name', () => {
const result = generateMutationTypes('test');
expect(result).toEqual({
SET_UI_FLAG: 'SET_TEST_UI_FLAG',
SET: 'SET_TEST',
ADD: 'ADD_TEST',
EDIT: 'EDIT_TEST',
DELETE: 'DELETE_TEST',
SET_META: 'SET_TEST_META',
UPSERT: 'UPSERT_TEST',
});
});
});
describe('createInitialState', () => {
it('returns the correct initial state structure', () => {
const result = createInitialState();
expect(result).toEqual({
records: [],
meta: {},
uiFlags: {
fetchingList: false,
fetchingItem: false,
creatingItem: false,
updatingItem: false,
deletingItem: false,
},
});
});
});
describe('createGetters', () => {
it('returns getters with correct implementations', () => {
const getters = createGetters();
const state = {
records: [{ id: 2 }, { id: 1 }, { id: 3 }],
uiFlags: { fetchingList: true },
meta: { totalCount: 10, page: 1 },
};
expect(getters.getRecords(state)).toEqual([
{ id: 3 },
{ id: 2 },
{ id: 1 },
]);
expect(getters.getRecord(state)(2)).toEqual({ id: 2 });
expect(getters.getRecord(state)(4)).toEqual({});
expect(getters.getUIFlags(state)).toEqual({
fetchingList: true,
});
expect(getters.getMeta(state)).toEqual({
totalCount: 10,
page: 1,
});
});
});
describe('createMutations', () => {
it('creates mutations with correct implementations', () => {
const mutationTypes = generateMutationTypes('test');
const mutations = createMutations(mutationTypes);
const state = { uiFlags: { fetchingList: false } };
mutations[mutationTypes.SET_UI_FLAG](state, { fetchingList: true });
expect(state.uiFlags).toEqual({ fetchingList: true });
const metaState = { meta: {} };
mutations[mutationTypes.SET_META](metaState, {
total_count: '10',
page: '2',
});
expect(metaState.meta).toEqual({ totalCount: 10, page: 2 });
expect(mutations[mutationTypes.SET]).toBe(MutationHelpers.set);
expect(mutations[mutationTypes.ADD]).toBe(MutationHelpers.create);
expect(mutations[mutationTypes.EDIT]).toBe(MutationHelpers.update);
expect(mutations[mutationTypes.DELETE]).toBe(MutationHelpers.destroy);
expect(mutations[mutationTypes.UPSERT]).toBe(
MutationHelpers.setSingleRecord
);
});
});
describe('createCrudActions', () => {
let API;
let commit;
let mutationTypes;
let actions;
beforeEach(() => {
API = {
get: vi.fn(),
show: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
commit = vi.fn();
mutationTypes = generateMutationTypes('test');
actions = createCrudActions(API, mutationTypes);
});
describe('get action', () => {
it('handles successful API response', async () => {
const payload = [{ id: 1 }];
const meta = { total_count: 10, page: 1 };
API.get.mockResolvedValue({ data: { payload, meta } });
const result = await actions.get({ commit });
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingList: true,
});
expect(commit).toHaveBeenCalledWith(mutationTypes.SET, payload);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_META, meta);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingList: false,
});
expect(result).toEqual(payload);
});
it('handles API error', async () => {
const error = new Error('API Error');
API.get.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const result = await actions.get({ commit });
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingList: true,
});
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingList: false,
});
expect(result).toEqual('Error thrown');
});
});
describe('show action', () => {
it('handles successful API response', async () => {
const data = { id: 1, name: 'Test' };
API.show.mockResolvedValue({ data });
const result = await actions.show({ commit }, 1);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingItem: true,
});
expect(commit).toHaveBeenCalledWith(mutationTypes.UPSERT, data);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingItem: false,
});
expect(result).toEqual(data);
});
it('handles API error', async () => {
const error = new Error('API Error');
API.show.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const result = await actions.show({ commit }, 1);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingItem: true,
});
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
fetchingItem: false,
});
expect(result).toEqual('Error thrown');
});
});
describe('create action', () => {
it('handles successful API response', async () => {
const data = { id: 1, name: 'Test' };
API.create.mockResolvedValue({ data });
const result = await actions.create({ commit }, { name: 'Test' });
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
creatingItem: true,
});
expect(commit).toHaveBeenCalledWith(mutationTypes.UPSERT, data);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
creatingItem: false,
});
expect(result).toEqual(data);
});
it('handles API error', async () => {
const error = new Error('API Error');
API.create.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const result = await actions.create({ commit }, { name: 'Test' });
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
creatingItem: true,
});
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
creatingItem: false,
});
expect(result).toEqual('Error thrown');
});
});
describe('update action', () => {
it('handles successful API response', async () => {
const data = { id: 1, name: 'Updated' };
API.update.mockResolvedValue({ data });
const result = await actions.update(
{ commit },
{ id: 1, name: 'Updated' }
);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
updatingItem: true,
});
expect(API.update).toHaveBeenCalledWith(1, { name: 'Updated' });
expect(commit).toHaveBeenCalledWith(mutationTypes.EDIT, data);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
updatingItem: false,
});
expect(result).toEqual(data);
});
it('handles API error', async () => {
const error = new Error('API Error');
API.update.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const result = await actions.update(
{ commit },
{ id: 1, name: 'Updated' }
);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
updatingItem: true,
});
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
updatingItem: false,
});
expect(result).toEqual('Error thrown');
});
});
describe('delete action', () => {
it('handles successful API response', async () => {
API.delete.mockResolvedValue({});
const result = await actions.delete({ commit }, 1);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
deletingItem: true,
});
expect(API.delete).toHaveBeenCalledWith(1);
expect(commit).toHaveBeenCalledWith(mutationTypes.DELETE, 1);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
deletingItem: false,
});
expect(result).toEqual(1);
});
it('handles API error', async () => {
const error = new Error('API Error');
API.delete.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const result = await actions.delete({ commit }, 1);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
deletingItem: true,
});
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
deletingItem: false,
});
expect(result).toEqual('Error thrown');
});
});
});
describe('createStore - Vuex type', () => {
it('creates a complete Vuex store with default options', () => {
const API = {};
const store = createStore({ name: 'test', API, type: 'vuex' });
expect(store.namespaced).toBe(true);
expect(store.state).toEqual(createInitialState());
expect(Object.keys(store.getters)).toEqual([
'getRecords',
'getRecord',
'getUIFlags',
'getMeta',
]);
expect(Object.keys(store.mutations)).toEqual([
'SET_TEST_UI_FLAG',
'SET_TEST_META',
'SET_TEST',
'ADD_TEST',
'EDIT_TEST',
'DELETE_TEST',
'UPSERT_TEST',
]);
expect(Object.keys(store.actions)).toEqual([
'get',
'show',
'create',
'update',
'delete',
]);
});
it('creates a Vuex store with custom actions and getters', () => {
const API = {};
const customGetters = { customGetter: () => 'custom' };
const customActions = () => ({
customAction: () => 'custom',
});
const store = createStore({
name: 'test',
API,
type: 'vuex',
getters: customGetters,
actions: customActions,
});
expect(store.getters).toHaveProperty('customGetter');
expect(store.actions).toHaveProperty('customAction');
expect(Object.keys(store.getters)).toEqual([
'getRecords',
'getRecord',
'getUIFlags',
'getMeta',
'customGetter',
]);
expect(Object.keys(store.actions)).toEqual([
'get',
'show',
'create',
'update',
'delete',
'customAction',
]);
});
it('creates a Vuex store with custom mutations', () => {
const API = {};
const customMutations = {
CUSTOM_MUTATION: state => {
state.custom = true;
},
};
const store = createStore({
name: 'test',
API,
type: 'vuex',
mutations: customMutations,
});
expect(store.mutations).toHaveProperty('CUSTOM_MUTATION');
});
});
describe('createStore - Pinia type', () => {
let API;
beforeEach(() => {
setActivePinia(createPinia());
API = {
get: vi.fn(),
show: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
});
it('creates a Pinia store with correct structure', () => {
const useTestStore = createStore({
name: 'test',
API,
type: 'pinia',
});
const store = useTestStore();
expect(store.records).toEqual([]);
expect(store.meta).toEqual({});
expect(store.uiFlags).toEqual({
fetchingList: false,
fetchingItem: false,
creatingItem: false,
updatingItem: false,
deletingItem: false,
});
});
it('has standard getters', () => {
const useTestStore = createStore({
name: 'test',
API,
type: 'pinia',
});
const store = useTestStore();
store.records = [{ id: 2 }, { id: 1 }, { id: 3 }];
store.meta = { totalCount: 10, page: 1 };
store.uiFlags = { fetchingList: true };
expect(store.getRecords).toEqual([{ id: 3 }, { id: 2 }, { id: 1 }]);
expect(store.getRecord(2)).toEqual({ id: 2 });
expect(store.getRecord(4)).toEqual({});
expect(store.getUIFlags).toEqual({ fetchingList: true });
expect(store.getMeta).toEqual({ totalCount: 10, page: 1 });
});
it('has custom getters', () => {
const useTestStore = createStore({
name: 'test',
API,
type: 'pinia',
getters: {
customGetter: state => state.records.length,
},
});
const store = useTestStore();
store.records = [{ id: 1 }, { id: 2 }];
expect(store.customGetter).toBe(2);
});
describe('setUIFlag action', () => {
it('updates UI flags correctly', () => {
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
store.setUIFlag({ fetchingList: true });
expect(store.uiFlags.fetchingList).toBe(true);
store.setUIFlag({ creatingItem: true });
expect(store.uiFlags.fetchingList).toBe(true);
expect(store.uiFlags.creatingItem).toBe(true);
});
});
describe('setMeta action', () => {
it('updates meta with snake_case input', () => {
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
store.setMeta({ total_count: '10', page: '2' });
expect(store.meta.totalCount).toBe(10);
expect(store.meta.page).toBe(2);
});
it('updates meta with camelCase input', () => {
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
store.setMeta({ totalCount: 15, page: 3 });
expect(store.meta.totalCount).toBe(15);
expect(store.meta.page).toBe(3);
});
});
describe('get action', () => {
it('fetches records successfully', async () => {
const payload = [{ id: 1, name: 'Test' }];
const meta = { total_count: 1, page: 1 };
API.get.mockResolvedValue({ data: { payload, meta } });
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
await store.get({ page: 1 });
expect(API.get).toHaveBeenCalledWith({ page: 1 });
expect(store.records).toEqual(payload);
expect(store.meta.totalCount).toBe(1);
expect(store.meta.page).toBe(1);
expect(store.uiFlags.fetchingList).toBe(false);
});
it('handles API response without meta', async () => {
const data = [{ id: 1, name: 'Test' }];
API.get.mockResolvedValue({ data });
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
await store.get();
expect(store.records).toEqual(data);
});
it('handles errors and resets UI flags', async () => {
const error = new Error('API Error');
API.get.mockRejectedValue(error);
throwErrorMessage.mockReturnValue('Error thrown');
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
const result = await store.get();
expect(throwErrorMessage).toHaveBeenCalledWith(error);
expect(store.uiFlags.fetchingList).toBe(false);
expect(result).toBe('Error thrown');
});
});
describe('show action', () => {
it('fetches and upserts a record', async () => {
const data = { id: 1, name: 'Test' };
API.show.mockResolvedValue({ data });
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
const result = await store.show(1);
expect(API.show).toHaveBeenCalledWith(1);
expect(store.records).toContainEqual(data);
expect(result).toEqual(data);
expect(store.uiFlags.fetchingItem).toBe(false);
});
it('updates existing record', async () => {
const data = { id: 1, name: 'Updated' };
API.show.mockResolvedValue({ data });
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
store.records = [{ id: 1, name: 'Original' }];
await store.show(1);
expect(store.records).toHaveLength(1);
expect(store.records[0].name).toBe('Updated');
});
it('handles payload wrapper', async () => {
const record = { id: 1, name: 'Test' };
API.show.mockResolvedValue({ data: { payload: record } });
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
const result = await store.show(1);
expect(result).toEqual(record);
expect(store.records).toContainEqual(record);
});
});
describe('create action', () => {
it('creates a new record', async () => {
const data = { id: 1, name: 'New' };
API.create.mockResolvedValue({ data });
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
const result = await store.create({ name: 'New' });
expect(API.create).toHaveBeenCalledWith({ name: 'New' });
expect(store.records).toContainEqual(data);
expect(result).toEqual(data);
expect(store.uiFlags.creatingItem).toBe(false);
});
it('handles payload wrapper', async () => {
const record = { id: 1, name: 'New' };
API.create.mockResolvedValue({ data: { payload: record } });
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
const result = await store.create({ name: 'New' });
expect(result).toEqual(record);
expect(store.records).toContainEqual(record);
});
});
describe('update action', () => {
it('updates an existing record', async () => {
const data = { id: 1, name: 'Updated' };
API.update.mockResolvedValue({ data });
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
store.records = [{ id: 1, name: 'Original' }];
const result = await store.update({ id: 1, name: 'Updated' });
expect(API.update).toHaveBeenCalledWith(1, { name: 'Updated' });
expect(store.records[0].name).toBe('Updated');
expect(result).toEqual(data);
expect(store.uiFlags.updatingItem).toBe(false);
});
it('handles payload wrapper', async () => {
const record = { id: 1, name: 'Updated' };
API.update.mockResolvedValue({ data: { payload: record } });
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
store.records = [{ id: 1, name: 'Original' }];
const result = await store.update({ id: 1, name: 'Updated' });
expect(result).toEqual(record);
expect(store.records[0]).toEqual(record);
});
});
describe('delete action', () => {
it('deletes a record', async () => {
API.delete.mockResolvedValue({});
const useTestStore = createStore({ name: 'test', API, type: 'pinia' });
const store = useTestStore();
store.records = [{ id: 1 }, { id: 2 }, { id: 3 }];
const result = await store.delete(2);
expect(API.delete).toHaveBeenCalledWith(2);
expect(store.records).toHaveLength(2);
expect(store.records).not.toContainEqual({ id: 2 });
expect(result).toBe(2);
expect(store.uiFlags.deletingItem).toBe(false);
});
});
describe('custom actions', () => {
it('includes custom actions', async () => {
const useTestStore = createStore({
name: 'test',
API,
type: 'pinia',
actions: () => ({
customAction() {
return 'custom result';
},
}),
});
const store = useTestStore();
const result = store.customAction();
expect(result).toBe('custom result');
});
it('custom actions can access store state', () => {
const useTestStore = createStore({
name: 'test',
API,
type: 'pinia',
actions: () => ({
getRecordCount() {
return this.records.length;
},
}),
});
const store = useTestStore();
store.records = [{ id: 1 }, { id: 2 }];
expect(store.getRecordCount()).toBe(2);
});
});
});
});

View File

@@ -0,0 +1,203 @@
import { throwErrorMessage } from 'dashboard/store/utils/api';
// ============================================================================
// VUEX HELPERS
// ============================================================================
export const getRecords =
(mutationTypes, API) =>
async ({ commit }, params = {}) => {
commit(mutationTypes.SET_UI_FLAG, { fetchingList: true });
try {
const response = await API.get(params);
commit(mutationTypes.SET, response.data.payload);
commit(mutationTypes.SET_META, response.data.meta);
return response.data.payload;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(mutationTypes.SET_UI_FLAG, { fetchingList: false });
}
};
export const showRecord =
(mutationTypes, API) =>
async ({ commit }, id) => {
commit(mutationTypes.SET_UI_FLAG, { fetchingItem: true });
try {
const response = await API.show(id);
commit(mutationTypes.UPSERT, response.data);
return response.data;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(mutationTypes.SET_UI_FLAG, { fetchingItem: false });
}
};
export const createRecord =
(mutationTypes, API) =>
async ({ commit }, dataObj) => {
commit(mutationTypes.SET_UI_FLAG, { creatingItem: true });
try {
const response = await API.create(dataObj);
commit(mutationTypes.UPSERT, response.data);
return response.data;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(mutationTypes.SET_UI_FLAG, { creatingItem: false });
}
};
export const updateRecord =
(mutationTypes, API) =>
async ({ commit }, { id, ...updateObj }) => {
commit(mutationTypes.SET_UI_FLAG, { updatingItem: true });
try {
const response = await API.update(id, updateObj);
commit(mutationTypes.EDIT, response.data);
return response.data;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(mutationTypes.SET_UI_FLAG, { updatingItem: false });
}
};
export const deleteRecord =
(mutationTypes, API) =>
async ({ commit }, id) => {
commit(mutationTypes.SET_UI_FLAG, { deletingItem: true });
try {
await API.delete(id);
commit(mutationTypes.DELETE, id);
return id;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(mutationTypes.SET_UI_FLAG, { deletingItem: false });
}
};
// ============================================================================
// PINIA HELPERS
// ============================================================================
/**
* Get records from API and update Pinia store
* @param {Object} store - Pinia store instance (this context)
* @param {Object} API - API client
* @param {Object} params - Query parameters
*/
export const piniaGetRecords = async (store, API, params = {}) => {
store.setUIFlag({ fetchingList: true });
try {
const response = await API.get(params);
const { data } = response;
store.records = data.payload || data;
if (data.meta) {
store.setMeta(data.meta);
}
return data.payload || data;
} catch (error) {
return throwErrorMessage(error);
} finally {
store.setUIFlag({ fetchingList: false });
}
};
/**
* Show single record from API and upsert to Pinia store
* @param {Object} store - Pinia store instance (this context)
* @param {Object} API - API client
* @param {Number|String} id - Record ID
*/
export const piniaShowRecord = async (store, API, id) => {
store.setUIFlag({ fetchingItem: true });
try {
const response = await API.show(id);
const { data } = response;
const record = data.payload || data;
// Upsert logic
const index = store.records.findIndex(r => r.id === record.id);
if (index !== -1) {
store.records[index] = record;
} else {
store.records.push(record);
}
return record;
} catch (error) {
return throwErrorMessage(error);
} finally {
store.setUIFlag({ fetchingItem: false });
}
};
/**
* Create new record via API and add to Pinia store
* @param {Object} store - Pinia store instance (this context)
* @param {Object} API - API client
* @param {Object} dataObj - Data to create
*/
export const piniaCreateRecord = async (store, API, dataObj) => {
store.setUIFlag({ creatingItem: true });
try {
const response = await API.create(dataObj);
const { data } = response;
const record = data.payload || data;
store.records.push(record);
return record;
} catch (error) {
return throwErrorMessage(error);
} finally {
store.setUIFlag({ creatingItem: false });
}
};
/**
* Update existing record via API and update in Pinia store
* @param {Object} store - Pinia store instance (this context)
* @param {Object} API - API client
* @param {Object} payload - Update payload with id
*/
export const piniaUpdateRecord = async (store, API, { id, ...updateObj }) => {
store.setUIFlag({ updatingItem: true });
try {
const response = await API.update(id, updateObj);
const { data } = response;
const record = data.payload || data;
const index = store.records.findIndex(r => r.id === record.id);
if (index !== -1) {
store.records[index] = record;
}
return record;
} catch (error) {
return throwErrorMessage(error);
} finally {
store.setUIFlag({ updatingItem: false });
}
};
/**
* Delete record via API and remove from Pinia store
* @param {Object} store - Pinia store instance (this context)
* @param {Object} API - API client
* @param {Number|String} id - Record ID to delete
*/
export const piniaDeleteRecord = async (store, API, id) => {
store.setUIFlag({ deletingItem: true });
try {
await API.delete(id);
store.records = store.records.filter(record => record.id !== id);
return id;
} catch (error) {
return throwErrorMessage(error);
} finally {
store.setUIFlag({ deletingItem: false });
}
};

View File

@@ -0,0 +1,31 @@
import CompanyAPI from 'dashboard/api/companies';
import { createStore } from 'dashboard/store/storeFactory';
import { throwErrorMessage } from 'dashboard/store/utils/api';
import camelcaseKeys from 'camelcase-keys';
export const useCompaniesStore = createStore({
name: 'companies',
type: 'pinia',
API: CompanyAPI,
getters: {
getCompaniesList: state => {
return camelcaseKeys(state.records, { deep: true });
},
},
actions: () => ({
async search({ search, page, sort }) {
this.setUIFlag({ fetchingList: true });
try {
const {
data: { payload, meta },
} = await CompanyAPI.search(search, page, sort);
this.records = payload;
this.setMeta(meta);
} catch (error) {
throwErrorMessage(error);
} finally {
this.setUIFlag({ fetchingList: false });
}
},
}),
});

View File

@@ -16,6 +16,7 @@ import createAxios from 'dashboard/helper/APIHelper';
import commonHelpers, { isJSONValid } from 'dashboard/helper/commons';
import { sync } from 'vuex-router-sync';
import { createPinia } from 'pinia';
import router, { initalizeRouter } from 'dashboard/routes';
import store from 'dashboard/store';
import constants from 'dashboard/constants/globals';
@@ -41,9 +42,12 @@ const i18n = createI18n({
sync(store, router);
const pinia = createPinia();
const app = createApp(App);
app.use(i18n);
app.use(store);
app.use(pinia);
app.use(router);
// [VITE] Disabled this, need to renable later