From 659cdf9d7299f21fac5cafc3ac0edd94f96f0574 Mon Sep 17 00:00:00 2001 From: Sviatoslav Bar Date: Sun, 15 Sep 2024 23:35:23 +0300 Subject: [PATCH] Feat/ lookups - router, plugins, store, dummy [WTEL-4740] --- src/app/composables/useDummy.js | 58 +++++ src/app/plugins/webitel-ui.js | 11 +- src/app/router/_internals/RouteNames.enum.js | 58 +++++ src/app/router/_internals/guards.js | 25 +++ src/app/router/index.js | 27 ++- .../BaseOpenedInstanceStoreModuleMixin.js | 59 +++++ .../BaseTableStoreModuleMixin.js | 212 ++++++++++++++++++ .../HistoryStoreModule/HistoryStoreModule.js | 56 +++++ .../StoreModules/NestedObjectStoreModule.js | 71 ++++++ .../StoreModules/ObjectStoreModule.js | 52 +++++ .../PermissionsStoreModule.js | 125 +++++++++++ .../_internals/headers.js | 28 +++ src/app/store/index.js | 8 + 13 files changed, 785 insertions(+), 5 deletions(-) create mode 100644 src/app/composables/useDummy.js create mode 100644 src/app/router/_internals/RouteNames.enum.js create mode 100644 src/app/router/_internals/guards.js create mode 100644 src/app/store/BaseStoreModules/StoreModuleMixins/BaseOpenedInstanceStoreModuleMixin.js create mode 100644 src/app/store/BaseStoreModules/StoreModuleMixins/BaseTableStoreModuleMixin.js create mode 100644 src/app/store/BaseStoreModules/StoreModules/HistoryStoreModule/HistoryStoreModule.js create mode 100644 src/app/store/BaseStoreModules/StoreModules/NestedObjectStoreModule.js create mode 100644 src/app/store/BaseStoreModules/StoreModules/ObjectStoreModule.js create mode 100644 src/app/store/BaseStoreModules/StoreModules/PermissionsStoreModule/PermissionsStoreModule.js create mode 100644 src/app/store/BaseStoreModules/StoreModules/PermissionsStoreModule/_internals/headers.js diff --git a/src/app/composables/useDummy.js b/src/app/composables/useDummy.js new file mode 100644 index 00000000..d013d538 --- /dev/null +++ b/src/app/composables/useDummy.js @@ -0,0 +1,58 @@ +import IsEmpty from '@webitel/ui-sdk/src/scripts/isEmpty'; +import getNamespacedState from '@webitel/ui-sdk/src/store/helpers/getNamespacedState'; +import { computed, ref, watch } from 'vue'; +import { useRoute } from 'vue-router'; +import { useStore } from 'vuex'; +import defaultDummyPicAfterSearchDark from '../assets/dummy-dark.svg'; +import defaultDummyPicAfterSearchLight from '../assets/dummy-light.svg'; + +export function useDummy({ + namespace, + showAction, + hiddenText, + dummyPic, + dummyText, + dummyPicAfterSearch, + dummyTextAfterSearch = 'objects.emptyResultSearch', +}) { + const store = useStore(); + const route = useRoute(); + + const dummy = ref(''); + + const dataList = computed(() => getNamespacedState(store.state, namespace).dataList); + const search = computed(() => getNamespacedState(store.state, namespace).search); + + const darkMode = computed(() => store.getters['appearance/DARK_MODE']); + const dummyImgAfterSearch = computed(() => { + if (dummyPicAfterSearch) return dummyPicAfterSearch; + return darkMode.value ? defaultDummyPicAfterSearchDark : defaultDummyPicAfterSearchLight; + }); + + watch( + () => dataList, + () => { + if (!dataList.value.length) { + if ( + IsEmpty(route?.query) + ? search.value + : Object.values(route.query).some((query) => query.length) + ) { + return (dummy.value = { + src: dummyImgAfterSearch, + text: dummyTextAfterSearch, + }); + } + return (dummy.value = { + src: dummyPic, + text: dummyText, + showAction, + hiddenText, + }); + } + return (dummy.value = ''); + }, + { deep: true }, + ); + return { dummy }; +} diff --git a/src/app/plugins/webitel-ui.js b/src/app/plugins/webitel-ui.js index f7c1ef7b..ac974655 100644 --- a/src/app/plugins/webitel-ui.js +++ b/src/app/plugins/webitel-ui.js @@ -1,19 +1,24 @@ +// import styles +import '@webitel/ui-sdk/dist/ui-sdk.css'; import WebitelUI from '@webitel/ui-sdk/dist/ui-sdk.js'; + +// import locale import WebitelUIEn from '@webitel/ui-sdk/src/locale/en/en.js'; import WebitelUIKz from '@webitel/ui-sdk/src/locale/kz/kz.js'; import WebitelUIRu from '@webitel/ui-sdk/src/locale/ru/ru.js'; import WebitelUIUa from '@webitel/ui-sdk/src/locale/ua/ua.js'; import eventBus from '@webitel/ui-sdk/src/scripts/eventBus.js'; import i18n from '../locale/i18n.js'; -import '@webitel/ui-sdk/dist/ui-sdk.css'; const globals = { $baseURL: import.meta.env.BASE_URL, }; +// init plugin +export default [WebitelUI, { eventBus, globals }]; +// add plugin locales to main i18n + i18n.global.mergeLocaleMessage('en', WebitelUIEn); i18n.global.mergeLocaleMessage('ru', WebitelUIRu); i18n.global.mergeLocaleMessage('ua', WebitelUIUa); i18n.global.mergeLocaleMessage('kz', WebitelUIKz); - -export default [WebitelUI, { eventBus, globals }]; diff --git a/src/app/router/_internals/RouteNames.enum.js b/src/app/router/_internals/RouteNames.enum.js new file mode 100644 index 00000000..42f0417c --- /dev/null +++ b/src/app/router/_internals/RouteNames.enum.js @@ -0,0 +1,58 @@ +export default Object.freeze({ + AUTH: 'auth', + APPLICATION_HUB: 'application-hub', + HOME: 'home', + START: 'start', + + // DIRECTORY + LICENSE: 'license', + USERS: 'users', + DEVICES: 'devices', + + // ROUTING + FLOW: 'flow', + DIALPLAN: 'dialplan', + GATEWAYS: 'gateways', + CHATPLAN: 'chatplan', + CHAT_GATEWAYS: 'chat-gateways', + + // LOOKUPS + SKILLS: 'skills', + BUCKETS: 'buckets', + BLACKLIST: 'blacklists', + REGIONS: 'regions', + CALENDARS: 'calendars', + COMMUNICATIONS: 'communications', + PAUSE_CAUSE: 'agent-pause-cause', + MEDIA: 'media', + CONTACT_GROUPS: 'contact-groups', + + // CONTACT-CENTER + AGENTS: 'agents', + TEAMS: 'teams', + RESOURCES: 'resources', + RESOURCE_GROUPS: 'resource-groups', + QUEUES: 'queues', + MEMBERS: 'queues-members', + + // INTEGRATIONS + STORAGE: 'storage', + COGNITIVE_PROFILES: 'cognitive-profiles', + EMAIL_PROFILES: 'email-profiles', + SINGLE_SIGN_ON: 'single-sign-on', + IMPORT_CSV: 'import-csv', + TRIGGERS: 'triggers', + + // PERMISSIONS + OBJECTS: 'objects', + ROLES: 'roles', + + // SYSTEM + CHANGELOGS: 'changelogs', + CONFIGURATION: 'configuration', + GLOBAL_VARIABLES: 'global-variables', + + SETTINGS_PAGE: 'settings', + PAGE_403: 'access-denied', + PAGE_404: '', +}); diff --git a/src/app/router/_internals/guards.js b/src/app/router/_internals/guards.js new file mode 100644 index 00000000..7f4dec26 --- /dev/null +++ b/src/app/router/_internals/guards.js @@ -0,0 +1,25 @@ +import store from "../../store/index.js"; + +export const checkAppAccess = (to, from, next) => { + // check for === false because it can be undefined + if (to.meta.requiresAccess === false) next(); + + const hasReadAccess = store.getters['userinfo/CHECK_APP_ACCESS'](store.getters['userinfo/THIS_APP']); + if (hasReadAccess) { + next(); + } else { + next(); + // next('/access-denied'); + } +}; + +export const checkRouteAccess = (to, from, next) => { + const hasReadAccess = store.getters['userinfo/HAS_READ_ACCESS']({ route: to }); + if (hasReadAccess) { + next(); + } else { + next(); + console.log('error?') + // next('/access-denied'); + } +}; diff --git a/src/app/router/index.js b/src/app/router/index.js index 37ab7ffa..ba7100e9 100644 --- a/src/app/router/index.js +++ b/src/app/router/index.js @@ -5,7 +5,6 @@ import ContactCommunications from '../../modules/contacts/components/opened-contact-communications.vue'; import OpenedContact from '../../modules/contacts/components/opened-contact.vue'; -import TheContacts from '../../modules/contacts/components/the-contacts.vue'; import ContactPermissions from '../../modules/contacts/modules/permissions/components/the-permissions.vue'; import ContactTimeline @@ -14,6 +13,14 @@ import ContactVariables from '../../modules/contacts/modules/variables/components/the-variables.vue'; import TheCrmWorkspace from '../components/the-crm-workspace.vue'; import AccessDenied from '../components/utils/access-denied-component.vue'; +import TheStartPage + from '../../modules/start-page/components/the-start-page.vue'; +import TheContacts from '../../modules/contacts/components/the-contacts.vue'; +import TheConfiguration from '../../modules/configuration/components/the-configuration.vue'; + +import ContactGroupsRoutes from "../../modules/lookups/modules/contact-groups/modules/cgroups/router/contact-groups.js"; + + import store from '../store'; const checkAppAccess = (to, from, next) => { @@ -40,16 +47,30 @@ const routes = [ { path: '/', name: 'crm-workspace', - redirect: { name: CrmSections.CONTACTS }, + redirect: { name: 'the-start-page' }, component: TheCrmWorkspace, beforeEnter: checkAppAccess, children: [ + { + path: 'start-page', + name: 'the-start-page', + component: TheStartPage, + }, { path: 'contacts', name: CrmSections.CONTACTS, component: TheContacts, beforeEnter: checkRouteAccess, + // redirect: { name: `the-start-page` }, }, + { + path: 'configuration', + name: CrmSections.CONFIGURATION, + component: TheConfiguration, + // beforeEnter: checkRouteAccess, + // redirect: { name: `the-start-page` }, + }, + { path: 'contacts/:id', name: `${CrmSections.CONTACTS}-card`, @@ -99,6 +120,7 @@ const routes = [ }, ], }, + ...ContactGroupsRoutes, ], }, { @@ -106,6 +128,7 @@ const routes = [ name: 'access-denied', component: AccessDenied, }, + ]; const router = createRouter({ diff --git a/src/app/store/BaseStoreModules/StoreModuleMixins/BaseOpenedInstanceStoreModuleMixin.js b/src/app/store/BaseStoreModules/StoreModuleMixins/BaseOpenedInstanceStoreModuleMixin.js new file mode 100644 index 00000000..f012be2b --- /dev/null +++ b/src/app/store/BaseStoreModules/StoreModuleMixins/BaseOpenedInstanceStoreModuleMixin.js @@ -0,0 +1,59 @@ +import deepCopy from 'deep-copy'; +import set from 'lodash/set'; + +const state = { + itemId: 0, + itemInstance: {}, +}; + +const actions = { + SET_PARENT_ITEM_ID: (context, id) => { + context.commit('SET_PARENT_ITEM_ID', id); + }, + SET_ITEM_ID: (context, id) => { + if (id !== 'new') context.commit('SET_ITEM_ID', id); + else context.commit('SET_ITEM_ID', 0); + }, + LOAD_ITEM: async (context) => { + if (context.state.itemId) { + const item = await context.dispatch('GET_ITEM'); + context.commit('SET_ITEM', item); + } + }, + SET_ITEM_PROPERTY: (context, payload) => { + context.commit('SET_ITEM_PROPERTY', payload); + context.commit('SET_ITEM_PROPERTY', { + prop: '_dirty', + value: true, + }); + }, + RESET_ITEM_STATE: async (context) => { + context.commit('RESET_ITEM_STATE'); + }, +}; + +const mutations = { + SET_PARENT_ITEM_ID: (state, id) => { + state.parentId = id; + }, + SET_ITEM_ID: (state, id) => { + state.itemId = id; + }, + SET_ITEM_PROPERTY: (state, { prop, value, path }) => { + if (path) { + set(state.itemInstance, path, value); + } else { + // DEPRECATED, LEGACY CODE + state.itemInstance[prop] = value; + } + }, + SET_ITEM: (state, item) => { + state.itemInstance = item; + }, +}; + +export default { + getActions: () => actions, + getMutations: () => mutations, + generateState: () => deepCopy(state), +}; diff --git a/src/app/store/BaseStoreModules/StoreModuleMixins/BaseTableStoreModuleMixin.js b/src/app/store/BaseStoreModules/StoreModuleMixins/BaseTableStoreModuleMixin.js new file mode 100644 index 00000000..ad786ee2 --- /dev/null +++ b/src/app/store/BaseStoreModules/StoreModuleMixins/BaseTableStoreModuleMixin.js @@ -0,0 +1,212 @@ +import { SortSymbols, sortToQueryAdapter } from '@webitel/ui-sdk/src/scripts/sortQueryAdapters'; +import deepCopy from 'deep-copy'; + +const state = { + headers: [], + dataList: [], + aggs: {}, + size: 10, + search: '', + page: 1, + sort: '', + isNextPage: false, +}; + +const actions = { + // HOOKS TO BE OVERRIDEN, IF NEEDED + BEFORE_SET_DATA_LIST_HOOK: (context, { items, next, aggs }) => ({ + items, + next, + aggs, + }), + AFTER_SET_DATA_LIST_HOOK: (context, { items, next }) => ({ items, next }), + + LOAD_DATA_LIST: async (context, _query) => { + /* + https://my.webitel.com/browse/WTEL-3560 + preventively disable isNext to handle case when user is clicking + "next" faster than actual request is made + */ + context.commit('SET_IS_NEXT', false); + + const query = { + ...context.getters['filters/GET_FILTERS'], + ..._query, + }; + try { + let { items = [], next = false, aggs = {} } = await context.dispatch('GET_LIST', query); + + /* [https://my.webitel.com/browse/WTEL-3793] + * When deleting the last item from list, + * if there are other items on the previous page, you need to go back */ + if (!items.length && context.state.page > 1) return context.dispatch('PREV_PAGE'); + + /* we should set _isSelected property to all items in tables cause their checkbox selection + * is based on this property. Previously, this prop was set it api consumers, but now + * admin-specific were replaced by webitel-sdk consumers and i supposed it will be + * weird to set this property in each api file through defaultListObject */ + items = items.map((item) => ({ + ...item, + _isSelected: false, + })); + + const afterHook = await context.dispatch('BEFORE_SET_DATA_LIST_HOOK', { + items, + next, + aggs, + }); + context.commit('SET_DATA_LIST', afterHook.items); + context.commit('SET_IS_NEXT', afterHook.next); + context.commit('AGGS', afterHook.aggs); + context.dispatch('AFTER_SET_DATA_LIST_HOOK', afterHook); + + } catch (err) { + console.error(err); + } + }, + SET_SIZE: async (context, size) => { + context.commit('SET_SIZE', size); + await context.dispatch('RESET_PAGE'); + }, + SET_SEARCH: async (context, search) => { + context.commit('SET_SEARCH', search); + await context.dispatch('RESET_PAGE'); + }, + NEXT_PAGE: (context) => { + const page = context.state.page + 1; + context.commit('SET_PAGE', page); + context.dispatch('LOAD_DATA_LIST'); + }, + PREV_PAGE: (context) => { + if (context.state.page > 1) { + const page = context.state.page - 1; + context.commit('SET_PAGE', page); + context.dispatch('LOAD_DATA_LIST'); + } + }, + RESET_PAGE: (context) => { + const page = 1; + context.commit('SET_PAGE', page); + }, + SET_HEADERS: (context, headers) => context.commit('SET_HEADERS', headers), + SORT: async (context, { header, nextSortOrder }) => { + const sort = nextSortOrder + ? `${sortToQueryAdapter(nextSortOrder)}${header.field}` + : nextSortOrder; + context.commit('SET_SORT', sort); + context.dispatch('UPDATE_HEADER_SORT', { + header, + nextSortOrder, + }); + await context.dispatch('RESET_PAGE'); + return context.dispatch('LOAD_DATA_LIST'); + }, + UPDATE_HEADER_SORT: (context, { header, nextSortOrder }) => { + const headers = context.state.headers.map((oldHeader) => { + // eslint-disable-next-line no-prototype-builtins + if (oldHeader.sort !== undefined) { + return { + ...oldHeader, + sort: oldHeader.field === header.field ? nextSortOrder : SortSymbols.NONE, + }; + } + return oldHeader; + }); + context.commit('SET_HEADERS', headers); + }, + PATCH_ITEM_PROPERTY: async (context, { item, index, prop, value }) => { + await context.commit('PATCH_ITEM_PROPERTY', { + index, + prop, + value, + }); + const id = item?.id || context.state.dataList[index].id; + const changes = { [prop]: value }; + try { + await context.dispatch('PATCH_ITEM', { + id, + changes, + }); + context.commit('PATCH_ITEM_PROPERTY', { + item, + index, + prop, + value, + }); + } catch { + context.dispatch('LOAD_DATA_LIST'); + } + }, + DELETE: async (context, deleted) => { + let action = 'DELETE_SINGLE'; + if (Array.isArray(deleted)) { + if (deleted.length) action = 'DELETE_BULK'; + else action = 'DELETE_ALL'; + } + try { + await context.dispatch(action, deleted); + } catch (err) { + throw err; + } finally { + await context.dispatch('LOAD_DATA_LIST'); + } + }, + DELETE_SINGLE: async (context, { id }) => { + try { + await context.dispatch('DELETE_ITEM', id); + } catch (err) { + throw err; + } + }, + DELETE_BULK: async (context, deleted) => + Promise.allSettled(deleted.map((item) => context.dispatch('DELETE_SINGLE', item))), + // REMOVE_ITEM: async (context, index) => { + // const id = context.state.dataList[index].id; + // context.commit('REMOVE_ITEM', index); + // try { + // await context.dispatch('DELETE_ITEM', id); + // } catch (err) { + // throw err; + // } + // }, +}; + +const mutations = { + SET_DATA_LIST: (state, items) => { + state.dataList = items; + }, + SET_SIZE: (state, size) => { + state.size = size; + }, + SET_SEARCH: (state, search) => { + state.search = search; + }, + SET_PAGE: (state, page) => { + state.page = page; + }, + SET_SORT: (state, sort) => { + state.sort = sort; + }, + SET_IS_NEXT: (state, next) => { + state.isNextPage = next; + }, + AGGS: (state, aggs) => { + state.aggs = aggs; + }, + SET_HEADERS: (state, headers) => { + state.headers = headers; + }, + PATCH_ITEM_PROPERTY: (state, { index, prop, value }) => { + state.dataList[index][prop] = value; + }, + // REMOVE_ITEM: (state, index) => { + // state.dataList.splice(index, 1); + // }, +}; + + +export default { + getActions: () => actions, + getMutations: () => mutations, + generateState: () => deepCopy(state), +}; diff --git a/src/app/store/BaseStoreModules/StoreModules/HistoryStoreModule/HistoryStoreModule.js b/src/app/store/BaseStoreModules/StoreModules/HistoryStoreModule/HistoryStoreModule.js new file mode 100644 index 00000000..001165aa --- /dev/null +++ b/src/app/store/BaseStoreModules/StoreModules/HistoryStoreModule/HistoryStoreModule.js @@ -0,0 +1,56 @@ +import BaseStoreModule from '@webitel/ui-sdk/src/store/BaseStoreModules/BaseStoreModule'; +import BaseOpenedInstanceModule from '../../StoreModuleMixins/BaseOpenedInstanceStoreModuleMixin'; +import BaseTableModule from '../../StoreModuleMixins/BaseTableStoreModuleMixin'; + +export class HistoryStoreModule extends BaseStoreModule { + getters = {}; + actions = { + ...BaseTableModule.getActions(), + ...BaseOpenedInstanceModule.getActions(), + + SET_PERIOD: (context, period) => { + context.commit('SET_FROM', period.from); + context.commit('SET_TO', period.to); + context.dispatch('LOAD_DATA_LIST'); + }, + + SET_FROM: (context, from) => { + context.commit('SET_FROM', from); + context.dispatch('LOAD_DATA_LIST'); + }, + SET_TO: (context, to) => { + context.commit('SET_TO', to); + context.dispatch('LOAD_DATA_LIST'); + }, + }; + + _resettableState = () => ({ + ...BaseTableModule.generateState(), + parentId: 0, + from: new Date().setHours(0, 0, 0, 0), + to: Date.now(), + }); + + state = this._resettableState(); + mutations = { + ...BaseTableModule.getMutations(), + ...BaseOpenedInstanceModule.getMutations(), + + SET_FROM: (state, from) => { + state.from = from; + }, + SET_TO: (state, to) => { + state.to = to; + }, + RESET_ITEM_STATE: (state) => { + Object.assign(state, this._resettableState()); + }, + }; + + generateGetListAction(APIMethod) { + this.actions.GET_LIST = (context) => APIMethod(context.state); + return this; + } +} + +export default HistoryStoreModule; diff --git a/src/app/store/BaseStoreModules/StoreModules/NestedObjectStoreModule.js b/src/app/store/BaseStoreModules/StoreModules/NestedObjectStoreModule.js new file mode 100644 index 00000000..d22c3db6 --- /dev/null +++ b/src/app/store/BaseStoreModules/StoreModules/NestedObjectStoreModule.js @@ -0,0 +1,71 @@ +import BaseStoreModule from '@webitel/ui-sdk/src/store/BaseStoreModules/BaseStoreModule'; +import deepCopy from 'deep-copy'; +import BaseOpenedInstanceModule from '../StoreModuleMixins/BaseOpenedInstanceStoreModuleMixin'; +import BaseTableModule from '../StoreModuleMixins/BaseTableStoreModuleMixin'; + +const DEFAULT_STATE = BaseTableModule.generateState(); +const DEFAULT_ITEM_STATE = BaseOpenedInstanceModule.generateState(); + +export default class NestedObjectStoreModule extends BaseStoreModule { + getters = { + GET_ITEM_BY_ID: (state) => (itemId) => state.dataList.find((item) => item.id === itemId), + GET_ITEM_PROP_BY_ID: (state, getters) => (itemId, prop) => { + if (itemId) return getters.GET_ITEM_BY_ID(itemId)[prop]; + }, + }; + + actions = { + ...BaseTableModule.getActions(), + ...BaseOpenedInstanceModule.getActions(), + + ADD_ITEM: async (context) => { + if (!context.state.itemId) { + const { id } = await context.dispatch('POST_ITEM'); + context.dispatch('SET_ITEM_ID', id); + await context.dispatch('LOAD_DATA_LIST'); + } + }, + UPDATE_ITEM: async (context) => { + if (context.state.itemInstance._dirty) { + await context.dispatch('UPD_ITEM'); + await context.dispatch('LOAD_DATA_LIST'); + } + }, + RESET_STATE: (context) => { + context.commit('RESET_STATE'); + }, + }; + + mutations = { + ...BaseTableModule.getMutations(), + ...BaseOpenedInstanceModule.getMutations(), + + RESET_STATE: (state) => { + Object.assign(state, this._resettableState(), this._resettableItemState()); + }, + RESET_ITEM_STATE: (state) => { + Object.assign(state, this._resettableItemState()); + }, + }; + + constructor({ resettableState, resettableItemState, headers } = {}) { + super(); + const state = { parentId: 0 }; + this._resettableState = () => + deepCopy({ + ...DEFAULT_STATE, + ...resettableState, + headers, + }); + this._resettableItemState = () => + deepCopy({ + ...DEFAULT_ITEM_STATE, + ...resettableItemState, + }); + this.state = { + ...state, + ...this._resettableState(), + ...this._resettableItemState(), + }; + } +} diff --git a/src/app/store/BaseStoreModules/StoreModules/ObjectStoreModule.js b/src/app/store/BaseStoreModules/StoreModules/ObjectStoreModule.js new file mode 100644 index 00000000..a0b2ee03 --- /dev/null +++ b/src/app/store/BaseStoreModules/StoreModules/ObjectStoreModule.js @@ -0,0 +1,52 @@ +import BaseStoreModule from '@webitel/ui-sdk/src/store/BaseStoreModules/BaseStoreModule'; +import deepCopy from 'deep-copy'; +import BaseOpenedInstanceModule from '../StoreModuleMixins/BaseOpenedInstanceStoreModuleMixin'; +import BaseTableModule from '../StoreModuleMixins/BaseTableStoreModuleMixin'; + +export default class ObjectStoreModule extends BaseStoreModule { + getters = {}; + + actions = { + ...BaseTableModule.getActions(), + ...BaseOpenedInstanceModule.getActions(), + // https://webitel.atlassian.net/browse/WTEL-4195 + ADD_ITEM: async (context) => { + if (!context.state.itemId) { + const { id } = await context.dispatch('POST_ITEM'); + await context.dispatch('SET_ITEM_ID', id); + context.dispatch('LOAD_ITEM'); + } + }, + UPDATE_ITEM: async (context) => { + if (context.state.itemInstance._dirty) { + await context.dispatch('UPD_ITEM'); + context.dispatch('LOAD_ITEM'); + } + }, + }; + + mutations = { + ...BaseTableModule.getMutations(), + ...BaseOpenedInstanceModule.getMutations(), + + RESET_ITEM_STATE: (state) => { + Object.assign(state, this._resettableState()); + }, + }; + + modules = {}; + + constructor({ resettableState, headers } = {}) { + super(); + this._resettableState = () => + deepCopy({ + ...BaseOpenedInstanceModule.generateState(), + ...resettableState, + }); + this.state = { + ...BaseTableModule.generateState(), + headers, + ...this._resettableState(), + }; + } +} diff --git a/src/app/store/BaseStoreModules/StoreModules/PermissionsStoreModule/PermissionsStoreModule.js b/src/app/store/BaseStoreModules/StoreModules/PermissionsStoreModule/PermissionsStoreModule.js new file mode 100644 index 00000000..85d414c1 --- /dev/null +++ b/src/app/store/BaseStoreModules/StoreModules/PermissionsStoreModule/PermissionsStoreModule.js @@ -0,0 +1,125 @@ +import BaseStoreModule from '@webitel/ui-sdk/src/store/BaseStoreModules/BaseStoreModule'; +import PermissionsAPI from '../../../../../modules/_shared/permissions-tab/api/PermissionsAPI'; +import AccessMode from '../../../../../modules/permissions/modules/objects/store/_internals/enums/AccessMode.enum'; +import BaseOpenedInstanceModule from '../../StoreModuleMixins/BaseOpenedInstanceStoreModuleMixin'; +import BaseTableModule from '../../StoreModuleMixins/BaseTableStoreModuleMixin'; +import defaultHeaders from './_internals/headers'; + +export class PermissionsStoreModule extends BaseStoreModule { + state = {}; + + getters = {}; + + actions = { + ...BaseTableModule.getActions(), + ...BaseOpenedInstanceModule.getActions(), + + CHANGE_CREATE_ACCESS_MODE: (context, payload) => + context.dispatch('CHANGE_ACCESS_MODE', { + ruleName: 'x', + ...payload, + }), + CHANGE_READ_ACCESS_MODE: (context, payload) => + context.dispatch('CHANGE_ACCESS_MODE', { + ruleName: 'r', + ...payload, + }), + CHANGE_UPDATE_ACCESS_MODE: (context, payload) => + context.dispatch('CHANGE_ACCESS_MODE', { + ruleName: 'w', + ...payload, + }), + CHANGE_DELETE_ACCESS_MODE: (context, payload) => + context.dispatch('CHANGE_ACCESS_MODE', { + ruleName: 'd', + ...payload, + }), + CHANGE_ACCESS_MODE: async (context, { mode, ruleName, item }) => { + const have = item.access[ruleName]; + let want; + /* + has | patch | got + -----+-------+----- + - | w | w + w | w | - + - | ww | ww + w | ww | ww + ww | ww | w + ww | w | - + */ + switch (mode.id) { + case AccessMode.FORBIDDEN: + want = ruleName; + break; + case AccessMode.ALLOW: + want = have.rule || ruleName; + break; + case AccessMode.MANAGE: + want = `${ruleName}${ruleName}`; + break; + default: + return; + } + const changes = { + grantee: +item.grantee.id, + grants: want, + }; + try { + await context.dispatch('PATCH_ACCESS_MODE', { + item, + changes, + }); + } catch (err) { + throw err; + } finally { + context.dispatch('LOAD_DATA_LIST'); + } + }, + ADD_ROLE_PERMISSIONS: async (context, role) => { + const changes = { + grantee: +role.id, + grants: 'r', + }; + try { + await context.dispatch('PATCH_ACCESS_MODE', { + changes, + }); + } catch { + } finally { + context.dispatch('LOAD_DATA_LIST'); + } + }, + RESET_ITEM_STATE: async (context) => { + context.commit('RESET_ITEM_STATE'); + }, + }; + + mutations = { + ...BaseTableModule.getMutations(), + ...BaseOpenedInstanceModule.getMutations(), + + RESET_ITEM_STATE: (state) => { + Object.assign(state, this._resettableState()); + }, + }; + + constructor({ headers = defaultHeaders } = {}) { + super(); + this._resettableState = () => ({ + ...BaseTableModule.generateState(), + headers, + parentId: 0, + }); + this.state = this._resettableState(); + } + + generateAPIActions(url) { + const permissionsAPI = new PermissionsAPI(url); + this.actions.GET_LIST = (context) => permissionsAPI.getList(context.state); + this.actions.PATCH_ACCESS_MODE = (context, { changes }) => + permissionsAPI.patch(context.state.parentId, [changes]); + return this; + } +} + +export default PermissionsStoreModule; diff --git a/src/app/store/BaseStoreModules/StoreModules/PermissionsStoreModule/_internals/headers.js b/src/app/store/BaseStoreModules/StoreModules/PermissionsStoreModule/_internals/headers.js new file mode 100644 index 00000000..edd236cc --- /dev/null +++ b/src/app/store/BaseStoreModules/StoreModules/PermissionsStoreModule/_internals/headers.js @@ -0,0 +1,28 @@ +import { SortSymbols } from '@webitel/ui-sdk/src/scripts/sortQueryAdapters'; + +export default [ + { + value: 'grantee', + locale: 'objects.name', + field: 'grantee', + sort: SortSymbols.NONE, + }, + { + value: 'read', + locale: 'objects.read', + field: 'r', + sort: SortSymbols.NONE, + }, + { + value: 'edit', + locale: 'objects.edit', + field: 'w', + sort: SortSymbols.NONE, + }, + { + value: 'delete', + locale: 'objects.permissions.object.delete', + field: 'd', + sort: SortSymbols.NONE, + }, +]; diff --git a/src/app/store/index.js b/src/app/store/index.js index cb1c56e2..dc113fc6 100644 --- a/src/app/store/index.js +++ b/src/app/store/index.js @@ -4,6 +4,10 @@ import userinfo from '../../modules/userinfo/store/userinfo'; import appearance from '../../modules/appearance/store/appearance'; import instance from '../api/instance'; +import cgroups from '../../modules/lookups/modules/contact-groups/store/cgroups'; +import permissions from '../../modules/permissions/store/permissions'; +// import routing from '../../modules/routing/store/routing'; + export default createStore({ state: { router: null, @@ -22,5 +26,9 @@ export default createStore({ contacts, userinfo, appearance, + + cgroups, + permissions, + // routing }, });