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:
@@ -39,7 +39,7 @@ exclude_patterns = [
|
|||||||
"**/target/**",
|
"**/target/**",
|
||||||
"**/templates/**",
|
"**/templates/**",
|
||||||
"**/testdata/**",
|
"**/testdata/**",
|
||||||
"**/vendor/**", "spec/", "**/specs/**/**", "**/spec/**/**", "db/*", "bin/**/*", "db/**/*", "config/**/*", "public/**/*", "vendor/**/*", "node_modules/**/*", "lib/tasks/auto_annotate_models.rake", "app/test-matchers.js", "docs/*", "**/*.md", "**/*.yml", "app/javascript/dashboard/i18n/locale", "**/*.stories.js", "stories/", "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js", "app/javascript/shared/constants/countries.js", "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js", "app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js", "app/javascript/dashboard/routes/dashboard/settings/automation/constants.js", "app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js", "app/javascript/dashboard/routes/dashboard/settings/reports/constants.js", "app/javascript/dashboard/store/captain/storeFactory.js", "app/javascript/dashboard/i18n/index.js", "app/javascript/widget/i18n/index.js", "app/javascript/survey/i18n/index.js", "app/javascript/shared/constants/locales.js", "app/javascript/dashboard/helper/specs/macrosFixtures.js", "app/javascript/dashboard/routes/dashboard/settings/macros/constants.js", "**/fixtures/**", "**/*/fixtures.js",
|
"**/vendor/**", "spec/", "**/specs/**/**", "**/spec/**/**", "db/*", "bin/**/*", "db/**/*", "config/**/*", "public/**/*", "vendor/**/*", "node_modules/**/*", "lib/tasks/auto_annotate_models.rake", "app/test-matchers.js", "docs/*", "**/*.md", "**/*.yml", "app/javascript/dashboard/i18n/locale", "**/*.stories.js", "stories/", "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js", "app/javascript/shared/constants/countries.js", "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js", "app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js", "app/javascript/dashboard/routes/dashboard/settings/automation/constants.js", "app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js", "app/javascript/dashboard/routes/dashboard/settings/reports/constants.js", "app/javascript/dashboard/store/storeFactory.js", "app/javascript/dashboard/i18n/index.js", "app/javascript/widget/i18n/index.js", "app/javascript/survey/i18n/index.js", "app/javascript/shared/constants/locales.js", "app/javascript/dashboard/helper/specs/macrosFixtures.js", "app/javascript/dashboard/routes/dashboard/settings/macros/constants.js", "**/fixtures/**", "**/*/fixtures.js",
|
||||||
]
|
]
|
||||||
|
|
||||||
test_patterns = [
|
test_patterns = [
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, reactive } from 'vue';
|
import { ref, computed, onMounted, reactive } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import { debounce } from '@chatwoot/utils';
|
import { debounce } from '@chatwoot/utils';
|
||||||
|
import { useCompaniesStore } from 'dashboard/stores/companies';
|
||||||
|
|
||||||
import CompaniesListLayout from 'dashboard/components-next/Companies/CompaniesListLayout.vue';
|
import CompaniesListLayout from 'dashboard/components-next/Companies/CompaniesListLayout.vue';
|
||||||
import CompaniesCard from 'dashboard/components-next/Companies/CompaniesCard/CompaniesCard.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 DEFAULT_SORT_FIELD = 'created_at';
|
||||||
const DEBOUNCE_DELAY = 300;
|
const DEBOUNCE_DELAY = 300;
|
||||||
|
|
||||||
const store = useStore();
|
const companiesStore = useCompaniesStore();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const { updateUISettings, uiSettings } = useUISettings();
|
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 searchQuery = computed(() => route.query?.search || '');
|
||||||
const searchValue = ref(searchQuery.value);
|
const searchValue = ref(searchQuery.value);
|
||||||
const pageNumber = computed(() => Number(route.query?.page) || 1);
|
const pageNumber = computed(() => Number(route.query?.page) || 1);
|
||||||
@@ -46,10 +50,6 @@ const sortState = reactive({
|
|||||||
const activeSort = computed(() => sortState.activeSort);
|
const activeSort = computed(() => sortState.activeSort);
|
||||||
const activeOrdering = computed(() => sortState.activeOrdering);
|
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 isFetchingList = computed(() => uiFlags.value.fetchingList);
|
||||||
|
|
||||||
const buildSortAttr = () =>
|
const buildSortAttr = () =>
|
||||||
@@ -89,13 +89,13 @@ const fetchCompanies = async (page, search, sort) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentSearch) {
|
if (currentSearch) {
|
||||||
await store.dispatch('companies/search', {
|
await companiesStore.search({
|
||||||
search: currentSearch,
|
search: currentSearch,
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
sort: currentSort,
|
sort: currentSort,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await store.dispatch('companies/get', {
|
await companiesStore.get({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
sort: currentSort,
|
sort: currentSort,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import CaptainAssistantAPI from 'dashboard/api/captain/assistant';
|
import CaptainAssistantAPI from 'dashboard/api/captain/assistant';
|
||||||
import { createStore } from './storeFactory';
|
import { createStore } from '../storeFactory';
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
name: 'CaptainAssistant',
|
name: 'CaptainAssistant',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import CaptainBulkActionsAPI from 'dashboard/api/captain/bulkActions';
|
import CaptainBulkActionsAPI from 'dashboard/api/captain/bulkActions';
|
||||||
import { createStore } from './storeFactory';
|
import { createStore } from '../storeFactory';
|
||||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import CopilotMessagesAPI from 'dashboard/api/captain/copilotMessages';
|
import CopilotMessagesAPI from 'dashboard/api/captain/copilotMessages';
|
||||||
import { createStore } from './storeFactory';
|
import { createStore } from '../storeFactory';
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
name: 'CopilotMessages',
|
name: 'CopilotMessages',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import CopilotThreadsAPI from 'dashboard/api/captain/copilotThreads';
|
import CopilotThreadsAPI from 'dashboard/api/captain/copilotThreads';
|
||||||
import { createStore } from './storeFactory';
|
import { createStore } from '../storeFactory';
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
name: 'CopilotThreads',
|
name: 'CopilotThreads',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import CaptainCustomTools from 'dashboard/api/captain/customTools';
|
import CaptainCustomTools from 'dashboard/api/captain/customTools';
|
||||||
import { createStore } from './storeFactory';
|
import { createStore } from '../storeFactory';
|
||||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import CaptainDocumentAPI from 'dashboard/api/captain/document';
|
import CaptainDocumentAPI from 'dashboard/api/captain/document';
|
||||||
import { createStore } from './storeFactory';
|
import { createStore } from '../storeFactory';
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
name: 'CaptainDocument',
|
name: 'CaptainDocument',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import CaptainInboxes from 'dashboard/api/captain/inboxes';
|
import CaptainInboxes from 'dashboard/api/captain/inboxes';
|
||||||
import { createStore } from './storeFactory';
|
import { createStore } from '../storeFactory';
|
||||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import CaptainResponseAPI from 'dashboard/api/captain/response';
|
import CaptainResponseAPI from 'dashboard/api/captain/response';
|
||||||
import { createStore } from './storeFactory';
|
import { createStore } from '../storeFactory';
|
||||||
|
|
||||||
const SET_PENDING_COUNT = 'SET_PENDING_COUNT';
|
const SET_PENDING_COUNT = 'SET_PENDING_COUNT';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import CaptainScenarios from 'dashboard/api/captain/scenarios';
|
import CaptainScenarios from 'dashboard/api/captain/scenarios';
|
||||||
import { createStore } from './storeFactory';
|
import { createStore } from '../storeFactory';
|
||||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -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',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createStore } from './storeFactory';
|
import { createStore } from '../storeFactory';
|
||||||
import CaptainToolsAPI from '../../api/captain/tools';
|
import CaptainToolsAPI from '../../api/captain/tools';
|
||||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import bulkActions from './modules/bulkActions';
|
|||||||
import campaigns from './modules/campaigns';
|
import campaigns from './modules/campaigns';
|
||||||
import cannedResponse from './modules/cannedResponse';
|
import cannedResponse from './modules/cannedResponse';
|
||||||
import categories from './modules/helpCenterCategories';
|
import categories from './modules/helpCenterCategories';
|
||||||
import companies from './modules/companies';
|
|
||||||
import contactConversations from './modules/contactConversations';
|
import contactConversations from './modules/contactConversations';
|
||||||
import contactLabels from './modules/contactLabels';
|
import contactLabels from './modules/contactLabels';
|
||||||
import contactNotes from './modules/contactNotes';
|
import contactNotes from './modules/contactNotes';
|
||||||
@@ -78,7 +77,6 @@ export default createStore({
|
|||||||
campaigns,
|
campaigns,
|
||||||
cannedResponse,
|
cannedResponse,
|
||||||
categories,
|
categories,
|
||||||
companies,
|
|
||||||
contactConversations,
|
contactConversations,
|
||||||
contactLabels,
|
contactLabels,
|
||||||
contactNotes,
|
contactNotes,
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
226
app/javascript/dashboard/store/storeFactory.js
Normal file
226
app/javascript/dashboard/store/storeFactory.js
Normal 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);
|
||||||
|
};
|
||||||
761
app/javascript/dashboard/store/storeFactory.spec.js
Normal file
761
app/javascript/dashboard/store/storeFactory.spec.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
203
app/javascript/dashboard/store/storeFactoryHelper.js
Normal file
203
app/javascript/dashboard/store/storeFactoryHelper.js
Normal 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
31
app/javascript/dashboard/stores/companies.js
Normal file
31
app/javascript/dashboard/stores/companies.js
Normal 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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -16,6 +16,7 @@ import createAxios from 'dashboard/helper/APIHelper';
|
|||||||
|
|
||||||
import commonHelpers, { isJSONValid } from 'dashboard/helper/commons';
|
import commonHelpers, { isJSONValid } from 'dashboard/helper/commons';
|
||||||
import { sync } from 'vuex-router-sync';
|
import { sync } from 'vuex-router-sync';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
import router, { initalizeRouter } from 'dashboard/routes';
|
import router, { initalizeRouter } from 'dashboard/routes';
|
||||||
import store from 'dashboard/store';
|
import store from 'dashboard/store';
|
||||||
import constants from 'dashboard/constants/globals';
|
import constants from 'dashboard/constants/globals';
|
||||||
@@ -41,9 +42,12 @@ const i18n = createI18n({
|
|||||||
|
|
||||||
sync(store, router);
|
sync(store, router);
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
app.use(store);
|
app.use(store);
|
||||||
|
app.use(pinia);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
// [VITE] Disabled this, need to renable later
|
// [VITE] Disabled this, need to renable later
|
||||||
|
|||||||
@@ -79,8 +79,9 @@
|
|||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"opus-recorder": "^8.0.5",
|
"opus-recorder": "^8.0.5",
|
||||||
"qrcode": "^1.5.4",
|
"pinia": "^3.0.4",
|
||||||
"posthog-js": "^1.260.2",
|
"posthog-js": "^1.260.2",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"semver": "7.6.3",
|
"semver": "7.6.3",
|
||||||
"snakecase-keys": "^8.0.1",
|
"snakecase-keys": "^8.0.1",
|
||||||
"timezone-phone-codes": "^0.0.2",
|
"timezone-phone-codes": "^0.0.2",
|
||||||
|
|||||||
94
pnpm-lock.yaml
generated
94
pnpm-lock.yaml
generated
@@ -157,6 +157,9 @@ importers:
|
|||||||
opus-recorder:
|
opus-recorder:
|
||||||
specifier: ^8.0.5
|
specifier: ^8.0.5
|
||||||
version: 8.0.5
|
version: 8.0.5
|
||||||
|
pinia:
|
||||||
|
specifier: ^3.0.4
|
||||||
|
version: 3.0.4(typescript@5.6.2)(vue@3.5.12(typescript@5.6.2))
|
||||||
posthog-js:
|
posthog-js:
|
||||||
specifier: ^1.260.2
|
specifier: ^1.260.2
|
||||||
version: 1.260.3
|
version: 1.260.3
|
||||||
@@ -1371,6 +1374,15 @@ packages:
|
|||||||
'@vue/devtools-api@6.6.4':
|
'@vue/devtools-api@6.6.4':
|
||||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||||
|
|
||||||
|
'@vue/devtools-api@7.7.8':
|
||||||
|
resolution: {integrity: sha512-BtFcAmDbtXGwurWUFf8ogIbgZyR+rcVES1TSNEI8Em80fD8Anu+qTRN1Fc3J6vdRHlVM3fzPV1qIo+B4AiqGzw==}
|
||||||
|
|
||||||
|
'@vue/devtools-kit@7.7.8':
|
||||||
|
resolution: {integrity: sha512-4Y8op+AoxOJhB9fpcEF6d5vcJXWKgHxC3B0ytUB8zz15KbP9g9WgVzral05xluxi2fOeAy6t140rdQ943GcLRQ==}
|
||||||
|
|
||||||
|
'@vue/devtools-shared@7.7.8':
|
||||||
|
resolution: {integrity: sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA==}
|
||||||
|
|
||||||
'@vue/reactivity@3.5.12':
|
'@vue/reactivity@3.5.12':
|
||||||
resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==}
|
resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==}
|
||||||
|
|
||||||
@@ -1623,6 +1635,9 @@ packages:
|
|||||||
birpc@0.1.1:
|
birpc@0.1.1:
|
||||||
resolution: {integrity: sha512-B64AGL4ug2IS2jvV/zjTYDD1L+2gOJTT7Rv+VaK7KVQtQOo/xZbCDsh7g727ipckmU+QJYRqo5RcifVr0Kgcmg==}
|
resolution: {integrity: sha512-B64AGL4ug2IS2jvV/zjTYDD1L+2gOJTT7Rv+VaK7KVQtQOo/xZbCDsh7g727ipckmU+QJYRqo5RcifVr0Kgcmg==}
|
||||||
|
|
||||||
|
birpc@2.8.0:
|
||||||
|
resolution: {integrity: sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==}
|
||||||
|
|
||||||
boolbase@1.0.0:
|
boolbase@1.0.0:
|
||||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||||
|
|
||||||
@@ -1827,6 +1842,10 @@ packages:
|
|||||||
constant-case@3.0.4:
|
constant-case@3.0.4:
|
||||||
resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==}
|
resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==}
|
||||||
|
|
||||||
|
copy-anything@4.0.5:
|
||||||
|
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
core-js@3.38.1:
|
core-js@3.38.1:
|
||||||
resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==}
|
resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==}
|
||||||
|
|
||||||
@@ -2607,6 +2626,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: 5.4.21
|
vite: 5.4.21
|
||||||
|
|
||||||
|
hookable@5.5.3:
|
||||||
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
hotkeys-js@3.8.7:
|
hotkeys-js@3.8.7:
|
||||||
resolution: {integrity: sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg==}
|
resolution: {integrity: sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg==}
|
||||||
|
|
||||||
@@ -2840,6 +2862,10 @@ packages:
|
|||||||
is-weakref@1.0.2:
|
is-weakref@1.0.2:
|
||||||
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
|
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
|
||||||
|
|
||||||
|
is-what@5.5.0:
|
||||||
|
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
isarray@2.0.5:
|
isarray@2.0.5:
|
||||||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||||
|
|
||||||
@@ -3443,6 +3469,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
|
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
|
||||||
engines: {node: '>= 14.16'}
|
engines: {node: '>= 14.16'}
|
||||||
|
|
||||||
|
perfect-debounce@1.0.0:
|
||||||
|
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||||
|
|
||||||
picocolors@1.0.1:
|
picocolors@1.0.1:
|
||||||
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
|
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
|
||||||
|
|
||||||
@@ -3465,6 +3494,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
pinia@3.0.4:
|
||||||
|
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.5.0'
|
||||||
|
vue: ^3.5.11
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
pirates@4.0.6:
|
pirates@4.0.6:
|
||||||
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -3872,6 +3910,9 @@ packages:
|
|||||||
rfdc@1.3.0:
|
rfdc@1.3.0:
|
||||||
resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
|
resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
|
||||||
|
|
||||||
|
rfdc@1.4.1:
|
||||||
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
rimraf@3.0.2:
|
rimraf@3.0.2:
|
||||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
@@ -4044,6 +4085,10 @@ packages:
|
|||||||
spark-md5@3.0.2:
|
spark-md5@3.0.2:
|
||||||
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
||||||
|
|
||||||
|
speakingurl@14.0.1:
|
||||||
|
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
sprintf-js@1.0.3:
|
sprintf-js@1.0.3:
|
||||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||||
|
|
||||||
@@ -4130,6 +4175,10 @@ packages:
|
|||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
superjson@2.2.5:
|
||||||
|
resolution: {integrity: sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -5842,6 +5891,24 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/devtools-api@6.6.4': {}
|
'@vue/devtools-api@6.6.4': {}
|
||||||
|
|
||||||
|
'@vue/devtools-api@7.7.8':
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-kit': 7.7.8
|
||||||
|
|
||||||
|
'@vue/devtools-kit@7.7.8':
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-shared': 7.7.8
|
||||||
|
birpc: 2.8.0
|
||||||
|
hookable: 5.5.3
|
||||||
|
mitt: 3.0.1
|
||||||
|
perfect-debounce: 1.0.0
|
||||||
|
speakingurl: 14.0.1
|
||||||
|
superjson: 2.2.5
|
||||||
|
|
||||||
|
'@vue/devtools-shared@7.7.8':
|
||||||
|
dependencies:
|
||||||
|
rfdc: 1.4.1
|
||||||
|
|
||||||
'@vue/reactivity@3.5.12':
|
'@vue/reactivity@3.5.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/shared': 3.5.12
|
'@vue/shared': 3.5.12
|
||||||
@@ -6137,6 +6204,8 @@ snapshots:
|
|||||||
|
|
||||||
birpc@0.1.1: {}
|
birpc@0.1.1: {}
|
||||||
|
|
||||||
|
birpc@2.8.0: {}
|
||||||
|
|
||||||
boolbase@1.0.0: {}
|
boolbase@1.0.0: {}
|
||||||
|
|
||||||
boxen@8.0.1:
|
boxen@8.0.1:
|
||||||
@@ -6383,6 +6452,10 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
upper-case: 2.0.2
|
upper-case: 2.0.2
|
||||||
|
|
||||||
|
copy-anything@4.0.5:
|
||||||
|
dependencies:
|
||||||
|
is-what: 5.5.0
|
||||||
|
|
||||||
core-js@3.38.1: {}
|
core-js@3.38.1: {}
|
||||||
|
|
||||||
countries-and-timezones@3.6.0: {}
|
countries-and-timezones@3.6.0: {}
|
||||||
@@ -7341,6 +7414,8 @@ snapshots:
|
|||||||
- terser
|
- terser
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
hotkeys-js@3.8.7: {}
|
hotkeys-js@3.8.7: {}
|
||||||
|
|
||||||
html-encoding-sniffer@3.0.0:
|
html-encoding-sniffer@3.0.0:
|
||||||
@@ -7558,6 +7633,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
|
|
||||||
|
is-what@5.5.0: {}
|
||||||
|
|
||||||
isarray@2.0.5: {}
|
isarray@2.0.5: {}
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
@@ -8215,6 +8292,8 @@ snapshots:
|
|||||||
|
|
||||||
pathval@2.0.0: {}
|
pathval@2.0.0: {}
|
||||||
|
|
||||||
|
perfect-debounce@1.0.0: {}
|
||||||
|
|
||||||
picocolors@1.0.1: {}
|
picocolors@1.0.1: {}
|
||||||
|
|
||||||
picocolors@1.1.0: {}
|
picocolors@1.1.0: {}
|
||||||
@@ -8227,6 +8306,13 @@ snapshots:
|
|||||||
|
|
||||||
pify@2.3.0: {}
|
pify@2.3.0: {}
|
||||||
|
|
||||||
|
pinia@3.0.4(typescript@5.6.2)(vue@3.5.12(typescript@5.6.2)):
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-api': 7.7.8
|
||||||
|
vue: 3.5.12(typescript@5.6.2)
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.6.2
|
||||||
|
|
||||||
pirates@4.0.6: {}
|
pirates@4.0.6: {}
|
||||||
|
|
||||||
pkcs7@1.0.4:
|
pkcs7@1.0.4:
|
||||||
@@ -8686,6 +8772,8 @@ snapshots:
|
|||||||
|
|
||||||
rfdc@1.3.0: {}
|
rfdc@1.3.0: {}
|
||||||
|
|
||||||
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
rimraf@3.0.2:
|
rimraf@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
@@ -8897,6 +8985,8 @@ snapshots:
|
|||||||
|
|
||||||
spark-md5@3.0.2: {}
|
spark-md5@3.0.2: {}
|
||||||
|
|
||||||
|
speakingurl@14.0.1: {}
|
||||||
|
|
||||||
sprintf-js@1.0.3: {}
|
sprintf-js@1.0.3: {}
|
||||||
|
|
||||||
stackback@0.0.2: {}
|
stackback@0.0.2: {}
|
||||||
@@ -8992,6 +9082,10 @@ snapshots:
|
|||||||
pirates: 4.0.6
|
pirates: 4.0.6
|
||||||
ts-interface-checker: 0.1.13
|
ts-interface-checker: 0.1.13
|
||||||
|
|
||||||
|
superjson@2.2.5:
|
||||||
|
dependencies:
|
||||||
|
copy-anything: 4.0.5
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user