From 0b7070549ad8678f8fe3fbf6c6373bcd51719178 Mon Sep 17 00:00:00 2001 From: Sviatoslav Bar Date: Fri, 13 Sep 2024 11:14:05 +0300 Subject: [PATCH] Feat/ CRM - lookups [WTEL-4740] --- package-lock.json | 274 ++++++----- package.json | 4 +- .../APIPermissionsGetter.js | 84 ++++ .../APIPermissionsPatcher.js | 27 ++ .../components/actions/delete-all-action.vue | 41 ++ src/app/components/actions/index.js | 5 + .../object-list-popup/object-list-popup.vue | 61 +++ .../utils/selection-popup/selection-popup.vue | 154 +++++++ .../one-plus-many-table-cell.vue | 41 ++ src/app/composables/useDummy.js | 58 +++ src/app/css/main.scss | 80 ++++ src/app/css/objects/objects.scss | 164 +++++++ src/app/css/objects/table-page.scss | 60 +++ src/app/locale/en/en.js | 428 ++++++++++++++++- src/app/locale/i18n.js | 2 +- src/app/locale/ru/ru.js | 435 +++++++++++++++++- src/app/locale/ua/ua.js | 428 ++++++++++++++++- .../__tests__/accessControlMixin.spec.js | 15 + .../accessControlMixin/accessControlMixin.js | 24 + .../__tests__/baseObjectMixin.spec.js | 34 ++ .../baseObjectMixin/baseObjectMixin.js | 76 +++ .../baseTableMixin/baseTableMixin.js | 48 ++ .../deleteMixin/tableDeleteMixin.js | 10 + .../baseTableMixin/itemLinkMixin.js | 13 + .../baseTableMixin/tableActionsMixin.js | 39 ++ .../headlineNavMixin/headlineNavMixin.js | 25 + .../openedObjectValidationMixin.js | 19 + src/app/mixins/baseMixins/readme.md | 4 + .../resetOnDestroyMixin.js | 15 + .../objectTableAccessControlMixin.js | 10 + .../objectTableMixin/tableComponentMixin.js | 112 +++++ .../openedObjectAccessControlMixin.js | 11 + .../openedObjectMixin/nestedObjectMixin.js | 54 +++ .../openedObjectMixin/openedObjectMixin.js | 78 ++++ .../openedObjectTabAccessControlMixin.js | 11 + .../openedTabComponentMixin.js | 44 ++ .../openedObjectTableTabMixin.js | 141 ++++++ .../permissionsTabMixin.js | 146 ++++++ .../permissionsTabRolePopupMixin.js | 60 +++ src/app/mixins/objectPagesMixins/readme.md | 8 + 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 | 4 + .../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 + src/main.js | 21 +- .../permissions-tab/api/PermissionsAPI.js | 35 ++ .../_internals/permissions-role-column.vue | 30 ++ .../_internals/permissions-role-select.vue | 46 ++ .../components/permissions-tab-role-popup.vue | 46 ++ .../components/permissions-tab.vue | 115 +++++ .../contacts/api/CommunicationTypesAPI.js | 4 +- src/modules/contacts/api/ContactsAPI.js | 4 +- src/modules/contacts/api/LabelsAPI.js | 4 +- src/modules/contacts/api/TimezonesAPI.js | 4 +- src/modules/contacts/api/UsersAPI.js | 4 +- .../contacts/modules/emails/api/EmailsAPI.js | 4 +- .../modules/messaging/api/MessagingAPI.js | 4 +- .../modules/permissions/api/PermissionsAPI.js | 4 +- .../modules/permissions/api/RolesAPI.js | 4 +- .../contacts/modules/phones/api/PhonesAPI.js | 4 +- .../modules/timeline/api/TimelineAPI.js | 2 +- .../timeline/modules/calls/api/HistoryAPI.js | 4 +- .../modules/chats/api/MessageHistoryAPI.js | 2 +- .../modules/variables/api/VariablesAPI.js | 4 +- .../modules/cgroups/api/contact-groups.js | 141 ++++++ .../cgroups/assets/adm-agent-history-dark.svg | 15 + .../assets/adm-agent-history-light.svg | 15 + .../opened-contact-groups-general.vue | 38 ++ .../components/opened-contact-groups.vue | 110 +++++ .../cgroups/components/the-contact-groups.vue | 235 ++++++++++ .../ContactGroupsRouteNames.enum.js | 6 + .../modules/cgroups/router/contact-groups.js | 39 ++ .../cgroups/store/_internals/headers.js | 16 + .../modules/cgroups/store/contact-groups.js | 46 ++ .../modules/contact-groups/store/cgroups.js | 10 + .../modules/objects/api/objects.js | 78 ++++ .../store/_internals/enums/AccessMode.enum.js | 5 + .../modules/objects/store/objects.js | 15 + .../permissions/modules/roles/api/roles.js | 192 ++++++++ .../modules/roles/store/_internals/headers.js | 16 + .../permissions/modules/roles/store/roles.js | 113 +++++ src/modules/permissions/store/permissions.js | 10 + ....timestamp-1716888890686-6c8de6262eaca.mjs | 75 --- 91 files changed, 5297 insertions(+), 275 deletions(-) create mode 100644 src/app/api/PermissionsAPIService/APIPermissionsGetter.js create mode 100644 src/app/api/PermissionsAPIService/APIPermissionsPatcher.js create mode 100644 src/app/components/actions/delete-all-action.vue create mode 100644 src/app/components/actions/index.js create mode 100644 src/app/components/utils/object-list-popup/object-list-popup.vue create mode 100644 src/app/components/utils/selection-popup/selection-popup.vue create mode 100644 src/app/components/utils/table-cell/one-plus-many-table-cell/one-plus-many-table-cell.vue create mode 100644 src/app/composables/useDummy.js create mode 100644 src/app/css/objects/objects.scss create mode 100644 src/app/css/objects/table-page.scss create mode 100644 src/app/mixins/baseMixins/accessControlMixin/__tests__/accessControlMixin.spec.js create mode 100644 src/app/mixins/baseMixins/accessControlMixin/accessControlMixin.js create mode 100644 src/app/mixins/baseMixins/baseObjectMixin/__tests__/baseObjectMixin.spec.js create mode 100644 src/app/mixins/baseMixins/baseObjectMixin/baseObjectMixin.js create mode 100644 src/app/mixins/baseMixins/baseTableMixin/baseTableMixin.js create mode 100644 src/app/mixins/baseMixins/baseTableMixin/deleteMixin/tableDeleteMixin.js create mode 100644 src/app/mixins/baseMixins/baseTableMixin/itemLinkMixin.js create mode 100644 src/app/mixins/baseMixins/baseTableMixin/tableActionsMixin.js create mode 100644 src/app/mixins/baseMixins/headlineNavMixin/headlineNavMixin.js create mode 100644 src/app/mixins/baseMixins/openedObjectValidationMixin/openedObjectValidationMixin.js create mode 100644 src/app/mixins/baseMixins/readme.md create mode 100644 src/app/mixins/baseMixins/resetOnDestroyMixin/resetOnDestroyMixin.js create mode 100644 src/app/mixins/objectPagesMixins/objectTableMixin/_internals/objectTableAccessControlMixin.js create mode 100644 src/app/mixins/objectPagesMixins/objectTableMixin/tableComponentMixin.js create mode 100644 src/app/mixins/objectPagesMixins/openedObjectMixin/_internals/openedObjectAccessControlMixin.js create mode 100644 src/app/mixins/objectPagesMixins/openedObjectMixin/nestedObjectMixin.js create mode 100644 src/app/mixins/objectPagesMixins/openedObjectMixin/openedObjectMixin.js create mode 100644 src/app/mixins/objectPagesMixins/openedObjectTabMixin/_internals/openedObjectTabAccessControlMixin.js create mode 100644 src/app/mixins/objectPagesMixins/openedObjectTabMixin/openedTabComponentMixin.js create mode 100644 src/app/mixins/objectPagesMixins/openedObjectTableTabMixin/openedObjectTableTabMixin.js create mode 100644 src/app/mixins/objectPagesMixins/permissionsTabMixin/permissionsTabMixin.js create mode 100644 src/app/mixins/objectPagesMixins/permissionsTabMixin/permissionsTabRolePopupMixin.js create mode 100644 src/app/mixins/objectPagesMixins/readme.md 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 create mode 100644 src/modules/_shared/permissions-tab/api/PermissionsAPI.js create mode 100644 src/modules/_shared/permissions-tab/components/_internals/permissions-role-column.vue create mode 100644 src/modules/_shared/permissions-tab/components/_internals/permissions-role-select.vue create mode 100644 src/modules/_shared/permissions-tab/components/permissions-tab-role-popup.vue create mode 100644 src/modules/_shared/permissions-tab/components/permissions-tab.vue create mode 100644 src/modules/lookups/modules/contact-groups/modules/cgroups/api/contact-groups.js create mode 100644 src/modules/lookups/modules/contact-groups/modules/cgroups/assets/adm-agent-history-dark.svg create mode 100644 src/modules/lookups/modules/contact-groups/modules/cgroups/assets/adm-agent-history-light.svg create mode 100644 src/modules/lookups/modules/contact-groups/modules/cgroups/components/opened-contact-groups-general.vue create mode 100644 src/modules/lookups/modules/contact-groups/modules/cgroups/components/opened-contact-groups.vue create mode 100644 src/modules/lookups/modules/contact-groups/modules/cgroups/components/the-contact-groups.vue create mode 100644 src/modules/lookups/modules/contact-groups/modules/cgroups/router/_internals/ContactGroupsRouteNames.enum.js create mode 100644 src/modules/lookups/modules/contact-groups/modules/cgroups/router/contact-groups.js create mode 100644 src/modules/lookups/modules/contact-groups/modules/cgroups/store/_internals/headers.js create mode 100644 src/modules/lookups/modules/contact-groups/modules/cgroups/store/contact-groups.js create mode 100644 src/modules/lookups/modules/contact-groups/store/cgroups.js create mode 100644 src/modules/permissions/modules/objects/api/objects.js create mode 100644 src/modules/permissions/modules/objects/store/_internals/enums/AccessMode.enum.js create mode 100644 src/modules/permissions/modules/objects/store/objects.js create mode 100644 src/modules/permissions/modules/roles/api/roles.js create mode 100644 src/modules/permissions/modules/roles/store/_internals/headers.js create mode 100644 src/modules/permissions/modules/roles/store/roles.js create mode 100644 src/modules/permissions/store/permissions.js delete mode 100644 vite.config.js.timestamp-1716888890686-6c8de6262eaca.mjs diff --git a/package-lock.json b/package-lock.json index c0b07639..9ec5a695 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", "@vueuse/core": "^10.10.0", - "@webitel/ui-sdk": "^24.8.11", + "@webitel/ui-sdk": "^24.10.7", "axios": "^1.7.2", "deep-equal": "^2.2.1", "dompurify": "^3.1.2", @@ -23,7 +23,7 @@ "vue-i18n": "^9.13.1", "vue-router": "^4.3.2", "vuex": "^4.1.0", - "webitel-sdk": "^24.4.16" + "webitel-sdk": "^24.8.1" }, "devDependencies": { "@vitejs/plugin-vue": "5.0.4", @@ -66,9 +66,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "engines": { "node": ">=6.9.0" } @@ -111,11 +111,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dependencies": { - "@babel/types": "^7.25.0", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -226,12 +226,12 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "dependencies": { "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/types": "^7.25.6" }, "engines": { "node": ">=6.9.0" @@ -321,11 +321,11 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.25.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -335,9 +335,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -346,9 +346,9 @@ } }, "node_modules/@babel/standalone": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.25.3.tgz", - "integrity": "sha512-uR+EoBqIIIvKGCG7fOj7HKupu3zVObiMfdEwoPZfVCPpcWJaZ1PkshaP5/6cl6BKAm1Zcv25O1rf+uoQ7V8nqA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.25.6.tgz", + "integrity": "sha512-Kf2ZcZVqsKbtYhlA7sP0z5A3q5hmCVYMKMWRWNK/5OVwHIve3JY1djVRmIVAx8FMueLIfZGKQDIILK2w8zO4mg==", "engines": { "node": ">=6.9.0" } @@ -367,15 +367,15 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -392,9 +392,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dependencies": { "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", @@ -847,34 +847,34 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.6.tgz", - "integrity": "sha512-Vkvsw6EcpMHjvZZdMkSY+djMGFbt7CRssW99Ne8tar2WLnZ/l3dbxeTShbLQj+/s35h+Qb4cmnob+EzwtjrXGQ==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz", + "integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==", "dependencies": { - "@floating-ui/utils": "^0.2.6" + "@floating-ui/utils": "^0.2.7" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.9.tgz", - "integrity": "sha512-zB1PcI350t4tkm3rvUhSRKa9sT7vH5CrAbQxW+VaPYJXKAO0gsg4CTueL+6Ajp7XzAQC8CW4Jj1Wgqc0sB6oUQ==", + "version": "1.6.10", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.10.tgz", + "integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==", "dependencies": { "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.6" + "@floating-ui/utils": "^0.2.7" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.6.tgz", - "integrity": "sha512-0KI3zGxIUs1KDR/pjQPdJH4Z8nGBm0yJ5WRoRfdw1Kzeh45jkIfA0rmD0kBF6fKHH+xaH7g8y4jIXyAV5MGK3g==" + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz", + "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==" }, "node_modules/@floating-ui/vue": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.3.tgz", - "integrity": "sha512-1YlBnfamNCwT85JZJ2HH9t5DO6DAArhQHAiDQW5l+d9MzlN8atywXw2wI9vitPkoZZSurrDJ2viMzrbhPTmLmQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.4.tgz", + "integrity": "sha512-ammH7T3vyCx7pmm9OF19Wc42zrGnUw0QvLoidgypWsCLJMtGXEwY7paYIHO+K+oLC3mbWpzIHzeTVienYenlNg==", "dependencies": { "@floating-ui/dom": "^1.0.0", - "@floating-ui/utils": "^0.2.6", + "@floating-ui/utils": "^0.2.7", "vue-demi": ">=0.13.0" } }, @@ -1162,29 +1162,29 @@ } }, "node_modules/@nuxt/kit": { - "version": "3.12.4", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.12.4.tgz", - "integrity": "sha512-aNRD1ylzijY0oYolldNcZJXVyxdGzNTl+Xd0UYyFQCu9f4wqUZqQ9l+b7arCEzchr96pMK0xdpvLcS3xo1wDcw==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.13.1.tgz", + "integrity": "sha512-FkUL349lp/3nVfTIyws4UDJ3d2jyv5Pk1DC1HQUCOkSloYYMdbRcQAUcb4fe2TCLNWvHM+FhU8jnzGTzjALZYA==", "dependencies": { - "@nuxt/schema": "3.12.4", - "c12": "^1.11.1", + "@nuxt/schema": "3.13.1", + "c12": "^1.11.2", "consola": "^3.2.3", "defu": "^6.1.4", "destr": "^2.0.3", "globby": "^14.0.2", "hash-sum": "^2.0.0", - "ignore": "^5.3.1", + "ignore": "^5.3.2", "jiti": "^1.21.6", "klona": "^2.0.6", "knitwork": "^1.1.0", "mlly": "^1.7.1", "pathe": "^1.1.2", - "pkg-types": "^1.1.3", + "pkg-types": "^1.2.0", "scule": "^1.3.0", "semver": "^7.6.3", "ufo": "^1.5.4", "unctx": "^2.3.1", - "unimport": "^3.9.0", + "unimport": "^3.11.1", "untyped": "^1.4.2" }, "engines": { @@ -1192,21 +1192,21 @@ } }, "node_modules/@nuxt/schema": { - "version": "3.12.4", - "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.12.4.tgz", - "integrity": "sha512-H7FwBV4ChssMaeiLyPdVLOLUa0326ebp3pNbJfGgFt7rSoKh1MmgjorecA8JMxOQZziy3w6EELf4+5cgLh/F1w==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.13.1.tgz", + "integrity": "sha512-ishbhzVGspjshG9AG0hYnKYY6LWXzCtua7OXV7C/DQ2yA7rRcy1xHpzKZUDbIRyxCHHCAcBd8jfHEUmEuhEPrA==", "dependencies": { "compatx": "^0.1.8", "consola": "^3.2.3", "defu": "^6.1.4", "hookable": "^5.5.3", "pathe": "^1.1.2", - "pkg-types": "^1.1.3", + "pkg-types": "^1.2.0", "scule": "^1.3.0", "std-env": "^3.7.0", "ufo": "^1.5.4", "uncrypto": "^0.1.3", - "unimport": "^3.9.0", + "unimport": "^3.11.1", "untyped": "^1.4.2" }, "engines": { @@ -1902,12 +1902,12 @@ } }, "node_modules/@vueuse/components": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.11.0.tgz", - "integrity": "sha512-ZvLZI23d5ZAtva5fGyYh/jQtZO8l+zJ5tAXyYNqHJZkq1o5yWyqZhENvSv5mfDmN5IuAOp4tq02mRmX/ipFGcg==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.11.1.tgz", + "integrity": "sha512-ThcreQCX/eq61sLkLKjigD4PQvs3Wy4zglICvQH9tP6xl87y5KsQEoizn6OI+R3hrOgwQHLJe7Y0wLLh3fBKcg==", "dependencies": { - "@vueuse/core": "10.11.0", - "@vueuse/shared": "10.11.0", + "@vueuse/core": "10.11.1", + "@vueuse/shared": "10.11.1", "vue-demi": ">=0.14.8" } }, @@ -1937,13 +1937,13 @@ } }, "node_modules/@vueuse/core": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.0.tgz", - "integrity": "sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", "dependencies": { "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.11.0", - "@vueuse/shared": "10.11.0", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", "vue-demi": ">=0.14.8" }, "funding": { @@ -1976,17 +1976,17 @@ } }, "node_modules/@vueuse/metadata": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.0.tgz", - "integrity": "sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.0.tgz", - "integrity": "sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", "dependencies": { "vue-demi": ">=0.14.8" }, @@ -2020,9 +2020,9 @@ } }, "node_modules/@webitel/ui-sdk": { - "version": "24.8.11", - "resolved": "https://registry.npmjs.org/@webitel/ui-sdk/-/ui-sdk-24.8.11.tgz", - "integrity": "sha512-0ip02vONb+JOe2lHzku6i+jZXGL8c6MnYHdaQvDye1EuxVYy0aUOATrHGFv886JfuK46vSb09fszyyMvECT5UQ==", + "version": "24.10.7", + "resolved": "https://registry.npmjs.org/@webitel/ui-sdk/-/ui-sdk-24.10.7.tgz", + "integrity": "sha512-bNu7BmyWYqvYEJf+wmigdv3MbL3tJZH4V85mbto6mssEVyTSth2/J8xlciwCRlbJcUPx9HJ0/tk0W20h/TO5Dw==", "dependencies": { "@floating-ui/vue": "^1.0.1", "@morev/vue-transitions": "^3.0.2", @@ -2041,7 +2041,7 @@ "jszip": "^3.5.0", "jszip-utils": "^0.1.0", "lodash": "^4.17.21", - "mitt": "^3.0.1", + "mitt": "3.0.1", "path-browserify": "^1.0.1", "plyr": "3.6.3", "query-string": "^8.1.0", @@ -2050,7 +2050,7 @@ "vue-multiselect": "^3.0.0-beta.3", "vue-observe-visibility": "^2.0.0-alpha.1", "vue-router": "^4.1.6", - "webitel-sdk": "^24.2.16", + "webitel-sdk": "^24.8.1", "xlsx": "^0.18.5" } }, @@ -2608,9 +2608,9 @@ "dev": true }, "node_modules/c12": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/c12/-/c12-1.11.1.tgz", - "integrity": "sha512-KDU0TvSvVdaYcQKQ6iPHATGz/7p/KiVjPg4vQrB6Jg/wX9R0yl5RZxWm9IoZqaIHD2+6PZd81+KMGwRr/lRIUg==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/c12/-/c12-1.11.2.tgz", + "integrity": "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew==", "dependencies": { "chokidar": "^3.6.0", "confbox": "^0.1.7", @@ -2622,7 +2622,7 @@ "ohash": "^1.1.3", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", - "pkg-types": "^1.1.1", + "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { @@ -2700,9 +2700,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001650", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001650.tgz", - "integrity": "sha512-fgEc7hP/LB7iicdXHUI9VsBsMZmUmlVJeQP2qqQW+3lkqVhbmjEU8zp+h5stWeilX+G7uXuIUIIlWlDw9jdt8g==", + "version": "1.0.30001660", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", + "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", "funding": [ { "type": "opencollective", @@ -3030,9 +3030,9 @@ } }, "node_modules/core-js": { - "version": "3.38.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.0.tgz", - "integrity": "sha512-XPpwqEodRljce9KswjZShh95qJ1URisBeKCjUdq27YdenkslVe7OO0ZJhlYXAChW7OhXaRLl8AAba7IBfoIHug==", + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz", + "integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -3370,9 +3370,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -3706,9 +3706,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", - "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==" + "version": "1.5.22", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.22.tgz", + "integrity": "sha512-tKYm5YHPU1djz0O+CGJ+oJIvimtsCcwR2Z9w7Skh08lUdyzXY5djods3q+z2JkWdb7tCcmM//eVavSRAiaPRNg==" }, "node_modules/elliptic": { "version": "6.5.5", @@ -3937,9 +3937,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "engines": { "node": ">=6" } @@ -5139,9 +5139,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "engines": { "node": ">= 4" } @@ -6677,16 +6677,16 @@ } }, "node_modules/nypm": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.9.tgz", - "integrity": "sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.11.tgz", + "integrity": "sha512-E5GqaAYSnbb6n1qZyik2wjPDZON43FqOJO59+3OkWrnmQtjggrMOVnsyzfjxp/tS6nlYJBA4zRA5jSM2YaadMg==", "dependencies": { "citty": "^0.1.6", "consola": "^3.2.3", "execa": "^8.0.1", "pathe": "^1.1.2", - "pkg-types": "^1.1.1", - "ufo": "^1.5.3" + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" @@ -7125,9 +7125,9 @@ } }, "node_modules/pkg-types": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz", - "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz", + "integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==", "dependencies": { "confbox": "^0.1.7", "mlly": "^1.7.1", @@ -8009,9 +8009,9 @@ "dev": true }, "node_modules/sortablejs": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz", - "integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==" + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.3.tgz", + "integrity": "sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==" }, "node_modules/source-map": { "version": "0.5.7", @@ -9011,9 +9011,9 @@ } }, "node_modules/type-fest": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.23.0.tgz", - "integrity": "sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", "engines": { "node": ">=16" }, @@ -9183,9 +9183,9 @@ } }, "node_modules/unimport": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.10.0.tgz", - "integrity": "sha512-/UvKRfWx3mNDWwWQhR62HsoM3wxHwYdTq8ellZzMOHnnw4Dp8tovgthyW7DjTrbjDL+i4idOp06voz2VKlvrLw==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.12.0.tgz", + "integrity": "sha512-5y8dSvNvyevsnw4TBQkIQR1Rjdbb+XjVSwQwxltpnVZrStBvvPkMPcZrh1kg5kY77kpx6+D4Ztd3W6FOBH/y2Q==", "dependencies": { "@rollup/pluginutils": "^5.1.0", "acorn": "^8.12.1", @@ -9196,10 +9196,10 @@ "magic-string": "^0.30.11", "mlly": "^1.7.1", "pathe": "^1.1.2", - "pkg-types": "^1.1.3", + "pkg-types": "^1.2.0", "scule": "^1.3.0", "strip-literal": "^2.1.0", - "unplugin": "^1.12.0" + "unplugin": "^1.14.1" } }, "node_modules/unimport/node_modules/escape-string-regexp": { @@ -9237,17 +9237,23 @@ } }, "node_modules/unplugin": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.12.0.tgz", - "integrity": "sha512-KeczzHl2sATPQUx1gzo+EnUkmN4VmGBYRRVOZSGvGITE9rGHRDGqft6ONceP3vgXcyJ2XjX5axG5jMWUwNCYLw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.14.1.tgz", + "integrity": "sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==", "dependencies": { "acorn": "^8.12.1", - "chokidar": "^3.6.0", - "webpack-sources": "^3.2.3", "webpack-virtual-modules": "^0.6.2" }, "engines": { "node": ">=14.0.0" + }, + "peerDependencies": { + "webpack-sources": "^3" + }, + "peerDependenciesMeta": { + "webpack-sources": { + "optional": true + } } }, "node_modules/unset-value": { @@ -9764,9 +9770,9 @@ } }, "node_modules/webitel-sdk": { - "version": "24.4.16", - "resolved": "https://registry.npmjs.org/webitel-sdk/-/webitel-sdk-24.4.16.tgz", - "integrity": "sha512-evkakLCmVaRa+AwJ5OvXq27En19vRh5t7ibahtyXDUDGQk2IfHzZ4El/4AVm/RFTow5Una6aGhyhL7Ln4CgDZw==", + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/webitel-sdk/-/webitel-sdk-24.8.1.tgz", + "integrity": "sha512-abdjKyIX7OgykvRJwAAJiIZAyvCExGa9aA9obtXbSfQtMvrF9mTXVQFYv5STYWLQGgpU04Q8BSLp2nj+GSFOZQ==", "dependencies": { "@types/webrtc": "~0.0.41", "deep-copy": "1.4.2", @@ -9829,14 +9835,6 @@ "node": ">=4" } }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", diff --git a/package.json b/package.json index 339b6e6c..0c82a583 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", "@vueuse/core": "^10.10.0", - "@webitel/ui-sdk": "^24.8.11", + "@webitel/ui-sdk": "^24.10.7", "axios": "^1.7.2", "deep-equal": "^2.2.1", "dompurify": "^3.1.2", @@ -26,7 +26,7 @@ "vue-i18n": "^9.13.1", "vue-router": "^4.3.2", "vuex": "^4.1.0", - "webitel-sdk": "^24.4.16" + "webitel-sdk": "^24.8.1" }, "devDependencies": { "@vitejs/plugin-vue": "5.0.4", diff --git a/src/app/api/PermissionsAPIService/APIPermissionsGetter.js b/src/app/api/PermissionsAPIService/APIPermissionsGetter.js new file mode 100644 index 00000000..90896020 --- /dev/null +++ b/src/app/api/PermissionsAPIService/APIPermissionsGetter.js @@ -0,0 +1,84 @@ +import { getDefaultGetListResponse, getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults/index.js'; +import applyTransform, { + camelToSnake, + generateUrl, + merge, + mergeEach, + notify, + sanitize, + snakeToCamel, + starToSearch, +} from '@webitel/ui-sdk/src/api/transformers/index.js'; +import instance from '../instance'; + +export default class APIPermissionsGetter { + nestedUrl = 'acl'; + + constructor(url) { + this.baseUrl = url; + + this.listGetter = async ({ parentId, ...params }) => { + const fieldsToSend = ['page', 'size', 'q', 'sort', 'fields', 'id']; + + const defaultObject = { + user: false, + }; + + const url = applyTransform(params, [ + merge(getDefaultGetParams()), + starToSearch('search'), + (params) => ({ + ...params, + q: params.search, + }), + sanitize(fieldsToSend), + camelToSnake(), + generateUrl(`${this.baseUrl}/${parentId}/${this.nestedUrl}`), + ]); + try { + const response = await instance.get(url); + const { items, next } = applyTransform(response.data, [ + snakeToCamel(), + merge(getDefaultGetListResponse()), + ]); + return { + items: applyTransform(items, [ + mergeEach(defaultObject), + APIPermissionsGetter.handlePermissionsList, + ]), + next, + }; + } catch (err) { + throw applyTransform(err, [notify]); + } + }; + } + + static handlePermissionsList(items) { + return items.map((item) => ({ + ...item, + access: { + x: { + id: (item.granted.match(/x/g) || []).length + 1, + rule: 'x'.repeat((item.granted.match(/x/g) || []).length), + }, + r: { + id: (item.granted.match(/r/g) || []).length + 1, + rule: 'r'.repeat((item.granted.match(/r/g) || []).length), + }, + w: { + id: (item.granted.match(/w/g) || []).length + 1, + rule: 'w'.repeat((item.granted.match(/w/g) || []).length), + }, + d: { + id: (item.granted.match(/d/g) || []).length + 1, + rule: 'd'.repeat((item.granted.match(/d/g) || []).length), + }, + }, + })); + } + + async getList(params) { + return this.listGetter(params); + } +} diff --git a/src/app/api/PermissionsAPIService/APIPermissionsPatcher.js b/src/app/api/PermissionsAPIService/APIPermissionsPatcher.js new file mode 100644 index 00000000..56c49413 --- /dev/null +++ b/src/app/api/PermissionsAPIService/APIPermissionsPatcher.js @@ -0,0 +1,27 @@ +import applyTransform, { + camelToSnake, + notify, + snakeToCamel, +} from '@webitel/ui-sdk/src/api/transformers/index.js'; +import instance from '../instance'; + +export default class APIPermissionsPatcher { + constructor(baseUrl) { + this.baseUrl = baseUrl; + this.patcher = async ({ changes, id }) => { + const afterUrl = 'acl'; + const body = applyTransform(changes, [camelToSnake()]); + const url = `${baseUrl}/${id}/${afterUrl}`; + try { + const response = await instance.patch(url, body); + return applyTransform(response.data, [snakeToCamel()]); + } catch (err) { + throw applyTransform(err, [notify]); + } + }; + } + + async patchItem({ id, changes }) { + return this.patcher({ id, changes }); + } +} diff --git a/src/app/components/actions/delete-all-action.vue b/src/app/components/actions/delete-all-action.vue new file mode 100644 index 00000000..fedc2791 --- /dev/null +++ b/src/app/components/actions/delete-all-action.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/app/components/actions/index.js b/src/app/components/actions/index.js new file mode 100644 index 00000000..6f6968b7 --- /dev/null +++ b/src/app/components/actions/index.js @@ -0,0 +1,5 @@ +import DeleteAllAction from './delete-all-action.vue'; + +const actions = [DeleteAllAction]; + +export default actions; diff --git a/src/app/components/utils/object-list-popup/object-list-popup.vue b/src/app/components/utils/object-list-popup/object-list-popup.vue new file mode 100644 index 00000000..ee7f7c4d --- /dev/null +++ b/src/app/components/utils/object-list-popup/object-list-popup.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/app/components/utils/selection-popup/selection-popup.vue b/src/app/components/utils/selection-popup/selection-popup.vue new file mode 100644 index 00000000..6a2dc2cc --- /dev/null +++ b/src/app/components/utils/selection-popup/selection-popup.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/src/app/components/utils/table-cell/one-plus-many-table-cell/one-plus-many-table-cell.vue b/src/app/components/utils/table-cell/one-plus-many-table-cell/one-plus-many-table-cell.vue new file mode 100644 index 00000000..0f0674bd --- /dev/null +++ b/src/app/components/utils/table-cell/one-plus-many-table-cell/one-plus-many-table-cell.vue @@ -0,0 +1,41 @@ + + + + + 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/css/main.scss b/src/app/css/main.scss index 54ea5552..039c7aa0 100644 --- a/src/app/css/main.scss +++ b/src/app/css/main.scss @@ -1,7 +1,10 @@ @import '@webitel/ui-sdk/src/css/main'; +@import './objects/table-page'; +@import './objects/objects'; //disables scrollbar on 100vh auth on small laptops body { + @extend %wt-scrollbar; @extend %typo-body-1; display: flex; min-width: 100%; @@ -10,6 +13,11 @@ body { background: var(--wt-page-wrapper-background-color); } +#app { + min-width: 100%; + min-height: 100%; +} + .wt-page-wrapper { flex-grow: 1; display: flex; @@ -26,6 +34,7 @@ body { .table-wrapper { flex-grow: 1; display: flex; + padding-top: var(--spacing-xs); flex-direction: column; gap: var(--spacing-xs); max-height: 100%; @@ -71,3 +80,74 @@ a { text-decoration: none; color: #000; } + +.mb-0 { + margin-bottom: 0; +} + +// helper class +.no-padding { + padding: 0; +} + +.box-shadow { + //box-shadow: 0px 5px 6px rgba(0, 0, 0, 0.2), 0px 3px 16px rgba(0, 0, 0, 0.12), 0px 9px 12px rgba(0, 0, 0, 0.14); + box-shadow: rgba(0, 0, 0, 0.08) 0 8px 18px; +} + +.icon-action { + cursor: pointer; + + &:before { + transition: var(--transition); + } + + &:hover:before { + color: #000; + } +} + +// helper underline class +.border-underline { + position: relative; + + &:before { + position: absolute; + right: 0; + bottom: 6px; + left: 0; + height: 1px; + content: ''; + } +} + +$scrollbar-bg-color: #EAEAEA; +$srollbar-thumb-color: var(--primary-color); +$srollbar-border-radius: 4px; + +.scrollbar { + // scrollbar itself + &::-webkit-scrollbar { + width: 4px; + height: 6px; + cursor: pointer; + background-color: $scrollbar-bg-color; + } + + // scrollable element + &::-webkit-scrollbar-thumb { + width: 4px; + height: 6px; + border-radius: $srollbar-border-radius; + background-color: $srollbar-thumb-color; + } +} + +.hidden { + pointer-events: none; + opacity: 0; +} + +.value-pair-wrap { + position: relative; +} diff --git a/src/app/css/objects/objects.scss b/src/app/css/objects/objects.scss new file mode 100644 index 00000000..d106d17b --- /dev/null +++ b/src/app/css/objects/objects.scss @@ -0,0 +1,164 @@ +// ----- new styles + +.tabs-page-wrapper { + display: flex; + flex-direction: column; + width: 100%; +} + +.wt-page-wrapper { + width: 100%; + + .main-container { + display: flex; + flex-direction: column; + width: 100%; + + & > section { + display: flex; + flex-direction: column; + height: 100%; + } + } +} + +// ----- old styles + +// inner content section +.object-content { + display: flex; + flex-direction: column; + box-sizing: border-box; + width: 100%; + min-height: 65vh; + padding: 30px; + border-radius: var(--border-radius); + background: #fff; + + .pagination { + margin: auto 0 0 auto; + padding-top: 28px; + } +} + +.main-section__wrapper { + display: flex; + flex-direction: column; + width: 100%; +} + +.content-wrapper { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.btn-controls { + .btn { + margin-left: 28px; + } +} + +// SECTION HEADING +.content-header { + position: relative; + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: space-between; + margin: var(--spacing-sm) 0; + + .content-title { + @extend %typo-heading-3; + + + .hint { + top: -2px; + } + } + + // WRAP FOR SEARCH INPUT + .content-header__actions-wrap { + display: flex; + align-items: center; + gap: var(--table-actions-icon-gap); + } +} + +// GRID TEMPLATE FOR MODULE INPUTS +.object-input-grid { + display: grid; + align-items: flex-start; + grid-template-columns: 1fr 1fr; + grid-column-gap: 20px; + grid-row-gap: 16px; + + &__1-col { + grid-template-columns: 1fr; + } + + &__w50 { + width: 50%; + } +} + +.grid-w100 { + .object-input-grid { + grid-template-columns: 1fr; + } +} + +.grid-w50 { + .object-input-grid { + grid-template-columns: 50%; + } +} + +.value-pair { + display: grid; + align-items: center; + margin-bottom: 20px; + grid-template-columns: 1fr 1fr 24px; + grid-gap: 20px; +} + +.module-new .object-input-grid .select-preview { + margin-bottom: 20px; +} + +.module-new { + .new_w50 { + width: 50%; + + .form-input { + margin: 8px 0; + } + } + + &.object-with-tabs { + padding-top: 28px; + padding-right: 0; + padding-left: 0; + + .tabs-inner-component { + padding: 0 58px 38px 58px; + } + } +} + +.opened-calendar-work-week { + .vuetable tr { + border-bottom: transparent; + + &.day-start { + //border-top: 1px solid rgba(0, 0, 0, 0.1); + } + + &:last-child { + //border-bottom: 1px solid rgba(0, 0, 0, 0.1); + } + } +} + +.dummy-wrapper { + height: 100%; +} diff --git a/src/app/css/objects/table-page.scss b/src/app/css/objects/table-page.scss new file mode 100644 index 00000000..5b9e2246 --- /dev/null +++ b/src/app/css/objects/table-page.scss @@ -0,0 +1,60 @@ + +.actions-panel-wrapper { + display: flex; + + .filter-wrap { + flex: 1; + } + + .wt-table-actions { + height: fit-content; + margin-top: 24px; + } +} + +.table-wrapper { + display: flex; + flex: 1 1 100%; + flex-direction: column; + justify-content: space-between; + box-sizing: border-box; + width: 100%; + + .wt-table { + flex-grow: 1; + } + + .wt-pagination { + margin-top: 20px; + margin-left: auto; + } + + .table-action { + margin-left: 10px; + } + + /* + wrapper class for table content overflow: visible preserving table scroll itself + .table-wrapper>.table-wrapper__visible-scroll-wrapper>.wt-table + */ + .table-wrapper__visible-scroll-wrapper { + overflow: visible; + height: 100%; + + .wt-table { + overflow: visible; + } + } + + .table-wrapper__scroll-wrapper { + @extend %wt-scrollbar; + + overflow: auto; + height: 100%; + min-height: 0; + + .wt-table { + overflow: visible; + } + } +} diff --git a/src/app/locale/en/en.js b/src/app/locale/en/en.js index b313ea6d..b5d12f69 100644 --- a/src/app/locale/en/en.js +++ b/src/app/locale/en/en.js @@ -92,13 +92,425 @@ export default { }, configuration: { [CrmConfigurationSections.LOOKUPS]: 'Lookups', - [CrmConfigurationSections.SLA]: 'SLA', - [CrmConfigurationSections.SERVICE_CATALOG]: 'Service catalog', - [CrmConfigurationSections.PRIORITIES]: 'Priorities', - [CrmConfigurationSections.STATUSES]: 'Statuses', - [CrmConfigurationSections.CLOSURE_REASONS]: 'Closure reasons', [CrmConfigurationSections.CONTACT_GROUPS]: 'Contact groups', - [CrmConfigurationSections.CASE_SOURCES]: 'Case sources', - [CrmConfigurationSections.CUSTOM_LOOKUPS]: 'Custom lookups', - } + }, + + validation: { + email: 'Should look email-like', + required: 'This field is required', + sameAs: 'Incorrect password', + gatewayHostValidator: 'Should look like IPv4 or FQDN', + ipValidator: 'Should look like IPv4', + macValidator: 'Should look like MAC', + minValue: 'Value should be at least', + maxValue: 'Value should be not much', + numeric: 'Should be numeric', + requiredArrayValue: 'Array should not be empty', + isPositiveValue: 'Value should be positive number', + cron: 'Invalid cron expression', + }, + + objects: { + all: 'All {entity}', + administration: 'Administration', + general: 'General', + new: 'New', + add: 'Add', + create: 'Create', + read: 'Read', + edit: 'Edit', + delete: 'Delete', + save: 'Save', + saved: 'Saved', + saveAs: 'Save as', + close: 'Close', + next: 'Next', + back: 'Back', + change: 'Change', + name: 'Name', + title: 'Title', + description: 'Description', + user: 'User', + users: 'Users', + action: 'Action', + allow: 'Allow', + password: 'Password', + history: 'History', + copy: 'Copy', + copied: 'Copied to clipboard!', + createdAt: 'Created', + createdBy: 'Created by', + importCSV: 'Import CSV', + online: 'Online', + offline: 'Offline', + on: 'On', + off: 'Off', + ok: 'Ok', + enabled: 'Enabled', + status: 'Status', + from: 'From', + to: 'To', + importJSON: 'Import JSON', + addNew: 'Add new', + provider: 'Provider', + default: 'Default', + service: 'Service', + tts: 'Text-to-Speech', + stt: 'Speech-to-Text', + key: 'Key', + ID: 'ID', + email: 'Email address', + updatedAt: 'Modified', + emptyWorkspace: 'There are no records yet', + emptyResultSearch: 'Your search yielded no results', + deleteConfirmation: { + title: 'Confirm deletion', + askingAlert: + 'Are you sure you want to delete {count} item? | Are you sure you want to delete {count} items?', + undoneActionAlert: 'This action cannot be undone.', + deleteAll: 'ALL', + }, + generalInfo: 'General info', + objectHeader: {}, + permissions: { + permissions: 'Permissions', + permissionsRole: 'Role | Roles', + allRoles: 'All roles', + searchPlaceholder: 'role ..', + roles: { + permissions: { + permissions: 'Role permission | Role permissions', + add: 'Add', + delete: 'Delete', + read: 'Select', + write: 'Update', + eavesdropCall: 'Eavesdrop call', + playbackRecordFile: 'Playback record file', + exportDataGrid: 'Export data grid', + viewCdrPhoneNumbers: 'View CDR phone numbers', + manageUserRoles: 'Manage user roles', + manageUserLicense: 'Manage user license', + changeUserPassword: 'Change user password', + systemSetting: 'Manage system configurations', + addDescription: 'Grants permission to create any objects', + deleteDescription: 'Grants permission to remove any objects', + readDescription: 'Grants permission to select any objects', + writeDescription: 'Grants permission to modify any objects', + eavesdropCallDescription: 'Grants permission to eavesdrop on an active conversation', + playbackRecordFileDescription: 'Grants permission to playback call-record files', + exportDataGridDescription: 'Grants permission to export data grid to a file', + viewCdrPhoneNumbersDescription: 'View CDR phone numbers', + manageUserRolesDescription: 'Grants permission to manage a set of user roles', + manageUserLicenseDescription: 'Grants permission to manage a set of user license', + changeUserPasswordDescription: 'Grants permission to change the users password', + systemSettingDescription: + 'Grants permission to access and manage the Configuration section', + schemeVariables: 'Manage global variables', + schemeVariablesDescription: + 'Grants permission to access and manage the Global variables section', + }, + addPermission: 'Add role permission', + usage: 'Usage', + applicationsAccess: { + applicationsAccess: 'Application access | Applications access', + access: 'Access', + }, + }, + object: { + object: 'Objects', + allObjects: 'All objects', + ObAC: 'Managed by operations', + RbAC: 'Managed by records', + operations: 'Operations', + searchPlaceholder: 'object ..', + newPermissionRole: 'New grantee', + rbacDefault: 'Default Record based Access', + grantor: 'Grantor', + grantee: 'Grantee', + create: 'Create', + delete: 'Delete', + accessMode: { + 1: 'Forbidden', + 2: 'Allow', + 3: 'Allow with delegation', + }, + }, + }, + routing: { + routing: 'Routing', + ip: 'IP', + protocol: 'Protocol', + port: 'Port', + configuration: 'Configuration', + schema: 'Flow schema', + json: 'JSON', + + flow: { + flow: 'Flow schema | Flow schemas', + allFlowSchemas: 'All flow schemas', + createFlowSelectionPopup: 'Create new flow', + createNew: 'Create new?', + editor: 'Editor', + diagram: { + diagram: 'Diagram', + description: 'Create new flow using Webitel Flow Diagram tool', + }, + code: { + code: 'Code', + description: 'Create new flow using JSON-schema in code editor', + }, + askingAlert: 'Do you want to save the changes you made to "{name}" ?', + }, + + dialplan: { + dialplan: 'Dialplans', + dialplanRules: 'Dialplan rules', + pattern: 'Destination number', + dialplanRule: 'Dialplan rule', + position: 'Position', + }, + + chatplan: { + chatplan: 'Chatplan | Chatplans', + allChatplans: 'All chatplans', + }, + + gateways: { + gateways: 'Gateway | Gateways', + allGateways: 'All gateways', + trunkingGateway: 'Trunking gateway', + registerGateway: 'Register gateway', + stateSuccess: 'Success', + stateFailed: 'Failed', + stateProgress: 'In progress', + stateNonreg: 'Not registered', + proxy: 'Proxy', + searchPlaceholder: 'gateway search...', + host: 'Host', + trunkingACLTitle: 'Access Control', + trunkingACL: 'Trunking access control list', + hostnameTrunking: 'Server/Gateway Hostname or IP', + hostnameRegister: 'Registrar/Server/Gateway Hostname or IP', + expire: 'Expire', + authID: 'Username', + account: 'Account', + outboundProxy: 'Outbound Proxy Hostname or IP', + newGateway: 'New gateway', + newGatewayDescription: 'New gateway description', + SIPregistrations: 'SIP Registration', + SIPregistrationsDescription: 'SIP registration description', + SIPtrunking: 'SIP Trunking', + SIPtrunkingDescription: 'SIP trunking description', + }, + chatGateways: { + templates: { + templates: 'Templates', + title: "Workspace member's name", + close: 'Chat complete message', + join: 'Agent joining message', + left: 'Agent disconnection message', + }, + chatGateways: 'Chat gateway | Chat gateways', + allChatGateways: 'All chat gateways', + telegramBot: { + telegramBot: 'Telegram Bot', + }, + telegramApp: { + telegramApp: 'Telegram App', + signedAs: "You're signed as", + joinTelegram: 'Sign in to Telegram', + metadata: { + apiId: 'API id', + apiHash: 'API hash', + }, + }, + infobip: { + infobip: 'Infobip', + }, + messenger: { + meta: 'Meta', + addOrRemovePages: 'Add or remove pages', + accounts: 'Accounts', + metadata: { + clientId: 'App ID', + clientSecret: 'App Secret', + }, + facebook: { + pages: 'Facebook pages', + subscription: 'Webhook subscription', + }, + instagram: { + instagram: 'Instagram', + comments: 'Track comments', + mentions: 'Track mentions', + }, + whatsapp: { + whatsapp: 'Whatsapp', + token: 'Token', + status: 'Status', + number: 'Phone number', + review: 'Review', + }, + }, + viber: { + viber: 'Viber', + style: { + style: 'Style', + btnBackColor: 'Button background color', + btnFontColor: 'Button font color', + }, + }, + webchat: { + webchat: 'Web chat', + copyCode: 'Copy code for site', + copyCodeHint: + 'You must regenerate and reinsert Widget code to website each time after Widget settings modification', + metadata: { + mediaMaxSize: 'Max media size (Mb)', + }, + view: { + view: 'View', + borderRadius: 'Button Shape', + language: 'Language', + position: 'Position', + btnColor: 'Button Color', + logoUrl: 'Logo URL', + logoHint: "Supported logo format is 'JPEG' or 'PNG', 24x24px", + rounded: 'Rounded', + square: 'Square', + right: 'Right', + left: 'Left', + static: 'Static', + }, + recaptcha: { + recaptcha: 'reCAPTCHA', + sitekey: 'Site key', + secret: 'Secret key', + threshold: 'Threshold', + showFlag: 'Show reCAPTCHA badge', + }, + chat: { + chat: 'Chat', + openTimeout: 'Open Timeout', + openTimeoutSec: 'Open Timeout Sec', + }, + appointment: { + appointment: 'Appointment', + days: 'Days', + availableAgents: 'Available agents', + showEmailField: 'Show email field', + showMessageField: 'Show message field', + resultPageText: 'Result page text', + headingText: 'Heading text', + subheadingText: 'Subheading text', + showDefaultHeading: 'Show default heading and subheading on result page', + }, + alternativeChannels: { + alternativeChannels: 'Alternative Channels', + title: 'Messaging channels', + email: 'Email', + whatsapp: 'WhatsApp', + telegram: 'Telegram', + messenger: 'Messenger', + }, + call: { + title: 'Online call', + url: 'WebSocket Endpoint', + }, + }, + customChat: { + customChat: 'Custom Chat', + customChatGateway: 'Custom Chat Gateway', + appSecretHint: 'Altering this field will interrupt the integration', + metadata: { + appSecret: 'App Secret', + callback: 'Callback', + }, + }, + uri: 'URI', + newChatGateway: 'New chat gateway', + metadata: { + apiKey: 'Api Key', + number: 'Number', + baseUrl: 'Base URL', + sendApi: 'Send API', + allowOrigin: 'Allow Origin', + readTimeout: 'Read Timeout (sec)', + writeTimeout: 'Write Timeout (sec)', + handshakeTimeout: 'Handshake Timeout (sec)', + messageSize: 'Message Size max (bytes)', + botName: 'Bot Name', + eventTypes: 'Event Types', + telegramToken: 'Telegram BOT API Token', + messengerApiUrl: 'Messenger API URL', + }, + }, + + callflow: { + callflow: 'Callflow', + }, + }, + pagination: { + rowsPerPage: 'Rows per page', + }, + }, + + lookups: { + lookups: 'Lookups', + contactGroups: { + contactGroups: 'Contact groups', + groups: 'Groups', + crm: 'CRM', + configurations: 'Configurations', + } + }, + + errorPages: { + goToHome: 'Go to Home page', + page403: { + title: 'Access denied', + text: 'Sorry, you have not enough privileges to see this page.', + }, + page404: { + title: "Looks like you're lost", + text: "Sorry, we can't find the page you want.", + }, + }, + + iconHints: { + upload: 'Upload', + reload: 'Refresh table', + edit: 'Edit', + delete: 'Delete', + deleteAll: 'Delete all items', + deleteSelected: 'Delete {count} selected items', + deleteFiltered: 'Delete all filtered items', + generate: 'Generate', + add: 'Add', + history: 'History', + download: 'Download', + downloadAll: 'Download all', + draggable: 'Draggable', + play: 'Play', + pause: 'Pause', + resume: 'Resume', + close: 'Close', + change: 'Change', + volume: 'Volume', + stop: 'Stop', + members: 'Members', + moveUp: 'Move up', + moveDown: 'Move down', + nextPage: 'Next page', + prevPage: 'Previous page', + expand: 'Expand', + collapse: 'Collapse', + }, + + errors: { + invalidJson: 'JSON is invalid', + }, + + reusable: { + }, }; diff --git a/src/app/locale/i18n.js b/src/app/locale/i18n.js index 7a99ca30..b0b835ce 100644 --- a/src/app/locale/i18n.js +++ b/src/app/locale/i18n.js @@ -13,9 +13,9 @@ const messages = { }; export default createI18n({ - legacy: false, locale: 'en', fallbackLocale: 'en', + allowComposition: true, messages, datetimeFormats, }); diff --git a/src/app/locale/ru/ru.js b/src/app/locale/ru/ru.js index 35ba7be9..e29f0d68 100644 --- a/src/app/locale/ru/ru.js +++ b/src/app/locale/ru/ru.js @@ -91,14 +91,431 @@ export default { }, }, configuration: { - [CrmConfigurationSections.LOOKUPS]: 'Справочники', - [CrmConfigurationSections.SLA]: 'SLA', - [CrmConfigurationSections.SERVICE_CATALOG]: 'Каталог сервисов', - [CrmConfigurationSections.PRIORITIES]: 'Приоритеты', - [CrmConfigurationSections.STATUSES]: 'Статусы', - [CrmConfigurationSections.CLOSURE_REASONS]: 'Причины закрытия', [CrmConfigurationSections.CONTACT_GROUPS]: 'Группы контактов', - [CrmConfigurationSections.CASE_SOURCES]: 'Источники обращений', - [CrmConfigurationSections.CUSTOM_LOOKUPS]: 'Пользовательские справочники', - } + }, + + validation: { + email: 'Введите адрес электронной почты', + required: 'Обязательное поле для заполнения', + sameAs: 'Неверный пароль', + gatewayHostValidator: 'Should look like IPv4 or FQDN', + ipValidator: 'Should look like IPv4', + macValidator: 'Should look like MAC', + minValue: 'Значение должно быть не менее', + maxValue: 'Значение должно быть не слишком большим', + numeric: 'Должны быть цифры', + requiredArrayValue: 'Поле не должно быть пустым', + isPositiveValue: 'Значение должно быть больше нуля', + cron: 'Некорректное cron-выражение', + }, + + objects: { + all: 'Все {entity}', + administration: 'Управление', + general: 'Общее', + new: 'Новый объект', + add: 'Добавить', + create: 'Создать', + read: 'Читать', + edit: 'Редактировать', + delete: 'Удалить', + save: 'Сохранить', + saved: 'Сохранено', + saveAs: 'Сохранить как нового', + close: 'Закрыть', + next: 'Далее', + back: 'Назад', + change: 'Заменить', + name: 'Имя', + title: 'Название', + description: 'Описание', + user: 'Пользователь', + users: 'Пользователи', + action: 'Action', + allow: 'Allow', + password: 'Пароль', + history: 'История', + copy: 'Копировать', + copied: 'Скопировано!', + createdAt: 'Создано', + createdBy: 'Кем создан', + importCSV: 'Импортировать CSV', + online: 'Онлайн', + offline: 'Оффлайн', + on: 'On', + off: 'Off', + ok: 'Ok', + enabled: 'Включено', + status: 'Статус', + from: 'От', + to: 'До', + importJSON: 'Импортировать JSON', + addNew: 'Добавить', + provider: 'Провайдер', + default: 'По умолчанию', + service: 'Сервис', + tts: 'Text-to-Speech', + stt: 'Speech-to-Text', + key: 'Ключ', + ID: 'ID', + email: 'Адрес электронной почты', + updatedAt: 'Изменено', + emptyWorkspace: 'Записи в разделе еще не созданы', + emptyResultSearch: 'Поиск не дал результатов', + + deleteConfirmation: { + title: 'Подтвердите удаление', + askingAlert: + 'Вы уверенны, что хотите удалить {count} запись? | Вы уверенны, что хотите удалить {count} записей?', + undoneActionAlert: 'Это действие не может быть отменено.', + deleteAll: 'ВСЕ', + }, + generalInfo: 'Общая информация', + objectHeader: {}, + permissions: { + permissions: 'Дозволи', + permissionsRole: 'Роль | Ролі', + allRoles: 'Всі ролі', + searchPlaceholder: 'роль ..', + roles: { + permissions: { + permissions: 'Право доступа роли | Права доступа ролей', + add: 'Создавать', + delete: 'Удалять', + read: 'Выбирать', + write: 'Редактировать', + eavesdropCall: 'Прослушивать активный звонок', + playbackRecordFile: 'Воспроизводить записи разговоров', + exportDataGrid: 'Экспортировать данные', + viewCdrPhoneNumbers: 'Просмотр незашифрованных номеров телефонов', + manageUserRoles: 'Управление ролями пользователей', + manageUserLicense: 'Управление лицензиями пользователей', + changeUserPassword: 'Изменять пароли пользователей', + systemSetting: 'Управление системными конфигурациями', + addDescription: 'Предоставляет разрешение на создание объектов', + deleteDescription: 'Предоставляет разрешение на удаление объектов', + readDescription: 'Предоставляет разрешение на выбор объектов', + writeDescription: 'Предоставляет разрешение на изменение объектов', + eavesdropCallDescription: 'Предоставляет разрешение на прослушивание активного разговора', + playbackRecordFileDescription: + 'Предоставляет разрешение на воспроизведение файлов с записями разговоров', + exportDataGridDescription: 'Предоставляет разрешение на экспорт данных', + viewCdrPhoneNumbersDescription: + 'Предоставляет разрешение на отображение незашифрованных телефонных номеров', + manageUserRolesDescription: + 'Предоставляет разрешение на управление набором ролей пользователей', + manageUserLicenseDescription: + 'Предоставляет разрешение на управление набором пользовательских лицензий', + changeUserPasswordDescription: + 'Предоставляет разрешение на изменение пароля пользователя', + systemSettingDescription: 'Предоставляет разрешение на управление разделом Конфигурация', + schemeVariables: 'Управление глобальными переменными', + schemeVariablesDescription: + 'Предоставляет разрешение на управление разделом Глобальные переменные', + }, + addPermission: 'Добавить право доступа для роли', + usage: 'Использование', + applicationsAccess: { + applicationsAccess: 'Приложение | Приложения', + access: 'Доступ', + }, + }, + object: { + object: 'Разделы', + allObjects: 'Все разделы', + ObAC: 'Управление действиями', + RbAC: 'Управление записями', + operations: 'Действия', + searchPlaceholder: 'раздел ..', + newPermissionRole: 'Новый владелец прав', + rbacDefault: 'Права доступа по записям по умолчанию', + grantor: 'Праводатель', + grantee: 'Получатель', + create: 'Создавать', + delete: 'Удалять', + accessMode: { + 1: 'Запрещено', + 2: 'Разрешено', + 3: 'Управление', + }, + }, + }, + routing: { + routing: 'Маршрутизация', + ip: 'IP', + protocol: 'Протокол', + port: 'Порт', + configuration: 'Конфигурация', + schema: 'Схема звонка', + json: 'JSON', + + flow: { + flow: 'Схема | Схемы', + allFlowSchemas: 'Все схемы', + createFlowSelectionPopup: 'Создать новую схему', + createNew: 'Создать новую схему?', + editor: 'Редактор', + diagram: { + diagram: 'Диаграмма', + description: 'Создать новую схему используя Webitel Flow Diagram', + }, + code: { + code: 'Код', + description: 'Создать новую схему используя JSON-схему в редакторе кода', + }, + askingAlert: 'Вы хотите сохранить изменения, внесенные в "{name}" ?', + }, + + dialplan: { + dialplan: 'Исходящая маршрутизация', + dialplanRules: 'Правила исходящей маршрутизации', + pattern: 'Номер назначения', + dialplanRule: 'Правило исходящей маршрутизации', + position: 'Позиция', + }, + + chatplan: { + chatplan: + 'Правило маршрутизации текстовых сообщений | Правила маршрутизации текстовых сообщений', + allChatplans: 'Все правила маршрутизации текстовых сообщений', + }, + + gateways: { + gateways: 'Шлюз | Шлюзы', + allGateways: 'Все шлюзы', + trunkingGateway: 'Многоканальный шлюз', + registerGateway: 'Зарегистрировать шлюз', + stateSuccess: 'Успешно', + stateFailed: 'Неудачно', + stateProgress: 'В процессе', + stateNonreg: 'Не зарегистрирован', + proxy: 'Прокси', + searchPlaceholder: 'поиск шлюза...', + host: 'Хост', + trunkingACLTitle: 'Контроль доступа', + trunkingACL: 'Многоканальный список доступа', + hostnameTrunking: 'Сервер/Хост шлюза или IP', + hostnameRegister: 'Регистратор/Сервер/Хост шлюза или IP', + expire: 'Срок действия', + authID: 'Имя пользователя', + account: 'Аккаунт', + outboundProxy: 'Адрес исходящего SIP-прокси', + newGateway: 'Новый шлюз', + newGatewayDescription: 'Описание нового шлюза', + SIPregistrations: 'SIP-регистрация', + SIPregistrationsDescription: 'Описание SIP-регистрации', + SIPtrunking: 'SIP-транки', + SIPtrunkingDescription: 'Описание SIP-транков', + }, + + chatGateways: { + templates: { + templates: 'Шаблоны', + title: 'Анонимное имя абонента в Workspace', + close: 'Сообщение завершения чата', + join: 'Сообщение присоединения оператора', + left: 'Сообщение отключения оператора', + }, + chatGateways: 'Текстовый шлюз | Текстовые шлюзы', + allChatGateways: 'Все текстовые шлюзы', + telegramBot: { + telegramBot: 'Telegram Бот', + }, + telegramApp: { + telegramApp: 'Telegram Приложение', + signedAs: 'Вы авторизированы как', + joinTelegram: 'Sign in to Telegram', + metadata: { + apiId: 'API идентификатор', + apiHash: 'API хэш', + }, + }, + infobip: { + infobip: 'Infobip', + }, + messenger: { + meta: 'Meta', + addOrRemovePages: 'Добавить или удалить страницы', + accounts: 'Аккаунты', + metadata: { + clientId: 'App ID', + clientSecret: 'App Secret', + }, + facebook: { + pages: 'Facebook страницы', + subscription: 'Webhook подписки', + }, + instagram: { + instagram: 'Instagram', + comments: 'Отслеживать комментарии', + mentions: 'Отслеживать упоминания', + }, + whatsapp: { + whatsapp: 'Whatsapp', + token: 'Токен', + status: 'Статус', + number: 'Номер телефона', + review: 'Рассмотрение', + }, + }, + viber: { + viber: 'Viber', + style: { + style: 'Стиль', + btnBackColor: 'Цвет фона кнопки', + btnFontColor: 'Цвет текста кнопки', + }, + }, + webchat: { + webchat: 'Web chat', + copyCode: 'Скопировать код', + copyCodeHint: + 'Необходимо перегенерировать и вставить код Виджета на сайт после каждой модификации настроек', + metadata: { + mediaMaxSize: 'Максимальный размер файла (Мб)', + }, + view: { + borderRadius: 'Вид кнопки', + language: 'Язык', + view: 'Вид', + position: 'Размещение', + btnColor: 'Цвет кнопки', + logoUrl: 'URL логотипа', + logoHint: "Поддерживаемый формат логотипа - 'JPEG' or 'PNG', 24x24px", + rounded: 'Круглая', + square: 'Квадратная', + right: 'Справа', + left: 'Слева', + static: 'Статическая', + }, + recaptcha: { + recaptcha: 'reCAPTCHA', + sitekey: 'Site key', + secret: 'Secret key', + threshold: 'Порог', + showFlag: 'Показывать значок reCAPTCHA', + }, + chat: { + chat: 'Чат', + openTimeout: 'Включить задержку открытия', + openTimeoutSec: 'Время задержки открытия', + }, + appointment: { + appointment: 'Заказ звонка', + days: 'Количество дней', + availableAgents: 'Свободные агенты', + showEmailField: 'Показать поле ввода почты', + showMessageField: 'Показать поле ввода сообщения', + resultPageText: 'Текст та странице результата', + headingText: 'Заголовок', + subheadingText: 'Подзаголовок', + showDefaultHeading: + 'Показать стандартный заголовок и подзаголовок на странице результата', + }, + call: { + title: 'Онлайн-звонок', + url: 'WebSocket Endpoint', + }, + alternativeChannels: { + alternativeChannels: 'Альтернативные каналы', + title: 'Текстовые каналы', + email: 'Электронная почта', + whatsapp: 'WhatsApp', + telegram: 'Telegram', + messenger: 'Messenger', + }, + }, + customChat: { + customChat: 'Custom Chat', + customChatGateway: 'Custom Chat Gateway', + appSecretHint: 'Изменение этого поля прервет интеграцию', + metadata: { + appSecret: 'App Secret', + callback: 'Callback', + }, + }, + uri: 'URI', + newChatGateway: 'Новый текстовый шлюз', + metadata: { + apiKey: 'Api Key', + number: 'Number', + baseUrl: 'Base URL', + sendApi: 'Send API', + allowOrigin: 'Allow Origin', + readTimeout: 'Read Timeout (sec)', + writeTimeout: 'Write Timeout (sec)', + handshakeTimeout: 'Handshake Timeout (sec)', + messageSize: 'Message Size max (bytes)', + botName: 'Bot Name', + eventTypes: 'Event Types', + telegramToken: 'Telegram BOT API Token', + messengerApiUrl: 'Messenger API URL', + }, + }, + + callflow: { + callflow: 'Конструктор звонка', + }, + }, + pagination: { + rowsPerPage: 'Количество строк на странице', + }, + }, + + lookups: { + lookups: 'Справочники', + contactGroups: { + contactGroups: 'Группы контактов', + groups: 'Группы', + crm: 'CRM', + configurations: 'Конфигурация', + } + }, + + errorPages: { + goToHome: 'Вернуться Домой', + page403: { + title: 'Нет доступа', + text: 'Извините, у вас недостаточно прав доступа для просмотра этой страницы.', + }, + page404: { + title: 'Похоже, вы потерялись', + text: 'Извините, мы не можем найти то, что вы ищете', + }, + }, + + iconHints: { + upload: 'Загрузить', + reload: 'Обновить', + edit: 'Редактировать', + delete: 'Удалить', + deleteAll: 'Удалить все объекты', + deleteSelected: 'Удалить {count} выбранных объектов', + deleteFiltered: 'Удалить все отфильтрованные объекты', + generate: 'Создать', + add: 'Добавить', + history: 'История', + download: 'Скачать', + downloadAll: 'Скачать все', + draggable: 'Перетащить', + play: 'Играть', + pause: 'Пауза', + resume: 'Возобновить', + close: 'Закрыть', + volume: 'Громкость', + stop: 'Остановить', + members: 'Абоненты', + moveUp: 'Передвинуть вверх', + moveDown: 'Передвинуть вниз', + nextPage: 'Следующая страница', + prevPage: 'Предыдущая страница', + expand: 'Развернуть', + collapse: 'Свернуть', + }, + + errors: { + invalidJson: 'Некорректный JSON', + }, + reusable: { + }, }; diff --git a/src/app/locale/ua/ua.js b/src/app/locale/ua/ua.js index 590cb018..9789cc21 100644 --- a/src/app/locale/ua/ua.js +++ b/src/app/locale/ua/ua.js @@ -92,13 +92,425 @@ export default { }, configuration: { [CrmConfigurationSections.LOOKUPS]: 'Довідники', - [CrmConfigurationSections.SLA]: 'SLA', - [CrmConfigurationSections.SERVICE_CATALOG]: 'Каталог сервісів', - [CrmConfigurationSections.PRIORITIES]: 'Пріоритети', - [CrmConfigurationSections.STATUSES]: 'Статуси', - [CrmConfigurationSections.CLOSURE_REASONS]: 'Причини закриття', [CrmConfigurationSections.CONTACT_GROUPS]: 'Групи контактів', - [CrmConfigurationSections.CASE_SOURCES]: 'Джерела звернень', - [CrmConfigurationSections.CUSTOM_LOOKUPS]: 'Користувацькі довідники', - } + }, + + validation: { + email: 'Введіть адресу електронної пошти', + required: "Поле обов'язкове для заповнення", + sameAs: 'Невірний пароль', + gatewayHostValidator: 'Має виглядати як IPv4 або FQDN', + ipValidator: 'Має виглядати як IPv4', + macValidator: 'Має виглядати як MAC', + minValue: 'Значення має бути не менше', + maxValue: 'Значення має бути не надто велкиим', + numeric: 'Мають бути цифри', + requiredArrayValue: 'Поле не може бути пустим', + isPositiveValue: 'Значення має бути додатнім числом', + cron: 'Некорректний cron-вираз', + }, + + objects: { + all: 'Всі {entity}', + administration: 'Управління', + general: 'Загальне', + new: 'Новий', + add: 'Додати', + create: 'Створити', + read: 'Читати', + edit: 'Редагувати', + delete: 'Видалити', + save: 'Зберегти', + saved: 'Збережено', + saveAs: 'Зберігти як нового', + close: 'Закрити', + next: 'Далі', + back: 'Назад', + change: 'Замінити', + name: "Ім'я", + title: 'Назва', + description: 'Опис', + user: 'Користувач', + users: 'Користувачі', + action: 'Дія', + allow: 'Дозволити', + password: 'Пароль', + history: 'Історія', + copy: 'Копіювати', + copied: 'Скопійовано!', + createdAt: 'Створено', + createdBy: 'Ким створено', + importCSV: 'Імпортувати CSV', + online: 'Онлайн', + offline: 'Офлайн', + on: 'On', + off: 'Off', + ok: 'Ok', + enabled: 'Ввімкнено', + status: 'Статус', + from: 'Від', + to: 'До', + importJSON: 'Імпортувати JSON', + addNew: 'Додати', + provider: 'Провайдер', + default: 'За замовчуванням', + service: 'Сервіс', + tts: 'Text-to-Speech', + stt: 'Speech-to-Text', + key: 'Ключ', + ID: 'ID', + email: 'Адреса електронної пошти', + updatedAt: 'Змінено', + emptyWorkspace: 'Записи у розділі ще не створені', + emptyResultSearch: 'Пошук не дав результатів', + + deleteConfirmation: { + title: 'Підтвердіть видалення', + askingAlert: + 'Ви впевнені, що хочете видалити {count} запис? | Ви впевнені, що хочете видалити {count} записів?', + undoneActionAlert: 'Дана дія не може бути скасована.', + deleteAll: 'ВСІ', + }, + generalInfo: 'Загальна інформація', + objectHeader: {}, + permissions: { + permissions: 'Дозволи', + permissionsRole: 'Роль | Ролі', + allRoles: 'Всі ролі', + searchPlaceholder: 'роль ..', + roles: { + permissions: { + permissions: 'Дозвіл ролі | Дозволи ролей', + add: 'Створювати', + delete: 'Видаляти', + read: 'Вибирати', + write: 'Редагувати', + eavesdropCall: 'Прослуховувати активний дзвінок', + playbackRecordFile: 'Відтворювати записи розмов', + exportDataGrid: 'Експортувати дані', + viewCdrPhoneNumbers: 'Перегляд незашифрованих номерів телефонів', + manageUserRoles: 'Управління ролями користувачів', + manageUserLicense: 'Управління ліцензіями користувачів', + changeUserPassword: 'Змінювати паролі користувачів', + systemSetting: 'Управління системними конфігураціями', + addDescription: 'Надає дозвіл на створення об’єктів', + deleteDescription: 'Надає дозвіл на видалення об’єктів', + readDescription: 'Надає дозвіл на вибір об’єктів', + writeDescription: 'Надає дозвіл змінювати об’єкти', + eavesdropCallDescription: 'Надає дозвіл прослуховувати активну розмову', + playbackRecordFileDescription: 'Надає дозвіл на відтворення файлів записів дзвінків', + exportDataGridDescription: 'Надає дозвіл на експорт даних', + viewCdrPhoneNumbersDescription: + 'Надає дозвіл на відображення незашифрованих телефонних номерів', + manageUserRolesDescription: 'Надає дозвіл на керування набором ролей користувачів', + manageUserLicenseDescription: 'Надає дозвіл на керування набором ліцензій користувача', + changeUserPasswordDescription: 'Надає дозвіл на зміну пароля користувача', + systemSettingDescription: 'Надає дозвіл на керування розділом Конфігурація', + schemeVariables: 'Управління глобальними змінними', + schemeVariablesDescription: 'Надає дозвіл на керування розділом Глобальні змінні', + }, + addPermission: 'Надати дозвіл', + usage: 'Використання', + applicationsAccess: { + applicationsAccess: 'Застосунок | Застосунки', + access: 'Доступ', + }, + }, + object: { + object: 'Розділи', + allObjects: 'Всі розділи', + ObAC: 'Управління діями', + RbAC: 'Управління записами', + operations: 'Дії', + searchPlaceholder: 'розділ ..', + newPermissionRole: 'Новий власник прав', + rbacDefault: 'Права доступу на записи за замовчуванням', + grantor: 'Праводатель', + grantee: 'Отримувач', + create: 'Створювати', + delete: 'Видаляти', + accessMode: { + 1: 'Заборонено', + 2: 'Дозволено', + 3: 'Управління', + }, + }, + }, + routing: { + routing: 'Routing', + ip: 'IP', + protocol: 'Protocol', + port: 'Port', + configuration: 'Configuration', + schema: 'Flow schema', + json: 'JSON', + + flow: { + flow: 'Flow schema | Flow schemas', + allFlowSchemas: 'All flow schemas', + createFlowSelectionPopup: 'Create new flow', + createNew: 'Create new?', + editor: 'Editor', + diagram: { + diagram: 'Diagram', + description: 'Create new flow using Webitel Flow Diagram tool', + }, + code: { + code: 'Code', + description: 'Create new flow using JSON-schema in code editor', + }, + askingAlert: 'Do you want to save the changes you made to "{name}" ?', + }, + + dialplan: { + dialplan: 'Dialplans', + dialplanRules: 'Dialplan rules', + pattern: 'Destination number', + dialplanRule: 'Dialplan rule', + position: 'Position', + }, + + chatplan: { + chatplan: 'Chatplan | Chatplans', + allChatplans: 'All chatplans', + }, + + gateways: { + gateways: 'Gateway | Gateways', + allGateways: 'All gateways', + trunkingGateway: 'Trunking gateway', + registerGateway: 'Register gateway', + stateSuccess: 'Success', + stateFailed: 'Failed', + stateProgress: 'In progress', + stateNonreg: 'Not registered', + proxy: 'Proxy', + searchPlaceholder: 'gateway search...', + host: 'Host', + trunkingACLTitle: 'Access Control', + trunkingACL: 'Trunking access control list', + hostnameTrunking: 'Server/Gateway Hostname or IP', + hostnameRegister: 'Registrar/Server/Gateway Hostname or IP', + expire: 'Expire', + authID: 'Username', + account: 'Account', + outboundProxy: 'Outbound Proxy Hostname or IP', + newGateway: 'New gateway', + newGatewayDescription: 'New gateway description', + SIPregistrations: 'SIP Registration', + SIPregistrationsDescription: 'SIP registration description', + SIPtrunking: 'SIP Trunking', + SIPtrunkingDescription: 'SIP trunking description', + }, + chatGateways: { + templates: { + templates: 'Templates', + title: "Workspace member's name", + close: 'Chat complete message', + join: 'Agent joining message', + left: 'Agent disconnection message', + }, + chatGateways: 'Chat gateway | Chat gateways', + allChatGateways: 'All chat gateways', + telegramBot: { + telegramBot: 'Telegram Bot', + }, + telegramApp: { + telegramApp: 'Telegram App', + signedAs: "You're signed as", + joinTelegram: 'Sign in to Telegram', + metadata: { + apiId: 'API id', + apiHash: 'API hash', + }, + }, + infobip: { + infobip: 'Infobip', + }, + messenger: { + meta: 'Meta', + addOrRemovePages: 'Add or remove pages', + accounts: 'Accounts', + metadata: { + clientId: 'App ID', + clientSecret: 'App Secret', + }, + facebook: { + pages: 'Facebook pages', + subscription: 'Webhook subscription', + }, + instagram: { + instagram: 'Instagram', + comments: 'Track comments', + mentions: 'Track mentions', + }, + whatsapp: { + whatsapp: 'Whatsapp', + token: 'Token', + status: 'Status', + number: 'Phone number', + review: 'Review', + }, + }, + viber: { + viber: 'Viber', + style: { + style: 'Style', + btnBackColor: 'Button background color', + btnFontColor: 'Button font color', + }, + }, + webchat: { + webchat: 'Web chat', + copyCode: 'Copy code for site', + copyCodeHint: + 'You must regenerate and reinsert Widget code to website each time after Widget settings modification', + metadata: { + mediaMaxSize: 'Max media size (Mb)', + }, + view: { + view: 'View', + borderRadius: 'Button Shape', + language: 'Language', + position: 'Position', + btnColor: 'Button Color', + logoUrl: 'Logo URL', + logoHint: "Supported logo format is 'JPEG' or 'PNG', 24x24px", + rounded: 'Rounded', + square: 'Square', + right: 'Right', + left: 'Left', + static: 'Static', + }, + recaptcha: { + recaptcha: 'reCAPTCHA', + sitekey: 'Site key', + secret: 'Secret key', + threshold: 'Threshold', + showFlag: 'Show reCAPTCHA badge', + }, + chat: { + chat: 'Chat', + openTimeout: 'Open Timeout', + openTimeoutSec: 'Open Timeout Sec', + }, + appointment: { + appointment: 'Appointment', + days: 'Days', + availableAgents: 'Available agents', + showEmailField: 'Show email field', + showMessageField: 'Show message field', + resultPageText: 'Result page text', + headingText: 'Heading text', + subheadingText: 'Subheading text', + showDefaultHeading: 'Show default heading and subheading on result page', + }, + alternativeChannels: { + alternativeChannels: 'Alternative Channels', + title: 'Messaging channels', + email: 'Email', + whatsapp: 'WhatsApp', + telegram: 'Telegram', + messenger: 'Messenger', + }, + call: { + title: 'Online call', + url: 'WebSocket Endpoint', + }, + }, + customChat: { + customChat: 'Custom Chat', + customChatGateway: 'Custom Chat Gateway', + appSecretHint: 'Altering this field will interrupt the integration', + metadata: { + appSecret: 'App Secret', + callback: 'Callback', + }, + }, + uri: 'URI', + newChatGateway: 'New chat gateway', + metadata: { + apiKey: 'Api Key', + number: 'Number', + baseUrl: 'Base URL', + sendApi: 'Send API', + allowOrigin: 'Allow Origin', + readTimeout: 'Read Timeout (sec)', + writeTimeout: 'Write Timeout (sec)', + handshakeTimeout: 'Handshake Timeout (sec)', + messageSize: 'Message Size max (bytes)', + botName: 'Bot Name', + eventTypes: 'Event Types', + telegramToken: 'Telegram BOT API Token', + messengerApiUrl: 'Messenger API URL', + }, + }, + + callflow: { + callflow: 'Callflow', + }, + }, + pagination: { + rowsPerPage: 'Rows per page', + }, + }, + + lookups: { + lookups: 'Довідники', + contactGroups: { + contactGroups: 'Групи контактів', + groups: 'Групи', + crm: 'CRM', + configurations: 'Конфігурація', + } + }, + + errorPages: { + goToHome: 'Повернутись додому', + page403: { + title: 'Немає доступу', + text: 'Вибачте, у Вас недостатньо прав доступу для перегляду цієї сторінки', + }, + page404: { + title: 'Здається, Ви загубились', + text: 'Вибачте, ми не можемо знайти те, що Ви шукаєте', + }, + }, + + iconHints: { + upload: 'Завантажити', + reload: 'Оновити', + edit: 'Редагувати', + delete: 'Видалити', + deleteAll: "Видалити всі об'єкти", + deleteSelected: "Видалити {count} обраних об'єктів", + deleteFiltered: "Видалити всі відфільтровані об'єкти", + generate: 'Створити', + add: 'Додати', + history: 'Історія', + download: 'Скачати', + downloadAll: 'Скачати все', + draggable: 'Перетягнути', + play: 'Програти', + pause: 'Пауза', + resume: 'Продовжити', + close: 'Зкрити', + change: 'Замінити', + volume: 'Гучність', + stop: 'Зупинити', + members: 'Абоненти', + moveUp: 'Пересунути вгору', + moveDown: 'Пересунути вниз', + nextPage: 'Наступна сторінка', + prevPage: 'Попередня сторінка', + expand: 'Розгорнути', + collapse: 'Згорнути', + }, + + errors: { + invalidJson: 'Некоректний JSON', + }, + + reusable: { + }, }; diff --git a/src/app/mixins/baseMixins/accessControlMixin/__tests__/accessControlMixin.spec.js b/src/app/mixins/baseMixins/accessControlMixin/__tests__/accessControlMixin.spec.js new file mode 100644 index 00000000..5eb9f616 --- /dev/null +++ b/src/app/mixins/baseMixins/accessControlMixin/__tests__/accessControlMixin.spec.js @@ -0,0 +1,15 @@ +import { mount } from '@vue/test-utils'; +import AccessControlMixin from '../accessControlMixin'; + +const Component = { + render() {}, +}; + +describe('AccessControlMixin', () => { + it('renders a component', () => { + const wrapper = mount(Component, { + mixins: [AccessControlMixin], + }); + expect(wrapper.exists()).toBe(true); + }); +}); diff --git a/src/app/mixins/baseMixins/accessControlMixin/accessControlMixin.js b/src/app/mixins/baseMixins/accessControlMixin/accessControlMixin.js new file mode 100644 index 00000000..66c2ede8 --- /dev/null +++ b/src/app/mixins/baseMixins/accessControlMixin/accessControlMixin.js @@ -0,0 +1,24 @@ +export default { + computed: { + hasReadAccess() { + return this.$store.getters['userinfo/HAS_READ_ACCESS']({ + route: this.$route, + }); + }, + hasCreateAccess() { + return this.$store.getters['userinfo/HAS_CREATE_ACCESS']({ + route: this.$route, + }); + }, + hasEditAccess() { + return this.$store.getters['userinfo/HAS_EDIT_ACCESS']({ + route: this.$route, + }); + }, + hasDeleteAccess() { + return this.$store.getters['userinfo/HAS_DELETE_ACCESS']({ + route: this.$route, + }); + }, + }, +}; diff --git a/src/app/mixins/baseMixins/baseObjectMixin/__tests__/baseObjectMixin.spec.js b/src/app/mixins/baseMixins/baseObjectMixin/__tests__/baseObjectMixin.spec.js new file mode 100644 index 00000000..4d21cf96 --- /dev/null +++ b/src/app/mixins/baseMixins/baseObjectMixin/__tests__/baseObjectMixin.spec.js @@ -0,0 +1,34 @@ +import { shallowMount } from '@vue/test-utils'; +import BaseObjectMixin from '../baseObjectMixin'; + +const Component = { + render() {}, +}; + +describe('BaseObjectMixin', () => { + it('renders a component', () => { + const wrapper = shallowMount(Component, { + data: () => ({ + itemInstance: {}, + }), + mixins: [BaseObjectMixin], + methods: { + checkValidations() { + return true; + }, + }, + global: { + mocks: { + v$: { + itemInstance: { + $error: true, + $touch: vi.fn(), + }, + }, + }, + }, + }); + expect(wrapper.exists()).toBe(true); + expect(wrapper.vm.disabledSave).toBe(true); + }); +}); diff --git a/src/app/mixins/baseMixins/baseObjectMixin/baseObjectMixin.js b/src/app/mixins/baseMixins/baseObjectMixin/baseObjectMixin.js new file mode 100644 index 00000000..8e7e2506 --- /dev/null +++ b/src/app/mixins/baseMixins/baseObjectMixin/baseObjectMixin.js @@ -0,0 +1,76 @@ +import { mapActions } from 'vuex'; +import openedObjectValidationMixin from '../openedObjectValidationMixin/openedObjectValidationMixin'; +import resetOnDestroyMixin from '../resetOnDestroyMixin/resetOnDestroyMixin'; + +/** + * @fileOverview abstract mixin, + * used by edit-components: main (openedObjectMixin) + * and nested (nestedObjectMixin) mixins + * @param {string} this.namespace - should be declared in data() + * and contain a string name for storeModule like 'ccenter/agents/skills' + * @extends openedObjectValidationMixin, openedObjectAccessControlMixin + */ +export default { + mixins: [resetOnDestroyMixin, openedObjectValidationMixin], + + computed: { + id() { + throw new Error('Override id param for a component'); + }, + new() { + return this.$route.params.id === 'new'; + }, + saveText() { + // if it's a new item + // OR any fields have changed + return !this.id || this.itemInstance._dirty + ? this.$t('objects.save') + : this.$t('objects.saved'); + }, + + disabledSave() { + // if there's a validation problem + // OR it's edit and any fields haven't changed + return this.checkValidations() || (!this.itemInstance._dirty && !!this.id); + }, + }, + + methods: { + ...mapActions({ + loadItem(dispatch, payload) { + return dispatch(`${this.namespace}/LOAD_ITEM`, payload); + }, + addItem(dispatch, payload) { + return dispatch(`${this.namespace}/ADD_ITEM`, payload); + }, + updateItem(dispatch, payload) { + return dispatch(`${this.namespace}/UPDATE_ITEM`, payload); + }, + }), + + async save() { + if (!this.disabledSave) { + if (!this.new) { + await this.updateItem(); + } else { + try { + await this.addItem(); + + if (this.id) { + await this.redirectToEdit(); + } + } catch (err) { + throw err; + } + } + } + }, + + async redirectToEdit(id = this.id) { + return this.$router.replace({ + ...this.$route, + params: { id }, + }); + }, + }, +}; diff --git a/src/app/mixins/baseMixins/baseTableMixin/baseTableMixin.js b/src/app/mixins/baseMixins/baseTableMixin/baseTableMixin.js new file mode 100644 index 00000000..1efc1ad8 --- /dev/null +++ b/src/app/mixins/baseMixins/baseTableMixin/baseTableMixin.js @@ -0,0 +1,48 @@ +import deleteMixin from './deleteMixin/tableDeleteMixin'; +import itemLinkMixin from './itemLinkMixin'; +import tableActionsHandlerMixin from './tableActionsMixin'; + +/** + * @fileOverview abstract mixin, + * used by main-table (tableComponentMixin) + * and table-in-tab (openedObjectTableTabMixin) mixins + * + * @extends itemLinkMixin, tableActionsHandlerMixin + */ +export default { + mixins: [deleteMixin, itemLinkMixin, tableActionsHandlerMixin], + + data: () => ({ + isLoaded: false, + }), + + created() { + this.initTableView(); + }, + + // computed: { + // selectedRows() { + // return this.dataList.filter((item) => item._isSelected); + // }, + // // shows delete table action if some items are selected + // anySelected() { + // return !this.selectedRows?.length; + // }, + // }, + + methods: { + initTableView() { + if (this.setParentId) this.setParentId(this.parentId); + this.loadList(); + }, + async loadList() { + this.isLoaded = false; + try { + await this.loadDataList(this.$route.query); + } catch (e) { + } finally { + this.isLoaded = true; + } + }, + }, +}; diff --git a/src/app/mixins/baseMixins/baseTableMixin/deleteMixin/tableDeleteMixin.js b/src/app/mixins/baseMixins/baseTableMixin/deleteMixin/tableDeleteMixin.js new file mode 100644 index 00000000..d74b7935 --- /dev/null +++ b/src/app/mixins/baseMixins/baseTableMixin/deleteMixin/tableDeleteMixin.js @@ -0,0 +1,10 @@ +export default { + methods: { + deleteData(deleted) { + return this.dispatchDelete(deleted); + }, + dispatchDelete() { + throw new TypeError('implement dispatchDelete method first'); + }, + }, +}; diff --git a/src/app/mixins/baseMixins/baseTableMixin/itemLinkMixin.js b/src/app/mixins/baseMixins/baseTableMixin/itemLinkMixin.js new file mode 100644 index 00000000..ee8bf5b4 --- /dev/null +++ b/src/app/mixins/baseMixins/baseTableMixin/itemLinkMixin.js @@ -0,0 +1,13 @@ +import RouteNames from '../../../router/_internals/RouteNames.enum'; + +export default { + data: () => ({ + RouteNames, + }), + methods: { + editLink({ id }) { + const routeName = this.routeName; + return { name: `${routeName}-card`, params: { id } }; + }, + }, +}; diff --git a/src/app/mixins/baseMixins/baseTableMixin/tableActionsMixin.js b/src/app/mixins/baseMixins/baseTableMixin/tableActionsMixin.js new file mode 100644 index 00000000..2a9d1143 --- /dev/null +++ b/src/app/mixins/baseMixins/baseTableMixin/tableActionsMixin.js @@ -0,0 +1,39 @@ +/** + * @fileOverview wt-table-actions extension mixin with default behavior, extended by baseTableMixin + */ +export default { + methods: { + tableActionsHandler(eventName) { + switch (eventName) { + case 'refresh': + this.refreshList(); + break; + case 'columnSelect': + this.openColumnSelect(); + break; + case 'filterReset': + this.resetFilters(); + break; + case 'settings': + this.expandFilters(); + break; + default: + } + }, + + expandFilters() { + this.isOpened = !this.isOpened; + }, + refreshList() { + this.loadList(); + }, + openColumnSelect() { + this.isFilterFieldsOpened = true; + }, + resetFilters() { + this.$router.replace({ + query: null, + }); + }, + }, +}; diff --git a/src/app/mixins/baseMixins/headlineNavMixin/headlineNavMixin.js b/src/app/mixins/baseMixins/headlineNavMixin/headlineNavMixin.js new file mode 100644 index 00000000..a5749972 --- /dev/null +++ b/src/app/mixins/baseMixins/headlineNavMixin/headlineNavMixin.js @@ -0,0 +1,25 @@ +export default { + data: () => ({ + pathName: '', + }), + watch: { + 'itemInstance._dirty': { + handler(value) { + if (!value) this.setPathName(); + }, + }, + }, + methods: { + setPathName() { + this.pathName = this.itemInstance.name; + }, + }, + mounted() { + // itemInstance._dirty isn't init as "false", + // so that we should set up first name representation in other way + const unwatch = this.$watch('itemInstance.name', () => { + this.setPathName(); + unwatch(); + }); + }, +}; diff --git a/src/app/mixins/baseMixins/openedObjectValidationMixin/openedObjectValidationMixin.js b/src/app/mixins/baseMixins/openedObjectValidationMixin/openedObjectValidationMixin.js new file mode 100644 index 00000000..66dcc333 --- /dev/null +++ b/src/app/mixins/baseMixins/openedObjectValidationMixin/openedObjectValidationMixin.js @@ -0,0 +1,19 @@ +/** + * @fileOverview implements work with validation + */ +export default { + props: { + v: { + type: Object, + }, + }, + + methods: { + checkValidations() { + const v = this.v$ ? this.v$ : this.v; + v.$touch(); + // if its still pending or an error is returned do not submit + return v.$pending || v.$error; + }, + }, +}; diff --git a/src/app/mixins/baseMixins/readme.md b/src/app/mixins/baseMixins/readme.md new file mode 100644 index 00000000..a54388c2 --- /dev/null +++ b/src/app/mixins/baseMixins/readme.md @@ -0,0 +1,4 @@ +# Base mixins +--- +Base mixins isn't strictly bound to whether type of UI component. They rather implement some basic functionality blocks, +then, extended by **objectPagesMixins** diff --git a/src/app/mixins/baseMixins/resetOnDestroyMixin/resetOnDestroyMixin.js b/src/app/mixins/baseMixins/resetOnDestroyMixin/resetOnDestroyMixin.js new file mode 100644 index 00000000..a729d190 --- /dev/null +++ b/src/app/mixins/baseMixins/resetOnDestroyMixin/resetOnDestroyMixin.js @@ -0,0 +1,15 @@ +import { mapActions } from 'vuex'; + +export default { + destroyed() { + this.resetState(); + }, + + methods: { + ...mapActions({ + resetState(dispatch, payload) { + return dispatch(`${this.namespace}/RESET_ITEM_STATE`, payload); + }, + }), + }, +}; diff --git a/src/app/mixins/objectPagesMixins/objectTableMixin/_internals/objectTableAccessControlMixin.js b/src/app/mixins/objectPagesMixins/objectTableMixin/_internals/objectTableAccessControlMixin.js new file mode 100644 index 00000000..b393b819 --- /dev/null +++ b/src/app/mixins/objectPagesMixins/objectTableMixin/_internals/objectTableAccessControlMixin.js @@ -0,0 +1,10 @@ +import accessControlMixin from '../../../baseMixins/accessControlMixin/accessControlMixin'; + +export default { + mixins: [accessControlMixin], + computed: { + hasTableActions() { + return this.hasEditAccess || this.hasDeleteAccess; + }, + }, +}; diff --git a/src/app/mixins/objectPagesMixins/objectTableMixin/tableComponentMixin.js b/src/app/mixins/objectPagesMixins/objectTableMixin/tableComponentMixin.js new file mode 100644 index 00000000..7cbb0243 --- /dev/null +++ b/src/app/mixins/objectPagesMixins/objectTableMixin/tableComponentMixin.js @@ -0,0 +1,112 @@ +import getNamespacedState from '@webitel/ui-sdk/src/store/helpers/getNamespacedState'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import baseTableMixin from '../../baseMixins/baseTableMixin/baseTableMixin'; +import objectTableAccessControlMixin from './_internals/objectTableAccessControlMixin'; + +/** + * @fileOverview contains main tables (like the-contact-groups.vue) common logic + * @param {string} this.namespace - should be declared in data() + * and contain a string name for storeModule like 'ccenter/agents/skills' + * @param {string} this.routeName - should be declared in data() + * and contain a string name for edit and new entity route names + * like 'cc-agent' for create() and edit() method standardization + * @extends baseTableMixin, objectTableAccessControlMixin + */ +export default { + mixins: [baseTableMixin, objectTableAccessControlMixin], + computed: { + ...mapState({ + headersValue(state) { + return getNamespacedState(state, this.namespace).headers; + }, + dataList(state) { + console.warn(getNamespacedState(state, this.namespace)); + return getNamespacedState(state, this.namespace).dataList; + }, + page(state) { + return getNamespacedState(state, this.namespace).page; + }, + size(state) { + return getNamespacedState(state, this.namespace).size; + }, + search(state) { + return getNamespacedState(state, this.namespace).search; + }, + isNext(state) { + return getNamespacedState(state, this.namespace).isNextPage; + }, + }), + ...mapGetters('appearance', { + darkMode: 'DARK_MODE', + }), + headers() { + if (!this.headersValue) return []; + return this.headersValue.map((header) => { + let localizedText; + // set "false" if no locale prop + + if (header.locale) { + localizedText = !header.locale || typeof header.locale === 'string' + ? this.$t(header.locale) + : this.$t(...header.locale); + } + return { + ...header, + text: localizedText || header.text, + }; + }); + }, + selectedRows() { + return this.dataList.filter((item) => item._isSelected); + }, + // shows delete table action if some items are selected + anySelected() { + return !this.selectedRows?.length; + }, + }, + methods: { + ...mapActions({ + loadDataList(dispatch, payload) { + return dispatch(`${this.namespace}/LOAD_DATA_LIST`, payload); + }, + setSize(dispatch, payload) { + return dispatch(`${this.namespace}/SET_SIZE`, payload); + }, + setSearch(dispatch, payload) { + return dispatch(`${this.namespace}/SET_SEARCH`, payload); + }, + nextPage(dispatch, payload) { + return dispatch(`${this.namespace}/NEXT_PAGE`, payload); + }, + prevPage(dispatch, payload) { + return dispatch(`${this.namespace}/PREV_PAGE`, payload); + }, + dispatchSort(dispatch, payload) { + return dispatch(`${this.namespace}/SORT`, payload); + }, + dispatchDelete(dispatch, payload) { + return dispatch(`${this.namespace}/DELETE`, payload); + }, + patchItem(dispatch, payload) { + return dispatch(`${this.namespace}/PATCH_ITEM_PROPERTY`, payload); + }, + resetState(dispatch, payload) { + return dispatch(`${this.namespace}/RESET_ITEM_STATE`, payload); + }, + }), + create() { + this.resetState(); + this.$router.push({ name: `${this.routeName}-card`, params: { id: 'new' } }); + }, + edit(item) { + this.resetState(); + this.$router.push(this.editLink(item)); + }, + sort(...params) { + this.dispatchSort({ + header: params[0], + nextSortOrder: params[1], + }); + }, + }, +}; diff --git a/src/app/mixins/objectPagesMixins/openedObjectMixin/_internals/openedObjectAccessControlMixin.js b/src/app/mixins/objectPagesMixins/openedObjectMixin/_internals/openedObjectAccessControlMixin.js new file mode 100644 index 00000000..5f6c40f1 --- /dev/null +++ b/src/app/mixins/objectPagesMixins/openedObjectMixin/_internals/openedObjectAccessControlMixin.js @@ -0,0 +1,11 @@ +import accessControlMixin from '../../../baseMixins/accessControlMixin/accessControlMixin'; + +export default { + mixins: [accessControlMixin], + computed: { + hasSaveActionAccess() { + if (this.$route.params.id === 'new') return this.hasEditAccess; + return this.hasCreateAccess; + }, + }, +}; diff --git a/src/app/mixins/objectPagesMixins/openedObjectMixin/nestedObjectMixin.js b/src/app/mixins/objectPagesMixins/openedObjectMixin/nestedObjectMixin.js new file mode 100644 index 00000000..d285f76d --- /dev/null +++ b/src/app/mixins/objectPagesMixins/openedObjectMixin/nestedObjectMixin.js @@ -0,0 +1,54 @@ +import getNamespacedState from '@webitel/ui-sdk/src/store/helpers/getNamespacedState'; +import { mapActions, mapState } from 'vuex'; +import baseObjectMixin from '../../baseMixins/baseObjectMixin/baseObjectMixin'; + +/** + * @fileOverview contains nestedObject common logic + * (popup entity inside objectTab like opened-agent-skills-popup.vue) + * @param {string} this.namespace - should be declared in data() + * and contain a string name for storeModule like 'ccenter/agents/skills' + * @extends baseObjectMixin + */ +export default { + mixins: [baseObjectMixin], + + computed: { + ...mapState({ + id(state) { + return getNamespacedState(state, this.namespace).itemId; + }, + itemInstance(state) { + return getNamespacedState(state, this.namespace).itemInstance; + }, + }), + }, + + methods: { + ...mapActions({ + setItemProp(dispatch, payload) { + return dispatch(`${this.namespace}/SET_ITEM_PROPERTY`, payload); + }, + setId(dispatch, payload) { + return dispatch(`${this.namespace}/SET_ITEM_ID`, payload); + }, + }), + + async save() { + const invalid = this.checkValidations(); + if (!invalid) { + try { + if (this.id) { + await this.updateItem(); + } else { + await this.addItem(); + } + this.close(); + } catch {} + } + }, + + close() { + this.$emit('close'); + }, + }, +}; diff --git a/src/app/mixins/objectPagesMixins/openedObjectMixin/openedObjectMixin.js b/src/app/mixins/objectPagesMixins/openedObjectMixin/openedObjectMixin.js new file mode 100644 index 00000000..4c999328 --- /dev/null +++ b/src/app/mixins/objectPagesMixins/openedObjectMixin/openedObjectMixin.js @@ -0,0 +1,78 @@ +import getNamespacedState from '@webitel/ui-sdk/src/store/helpers/getNamespacedState'; +import { mapActions, mapState } from 'vuex'; +import Permissions from '../../../../modules/_shared/permissions-tab/components/permissions-tab.vue'; +import baseObjectMixin from '../../baseMixins/baseObjectMixin/baseObjectMixin'; +import headlineNavMixin from '../../baseMixins/headlineNavMixin/headlineNavMixin'; +import openedObjectAccessControlMixin from './_internals/openedObjectAccessControlMixin'; + +/** + * @fileOverview contains openedObject (wrapper with tabs, like opened-agent.vue) common logic + * @param {string} this.namespace - should be declared in data() + * and contain a string name for storeModule like 'ccenter/agents/skills' + * @extends baseObjectMixin + */ +export default { + mixins: [openedObjectAccessControlMixin, headlineNavMixin, baseObjectMixin], + components: { + Permissions, + }, + + created() { + this.loadPageData(); + }, + + computed: { + ...mapState({ + id(state) { + return getNamespacedState(state, this.namespace).itemId; + }, + itemInstance(state) { + return getNamespacedState(state, this.namespace).itemInstance; + }, + }), + permissionsTab() { + return { + text: this.$t('objects.permissions.permissions', 2), + value: 'permissions', + pathName: this.permissionsTabPathName, + }; + }, + currentTab() { + return this.tabs.find(({pathName}) => this.$route.name === pathName) || this.tabs[0]; + }, + }, + + methods: { + ...mapActions({ + setId(dispatch, payload) { + return dispatch(`${this.namespace}/SET_ITEM_ID`, payload); + }, + resetState(dispatch, payload) { + return dispatch(`${this.namespace}/RESET_ITEM_STATE`, payload); + }, + }), + + async loadPageData() { + await this.setId(this.$route.params.id); + return this.loadItem(); + }, + + setInitialTab() { + // eslint-disable-next-line prefer-destructuring + if (this.tabs) this.currentTab = this.tabs[0]; + }, + + close() { + + // Need to close the tab if it was open in a new tab + // https://webitel.atlassian.net/browse/WTEL-4575 + // TODO delete close method in all opened objects and add to them routeName property + this.resetState(); + if(window.history.length === 1) window.close(); + this.$router.push({name: this.routeName}); + }, + changeTab(tab) { + this.$router.push({ ...this.$route, name: tab.pathName }); + } + }, +}; diff --git a/src/app/mixins/objectPagesMixins/openedObjectTabMixin/_internals/openedObjectTabAccessControlMixin.js b/src/app/mixins/objectPagesMixins/openedObjectTabMixin/_internals/openedObjectTabAccessControlMixin.js new file mode 100644 index 00000000..f54e1052 --- /dev/null +++ b/src/app/mixins/objectPagesMixins/openedObjectTabMixin/_internals/openedObjectTabAccessControlMixin.js @@ -0,0 +1,11 @@ +import accessControlMixin from '../../../baseMixins/accessControlMixin/accessControlMixin'; + +export default { + mixins: [accessControlMixin], + computed: { + disableUserInput() { + if (this.$route.params.id === 'new') return !this.hasEditAccess; + return !this.hasCreateAccess; + }, + }, +}; diff --git a/src/app/mixins/objectPagesMixins/openedObjectTabMixin/openedTabComponentMixin.js b/src/app/mixins/objectPagesMixins/openedObjectTabMixin/openedTabComponentMixin.js new file mode 100644 index 00000000..16956f11 --- /dev/null +++ b/src/app/mixins/objectPagesMixins/openedObjectTabMixin/openedTabComponentMixin.js @@ -0,0 +1,44 @@ +import getNamespacedState from '@webitel/ui-sdk/src/store/helpers/getNamespacedState'; +import { mapActions, mapState } from 'vuex'; + +import openedObjectValidationMixin from '../../baseMixins/openedObjectValidationMixin/openedObjectValidationMixin'; +import openedObjectTabAccessControlMixin from './_internals/openedObjectTabAccessControlMixin'; + +/** + * @fileOverview contains openedObject tab + * (tab with subordinate entity, like opened-agent-general.vue) common logic + * @param {string} this.namespace - should be passed as prop from tabs wrapper + * @extends openedObjectValidationMixin, openedObjectTabAccessControlMixin + */ +export default { + mixins: [openedObjectValidationMixin, openedObjectTabAccessControlMixin], + props: { + namespace: { + type: String, + // required: true, FIXME: MAKE ME REQUIRED AFTER REFACTOR + }, + }, + computed: { + ...mapState({ + itemInstance(state) { + return getNamespacedState(state, this.namespace).itemInstance; + }, + }), + }, + methods: { + ...mapActions({ + setItemProp(dispatch, payload) { + return dispatch(`${this.namespace}/SET_ITEM_PROPERTY`, payload); + }, + addVariable(dispatch, payload) { + return dispatch(`${this.namespace}/ADD_VARIABLE_PAIR`, payload); + }, + deleteVariable(dispatch, payload) { + return dispatch(`${this.namespace}/DELETE_VARIABLE_PAIR`, payload); + }, + setVariableProp(dispatch, payload) { + return dispatch(`${this.namespace}/SET_VARIABLE_PROP`, payload); + }, + }), + }, +}; diff --git a/src/app/mixins/objectPagesMixins/openedObjectTableTabMixin/openedObjectTableTabMixin.js b/src/app/mixins/objectPagesMixins/openedObjectTableTabMixin/openedObjectTableTabMixin.js new file mode 100644 index 00000000..7a4ce718 --- /dev/null +++ b/src/app/mixins/objectPagesMixins/openedObjectTableTabMixin/openedObjectTableTabMixin.js @@ -0,0 +1,141 @@ +import getNamespacedState from '@webitel/ui-sdk/src/store/helpers/getNamespacedState'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import ObjectListPopup from '../../../components/utils/object-list-popup/object-list-popup.vue'; +import OnePlusMany from '../../../components/utils/table-cell/one-plus-many-table-cell/one-plus-many-table-cell.vue'; +import baseTableMixin from '../../baseMixins/baseTableMixin/baseTableMixin'; +import openedTabComponentMixin from '../openedObjectTabMixin/openedTabComponentMixin'; + +/** + * @fileOverview contains openedObject tab with table + * (tab with subordinate entity, like opened-agent-teams.vue) common logic + * @param {string} this.subNamespace - should be declared in data() + * and contain a string name for sub-entity storeModule like 'teams' + * @extends openedTabComponentMixin, + * @extends baseTableMixin + */ +export default { + mixins: [openedTabComponentMixin, baseTableMixin], + components: { OnePlusMany, ObjectListPopup }, + inject: ['$eventBus'], + watch: { + parentId(value) { + this.setParentId(value); + }, + }, + + computed: { + ...mapState({ + parentId(state) { + return getNamespacedState(state, `${this.namespace}`).itemId; + }, + headersValue(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).headers; + }, + dataList(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).dataList; + }, + page(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).page; + }, + size(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).size; + }, + search(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).search; + }, + isNext(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).isNextPage; + }, + aggs(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).aggs; + }, + }), + ...mapGetters('appearance', { + darkMode: 'DARK_MODE', + }), + headers() { + if (!this.headersValue) return []; + return this.headersValue.map((header) => ({ + ...header, + text: + typeof header.locale === 'string' ? this.$t(header.locale) : this.$t(...header.locale), + })); + }, + }, + + methods: { + ...mapActions({ + addParentItem(dispatch, payload) { + return dispatch(`${this.namespace}/ADD_ITEM`, payload); + }, + }), + loadDataList(payload) { + if (!this.parentId) return; + this.loadDataListAction(payload); + }, + ...mapActions({ + setParentId(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/SET_PARENT_ITEM_ID`, payload); + }, + setId(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/SET_ITEM_ID`, payload); + }, + loadDataListAction(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/LOAD_DATA_LIST`, payload); + }, + setSize(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/SET_SIZE`, payload); + }, + setSearch(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/SET_SEARCH`, payload); + }, + nextPage(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/NEXT_PAGE`, payload); + }, + prevPage(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/PREV_PAGE`, payload); + }, + dispatchSort(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/SORT`, payload); + }, + dispatchDelete(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/DELETE`, payload); + }, + patchItem(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/PATCH_ITEM_PROPERTY`, payload); + }, + resetItemState(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/RESET_ITEM_STATE`, payload); + }, + }), + + async create() { + const invalid = this.checkValidations(); + if (!invalid) { + try { + if (!this.parentId) { + await this.addParentItem(); + await this.$router.replace({ + ...this.$route, + params: { id: this.parentId }, + }); + } + this.addItem(); + } catch (err) { + throw err; + } + } else { + this.$eventBus.$emit('notification', { + type: 'error', + text: 'Check your validations!', + }); + } + }, + sort(...params) { + this.dispatchSort({ + header: params[0], + nextSortOrder: params[1], + }); + }, + }, +}; diff --git a/src/app/mixins/objectPagesMixins/permissionsTabMixin/permissionsTabMixin.js b/src/app/mixins/objectPagesMixins/permissionsTabMixin/permissionsTabMixin.js new file mode 100644 index 00000000..1a38be52 --- /dev/null +++ b/src/app/mixins/objectPagesMixins/permissionsTabMixin/permissionsTabMixin.js @@ -0,0 +1,146 @@ +import getNamespacedState from '@webitel/ui-sdk/src/store/helpers/getNamespacedState'; +import { mapActions, mapState } from 'vuex'; +import AccessMode from '../../../../modules/permissions/modules/objects/store/_internals/enums/AccessMode.enum'; +import tableComponentMixin from '../objectTableMixin/tableComponentMixin'; +import openedTabComponentMixin from '../openedObjectTabMixin/openedTabComponentMixin'; + +export default { + mixins: [openedTabComponentMixin, tableComponentMixin], + data: () => ({ + isRoleSelectPopup: false, + accessMode: AccessMode, + }), + destroyed() { + this.resetState(); + }, + computed: { + ...mapState({ + parentId(state) { + return getNamespacedState(state, `${this.namespace}`).itemId; + }, + headersValue(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).headers; + }, + dataListValue(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).dataList; + }, + page(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).page; + }, + size(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).size; + }, + search(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).search; + }, + isNext(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).isNextPage; + }, + permissionId() { + return this.$route.params.permissionId; + }, + }), + headers() { + if (!this.headersValue) return []; + return this.headersValue.map((header) => ({ + ...header, + text: + typeof header.locale === 'string' ? this.$t(header.locale) : this.$t(...header.locale), + })); + }, + dataList() { + return this.dataListValue.map((item) => { + const access = {}; + Object.keys(item.access).forEach((rule) => { + access[rule] = { + ...item.access[rule], + name: this.$t(`objects.permissions.object.accessMode.${item.access[rule].id}`), + }; + }); + return { + ...item, + access, + }; + }); + }, + accessOptions() { + return Object.values(AccessMode).map((mode) => ({ + id: mode, + name: this.$t(`objects.permissions.object.accessMode.${mode}`), + })); + }, + }, + + methods: { + ...mapActions({ + setParentId(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/SET_PARENT_ITEM_ID`, payload); + }, + loadDataList(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/LOAD_DATA_LIST`, payload); + }, + setSize(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/SET_SIZE`, payload); + }, + setSearch(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/SET_SEARCH`, payload); + }, + nextPage(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/NEXT_PAGE`, payload); + }, + prevPage(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/PREV_PAGE`, payload); + }, + dispatchSort(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/SORT`, payload); + }, + changeCreateAccessMode(dispatch, payload) { + return dispatch( + `${this.namespace}/${this.subNamespace}/CHANGE_CREATE_ACCESS_MODE`, + payload, + ); + }, + changeReadAccessMode(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/CHANGE_READ_ACCESS_MODE`, payload); + }, + changeUpdateAccessMode(dispatch, payload) { + return dispatch( + `${this.namespace}/${this.subNamespace}/CHANGE_UPDATE_ACCESS_MODE`, + payload, + ); + }, + changeDeleteAccessMode(dispatch, payload) { + return dispatch( + `${this.namespace}/${this.subNamespace}/CHANGE_DELETE_ACCESS_MODE`, + payload, + ); + }, + resetState(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/RESET_ITEM_STATE`, payload); + }, + }), + addItem() { + return this.$router.push({ + ...this.$route, + params: { permissionId: 'new' }, + }); + }, + openRoleSelectPopup() { + this.isRoleSelectPopup = true; + }, + closeRoleSelectPopup() { + this.$router.go(-1); + this.isRoleSelectPopup = false; + }, + }, + watch: { + permissionId: { + handler(value) { + if (value === 'new') { + this.openRoleSelectPopup(); + } + }, + immediate: true, + }, + } +}; diff --git a/src/app/mixins/objectPagesMixins/permissionsTabMixin/permissionsTabRolePopupMixin.js b/src/app/mixins/objectPagesMixins/permissionsTabMixin/permissionsTabRolePopupMixin.js new file mode 100644 index 00000000..fed0427d --- /dev/null +++ b/src/app/mixins/objectPagesMixins/permissionsTabMixin/permissionsTabRolePopupMixin.js @@ -0,0 +1,60 @@ +import getNamespacedState from '@webitel/ui-sdk/src/store/helpers/getNamespacedState'; +import { mapActions, mapState } from 'vuex'; +import RolesAPI from '../../../../modules/permissions/modules/roles/api/roles'; + +export default { + props: { + namespace: { + type: String, + required: true, + }, + subNamespace: { + type: String, + required: true, + }, + }, + data: () => ({ + newGrantee: '', + }), + computed: { + ...mapState({ + dataList(state) { + return getNamespacedState(state, `${this.namespace}/${this.subNamespace}`).dataList; + }, + }), + }, + methods: { + ...mapActions({ + addRolePermissions(dispatch, payload) { + return dispatch(`${this.namespace}/${this.subNamespace}/ADD_ROLE_PERMISSIONS`, payload); + }, + }), + async save() { + try { + await this.addRolePermissions(this.newGrantee); + this.close(); + } catch (err) { + throw err; + } + }, + + // filter new roles + async getAvailableGrantees(params) { + const roles = await this.loadRoles(params); + roles.items = roles.items.filter( + (role) => !this.dataList.some((usedRoles) => role.id === usedRoles.grantee.id), + ); + return roles; + }, + async loadRoles(params) { + const fields = ['name', 'id', 'user']; + return RolesAPI.getExtendedRoles({ + ...params, + fields, + }); + }, + close() { + this.$emit('close'); + }, + }, +}; diff --git a/src/app/mixins/objectPagesMixins/readme.md b/src/app/mixins/objectPagesMixins/readme.md new file mode 100644 index 00000000..c2e58bce --- /dev/null +++ b/src/app/mixins/objectPagesMixins/readme.md @@ -0,0 +1,8 @@ +# objectPagesMixins +--- +Implement common logic of a default type of UI Component like: + +* Object **tables** (`the-users.vue`) +* Opened object card components wrapper: **openedObject** (`opened-user.vue`) +* Opened object tabs: **openedTabComponent** (`opened-user-tokens.vue`) +* etc.. \ No newline at end of file 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 203cd344..ba7100e9 100644 --- a/src/app/router/index.js +++ b/src/app/router/index.js @@ -18,6 +18,9 @@ import TheStartPage 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) => { @@ -117,6 +120,7 @@ const routes = [ }, ], }, + ...ContactGroupsRoutes, ], }, { 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 }, }); diff --git a/src/main.js b/src/main.js index 8f8e6386..2480f82c 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ import i18n from './app/locale/i18n'; import WebitelUi from './app/plugins/webitel-ui'; import store from './app/store'; import './app/assets/icons/sprite'; +import ActionComponents from './app/components/actions'; const setTokenFromUrl = () => { try { @@ -29,11 +30,20 @@ const fetchConfig = async () => { return response.json(); }; -const initApp = () => createApp(App) -.use(store) -.use(router) -.use(i18n) -.use(...WebitelUi); +const initApp = () => { + const app = createApp(App) + .use(store) + .use(router) + .use(i18n) + .use(...WebitelUi) + + ActionComponents.forEach((component) => { + app.component(component.name, component); + }); + + return app; +}; + (async () => { let config; @@ -50,3 +60,4 @@ const initApp = () => createApp(App) app.mount('#app'); } })(); + diff --git a/src/modules/_shared/permissions-tab/api/PermissionsAPI.js b/src/modules/_shared/permissions-tab/api/PermissionsAPI.js new file mode 100644 index 00000000..038ed23f --- /dev/null +++ b/src/modules/_shared/permissions-tab/api/PermissionsAPI.js @@ -0,0 +1,35 @@ +import APIPermissionsGetter from '../../../../app/api/PermissionsAPIService/APIPermissionsGetter'; +import APIPermissionsPatcher from '../../../../app/api/PermissionsAPIService/APIPermissionsPatcher'; + +export default class PermissionsAPI { + constructor(url) { + this.url = url; + this._permissionsGetter = new APIPermissionsGetter(url); + this._permissionsPatcher = new APIPermissionsPatcher(url); + } + + static getListByUrl = (url) => { + const permissionsGetter = new APIPermissionsGetter(url); + return (params) => permissionsGetter.getList(params); + }; + + static patchByUrl = (url) => { + const permissionsPatcher = new APIPermissionsPatcher(url); + return (parentId, changes) => + permissionsPatcher.patchItem({ + id: parentId, + changes, + }); + }; + + getList(params) { + return this._permissionsGetter.getList(params); + } + + patch(parentId, changes) { + return this._permissionsPatcher.patchItem({ + id: parentId, + changes, + }); + } +} diff --git a/src/modules/_shared/permissions-tab/components/_internals/permissions-role-column.vue b/src/modules/_shared/permissions-tab/components/_internals/permissions-role-column.vue new file mode 100644 index 00000000..d9ee8d3b --- /dev/null +++ b/src/modules/_shared/permissions-tab/components/_internals/permissions-role-column.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/modules/_shared/permissions-tab/components/_internals/permissions-role-select.vue b/src/modules/_shared/permissions-tab/components/_internals/permissions-role-select.vue new file mode 100644 index 00000000..ad2c8b97 --- /dev/null +++ b/src/modules/_shared/permissions-tab/components/_internals/permissions-role-select.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/modules/_shared/permissions-tab/components/permissions-tab-role-popup.vue b/src/modules/_shared/permissions-tab/components/permissions-tab-role-popup.vue new file mode 100644 index 00000000..c062b0f5 --- /dev/null +++ b/src/modules/_shared/permissions-tab/components/permissions-tab-role-popup.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/modules/_shared/permissions-tab/components/permissions-tab.vue b/src/modules/_shared/permissions-tab/components/permissions-tab.vue new file mode 100644 index 00000000..7151d130 --- /dev/null +++ b/src/modules/_shared/permissions-tab/components/permissions-tab.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/src/modules/contacts/api/CommunicationTypesAPI.js b/src/modules/contacts/api/CommunicationTypesAPI.js index d6356648..92182778 100644 --- a/src/modules/contacts/api/CommunicationTypesAPI.js +++ b/src/modules/contacts/api/CommunicationTypesAPI.js @@ -1,13 +1,13 @@ import { getDefaultGetListResponse, getDefaultGetParams, -} from '@webitel/ui-sdk/src/api/defaults'; +} from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { camelToSnake, merge, notify, sanitize, snakeToCamel, starToSearch, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import { CommunicationTypeServiceApiFactory } from 'webitel-sdk'; import instance from '../../../app/api/instance'; import configuration from '../../../app/api/openAPIConfig'; diff --git a/src/modules/contacts/api/ContactsAPI.js b/src/modules/contacts/api/ContactsAPI.js index 3b08e35d..91d3b486 100644 --- a/src/modules/contacts/api/ContactsAPI.js +++ b/src/modules/contacts/api/ContactsAPI.js @@ -1,11 +1,11 @@ -import { getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults'; +import { getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { camelToSnake, merge, notify, sanitize, snakeToCamel, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import { ContactsApiFactory } from 'webitel-sdk'; import getDefaultGetListResponse from '../../../app/api/defaults/getDefaultGetListResponse'; diff --git a/src/modules/contacts/api/LabelsAPI.js b/src/modules/contacts/api/LabelsAPI.js index d3963398..231ca9bf 100644 --- a/src/modules/contacts/api/LabelsAPI.js +++ b/src/modules/contacts/api/LabelsAPI.js @@ -1,9 +1,9 @@ -import { getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults'; +import { getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { camelToSnake, merge, notify, sanitize, snakeToCamel, starToSearch, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import { LabelsApiFactory } from 'webitel-sdk'; import instance from '../../../app/api/instance'; import configuration from '../../../app/api/openAPIConfig'; diff --git a/src/modules/contacts/api/TimezonesAPI.js b/src/modules/contacts/api/TimezonesAPI.js index 78dcb751..dca22aa7 100644 --- a/src/modules/contacts/api/TimezonesAPI.js +++ b/src/modules/contacts/api/TimezonesAPI.js @@ -1,12 +1,12 @@ import { getDefaultGetListResponse, getDefaultGetParams, -} from '@webitel/ui-sdk/src/api/defaults'; +} from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { camelToSnake, merge, notify, sanitize, snakeToCamel, starToSearch, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import { CalendarServiceApiFactory } from 'webitel-sdk'; import instance from '../../../app/api/instance'; import configuration from '../../../app/api/openAPIConfig'; diff --git a/src/modules/contacts/api/UsersAPI.js b/src/modules/contacts/api/UsersAPI.js index b34a46e5..cdd5ca4c 100644 --- a/src/modules/contacts/api/UsersAPI.js +++ b/src/modules/contacts/api/UsersAPI.js @@ -1,13 +1,13 @@ import { getDefaultGetListResponse, getDefaultGetParams, -} from '@webitel/ui-sdk/src/api/defaults'; +} from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { camelToSnake, merge, notify, snakeToCamel, starToSearch, log, sanitize, generateUrl, mergeEach, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import instance from '../../../app/api/instance'; const baseUrl = '/users'; diff --git a/src/modules/contacts/modules/emails/api/EmailsAPI.js b/src/modules/contacts/modules/emails/api/EmailsAPI.js index 4edb3441..68bf856c 100644 --- a/src/modules/contacts/modules/emails/api/EmailsAPI.js +++ b/src/modules/contacts/modules/emails/api/EmailsAPI.js @@ -1,4 +1,4 @@ -import { getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults'; +import { getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { camelToSnake, merge, @@ -7,7 +7,7 @@ import applyTransform, { sanitize, snakeToCamel, starToSearch, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import { EmailsApiFactory } from 'webitel-sdk'; import getDefaultGetListResponse from '../../../../../app/api/defaults/getDefaultGetListResponse'; diff --git a/src/modules/contacts/modules/messaging/api/MessagingAPI.js b/src/modules/contacts/modules/messaging/api/MessagingAPI.js index c3d7d690..52f6fa49 100644 --- a/src/modules/contacts/modules/messaging/api/MessagingAPI.js +++ b/src/modules/contacts/modules/messaging/api/MessagingAPI.js @@ -1,8 +1,8 @@ -import { getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults'; +import { getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { merge, notify, sanitize, snakeToCamel, starToSearch -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import { IMClientsApiFactory } from 'webitel-sdk'; import getDefaultGetListResponse from '../../../../../app/api/defaults/getDefaultGetListResponse'; diff --git a/src/modules/contacts/modules/permissions/api/PermissionsAPI.js b/src/modules/contacts/modules/permissions/api/PermissionsAPI.js index 7d91bd2a..8871b9a0 100644 --- a/src/modules/contacts/modules/permissions/api/PermissionsAPI.js +++ b/src/modules/contacts/modules/permissions/api/PermissionsAPI.js @@ -1,13 +1,13 @@ import { getDefaultGetListResponse, getDefaultGetParams, -} from '@webitel/ui-sdk/src/api/defaults'; +} from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { camelToSnake, merge, notify, snakeToCamel, starToSearch, log, sanitize, generateUrl, mergeEach, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import instance from '../../../../../app/api/instance'; const baseUrl = '/contacts'; diff --git a/src/modules/contacts/modules/permissions/api/RolesAPI.js b/src/modules/contacts/modules/permissions/api/RolesAPI.js index a23dcf21..8a151c52 100644 --- a/src/modules/contacts/modules/permissions/api/RolesAPI.js +++ b/src/modules/contacts/modules/permissions/api/RolesAPI.js @@ -1,13 +1,13 @@ import { getDefaultGetListResponse, getDefaultGetParams, -} from '@webitel/ui-sdk/src/api/defaults'; +} from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { camelToSnake, merge, notify, snakeToCamel, starToSearch, log, sanitize, generateUrl, mergeEach, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import instance from '../../../../../app/api/instance'; const baseUrl = '/roles'; diff --git a/src/modules/contacts/modules/phones/api/PhonesAPI.js b/src/modules/contacts/modules/phones/api/PhonesAPI.js index b00a2daa..0145ebcb 100644 --- a/src/modules/contacts/modules/phones/api/PhonesAPI.js +++ b/src/modules/contacts/modules/phones/api/PhonesAPI.js @@ -1,4 +1,4 @@ -import { getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults'; +import { getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { camelToSnake, merge, @@ -7,7 +7,7 @@ import applyTransform, { sanitize, snakeToCamel, starToSearch, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import { PhonesApiFactory } from 'webitel-sdk'; import getDefaultGetListResponse from '../../../../../app/api/defaults/getDefaultGetListResponse'; diff --git a/src/modules/contacts/modules/timeline/api/TimelineAPI.js b/src/modules/contacts/modules/timeline/api/TimelineAPI.js index 13edcb0e..c9893a44 100644 --- a/src/modules/contacts/modules/timeline/api/TimelineAPI.js +++ b/src/modules/contacts/modules/timeline/api/TimelineAPI.js @@ -1,7 +1,7 @@ import applyTransform, { merge, notify, sanitize, snakeToCamel, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import deepCopy from 'deep-copy'; import { TimelineApiFactory, WebitelContactsTimelineEventType } from 'webitel-sdk'; import getDefaultGetListResponse diff --git a/src/modules/contacts/modules/timeline/modules/calls/api/HistoryAPI.js b/src/modules/contacts/modules/timeline/modules/calls/api/HistoryAPI.js index c93ba85b..679c818c 100644 --- a/src/modules/contacts/modules/timeline/modules/calls/api/HistoryAPI.js +++ b/src/modules/contacts/modules/timeline/modules/calls/api/HistoryAPI.js @@ -1,9 +1,9 @@ -import { getDefaultGetListResponse } from '@webitel/ui-sdk/src/api/defaults'; +import { getDefaultGetListResponse } from '@webitel/ui-sdk/src/api/defaults/index.js'; import { merge, notify, snakeToCamel, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import applyTransform from '@webitel/ui-sdk/src/api/transformers/applyTransform'; import { CallServiceApiFactory } from 'webitel-sdk'; diff --git a/src/modules/contacts/modules/timeline/modules/chats/api/MessageHistoryAPI.js b/src/modules/contacts/modules/timeline/modules/chats/api/MessageHistoryAPI.js index abad36ba..4e62ed8c 100644 --- a/src/modules/contacts/modules/timeline/modules/chats/api/MessageHistoryAPI.js +++ b/src/modules/contacts/modules/timeline/modules/chats/api/MessageHistoryAPI.js @@ -5,7 +5,7 @@ import applyTransform, { merge, camelToSnake, generateUrl, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import instance from '../../../../../../../app/api/instance'; diff --git a/src/modules/contacts/modules/variables/api/VariablesAPI.js b/src/modules/contacts/modules/variables/api/VariablesAPI.js index 21991c49..9097bbd4 100644 --- a/src/modules/contacts/modules/variables/api/VariablesAPI.js +++ b/src/modules/contacts/modules/variables/api/VariablesAPI.js @@ -1,7 +1,7 @@ import { getDefaultGetListResponse, getDefaultGetParams, -} from '@webitel/ui-sdk/src/api/defaults'; +} from '@webitel/ui-sdk/src/api/defaults/index.js'; import applyTransform, { camelToSnake, merge, @@ -9,7 +9,7 @@ import applyTransform, { sanitize, snakeToCamel, starToSearch, -} from '@webitel/ui-sdk/src/api/transformers'; +} from '@webitel/ui-sdk/src/api/transformers/index.js'; import { VariablesApiFactory } from 'webitel-sdk'; import instance from '../../../../../app/api/instance'; diff --git a/src/modules/lookups/modules/contact-groups/modules/cgroups/api/contact-groups.js b/src/modules/lookups/modules/contact-groups/modules/cgroups/api/contact-groups.js new file mode 100644 index 00000000..92c1058b --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/modules/cgroups/api/contact-groups.js @@ -0,0 +1,141 @@ +import { + getDefaultGetListResponse, + getDefaultGetParams, +} from '@webitel/ui-sdk/src/api/defaults/index.js'; +import applyTransform, { + camelToSnake, + merge, + mergeEach, + log, + notify, + sanitize, + snakeToCamel, + starToSearch, + generateUrl, +} from '@webitel/ui-sdk/src/api/transformers/index.js'; + +import instance from '../../../../../../../app/api/instance'; + +const contactGroupsUrl = '/contacts/groups'; + +const getContactGroupsList = async (params) => { + const fieldsToSend = ['page', 'size', 'fields', 'sort', 'id', 'q', 'name']; + + const defaultObject = { + }; + + const url = applyTransform(params, [ + starToSearch('search'), + (params) => ({ ...params, q: params.search }), + sanitize(fieldsToSend), + camelToSnake(), + generateUrl(contactGroupsUrl), + ]); + + try { + const response = await instance.get(url); + + const { items, next } = applyTransform(response.data, [ + snakeToCamel(), + merge(getDefaultGetListResponse()), + ]); + return { + items: applyTransform(items, [mergeEach(defaultObject), log]), + next, + }; + + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const getAgent = async ({ itemId: id }) => { + const url = `${contactGroupsUrl}/${id}`; + const defaultObject = { + }; + + try { + const response = await instance.get(url); + + return applyTransform(response.data.group, [ + snakeToCamel(), + merge(defaultObject), + ]); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const fieldsToSend = [ + 'name', + 'description', +]; + +const addAgent = async ({ itemInstance }) => { + const item = applyTransform(itemInstance, [ + sanitize(fieldsToSend), + camelToSnake(), + ]); + + try { + const response = await instance.post(contactGroupsUrl, item); + return applyTransform(response.data, []); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const patchAgent = async ({ itemInstance, id }) => { + const url = `${contactGroupsUrl}/${id}`; + + const item = applyTransform(itemInstance, [ + sanitize(fieldsToSend), + camelToSnake(), + ]); + + try { + const response = await instance.patch(url, item); + return applyTransform(response.data, []); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const updateAgent = async ({ itemInstance, itemId: id }) => { + const url = `${contactGroupsUrl}/${id}`; + + const item = applyTransform(itemInstance, [ + sanitize(fieldsToSend), + camelToSnake(), + ]); + + try { + const response = await instance.put(url, item); + return applyTransform(response.data, []); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const deleteAgent = async ({ id }) => { + const url = `${contactGroupsUrl}/${id}` + + try { + const response = await instance.delete(url); + return applyTransform(response.data, []); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const ContactGroupsAPI = { + getList: getContactGroupsList, + getListGroups: getContactGroupsList, + get: getAgent, + add: addAgent, + patch: patchAgent, + update: updateAgent, + delete: deleteAgent, +}; + +export default ContactGroupsAPI; diff --git a/src/modules/lookups/modules/contact-groups/modules/cgroups/assets/adm-agent-history-dark.svg b/src/modules/lookups/modules/contact-groups/modules/cgroups/assets/adm-agent-history-dark.svg new file mode 100644 index 00000000..8158c374 --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/modules/cgroups/assets/adm-agent-history-dark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/modules/lookups/modules/contact-groups/modules/cgroups/assets/adm-agent-history-light.svg b/src/modules/lookups/modules/contact-groups/modules/cgroups/assets/adm-agent-history-light.svg new file mode 100644 index 00000000..67b91b1f --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/modules/cgroups/assets/adm-agent-history-light.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/modules/lookups/modules/contact-groups/modules/cgroups/components/opened-contact-groups-general.vue b/src/modules/lookups/modules/contact-groups/modules/cgroups/components/opened-contact-groups-general.vue new file mode 100644 index 00000000..fc038d17 --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/modules/cgroups/components/opened-contact-groups-general.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/modules/lookups/modules/contact-groups/modules/cgroups/components/opened-contact-groups.vue b/src/modules/lookups/modules/contact-groups/modules/cgroups/components/opened-contact-groups.vue new file mode 100644 index 00000000..7444c58f --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/modules/cgroups/components/opened-contact-groups.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/src/modules/lookups/modules/contact-groups/modules/cgroups/components/the-contact-groups.vue b/src/modules/lookups/modules/contact-groups/modules/cgroups/components/the-contact-groups.vue new file mode 100644 index 00000000..0cd7f9b8 --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/modules/cgroups/components/the-contact-groups.vue @@ -0,0 +1,235 @@ + + + + + diff --git a/src/modules/lookups/modules/contact-groups/modules/cgroups/router/_internals/ContactGroupsRouteNames.enum.js b/src/modules/lookups/modules/contact-groups/modules/cgroups/router/_internals/ContactGroupsRouteNames.enum.js new file mode 100644 index 00000000..226de5f8 --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/modules/cgroups/router/_internals/ContactGroupsRouteNames.enum.js @@ -0,0 +1,6 @@ +import RouteNames from "../../../../../../../../app/router/_internals/RouteNames.enum.js"; + +export default Object.freeze({ + GENERAL: `${RouteNames.CONTACT_GROUPS}-general`, + PERMISSIONS: `${RouteNames.CONTACT_GROUPS}-permissions`, +}); diff --git a/src/modules/lookups/modules/contact-groups/modules/cgroups/router/contact-groups.js b/src/modules/lookups/modules/contact-groups/modules/cgroups/router/contact-groups.js new file mode 100644 index 00000000..90869368 --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/modules/cgroups/router/contact-groups.js @@ -0,0 +1,39 @@ +import RouteNames from "../../../../../../../app/router/_internals/RouteNames.enum.js"; +import ContactGroupsRouteNames from "./_internals/ContactGroupsRouteNames.enum.js"; + +const ContactGroups = () => import( '../components/the-contact-groups.vue'); +const Agent = () => import('../components/opened-contact-groups.vue'); +const General = () => import("../components/opened-contact-groups-general.vue"); +const Permissions = () => import("../../../../../../_shared/permissions-tab/components/permissions-tab.vue"); + +import {checkRouteAccess} from "../../../../../../../app/router/_internals/guards.js"; + + +const ContactGroupsRoutes = [ + { + path: '/lookups/contact-groups', + name: RouteNames.CONTACT_GROUPS, + component: ContactGroups, + beforeEnter: checkRouteAccess, + }, + { + path: '/lookups/contact-groups/:id', + name: `${RouteNames.CONTACT_GROUPS}-card`, + redirect: { name: ContactGroupsRouteNames.GENERAL }, + component: Agent, + beforeEnter: checkRouteAccess, + children: [ + { + path: 'general', + name: ContactGroupsRouteNames.GENERAL, + component: General, + },{ + path: 'permissions/:permissionId?', + name: ContactGroupsRouteNames.PERMISSIONS, + component: Permissions, + } + ], + }, +] + +export default ContactGroupsRoutes; diff --git a/src/modules/lookups/modules/contact-groups/modules/cgroups/store/_internals/headers.js b/src/modules/lookups/modules/contact-groups/modules/cgroups/store/_internals/headers.js new file mode 100644 index 00000000..ff2ea4fd --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/modules/cgroups/store/_internals/headers.js @@ -0,0 +1,16 @@ +import { SortSymbols } from '@webitel/ui-sdk/src/scripts/sortQueryAdapters'; + +export default [ + { + value: 'name', + locale: 'objects.name', + field: 'name', + sort: SortSymbols.NONE, + }, + { + value: 'description', + locale: 'objects.description', + field: 'description', + sort: SortSymbols.NONE, + }, +]; diff --git a/src/modules/lookups/modules/contact-groups/modules/cgroups/store/contact-groups.js b/src/modules/lookups/modules/contact-groups/modules/cgroups/store/contact-groups.js new file mode 100644 index 00000000..13c73f34 --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/modules/cgroups/store/contact-groups.js @@ -0,0 +1,46 @@ +import HistoryStoreModule from '../../../../../../../app/store/BaseStoreModules/StoreModules/HistoryStoreModule/HistoryStoreModule'; +import ObjectStoreModule from '../../../../../../../app/store/BaseStoreModules/StoreModules/ObjectStoreModule'; +import PermissionsStoreModule from '../../../../../../../app/store/BaseStoreModules/StoreModules/PermissionsStoreModule/PermissionsStoreModule'; +import ContactGroupsAPI from '../api/contact-groups'; +import headers from './_internals/headers'; + +const resettableState = { + itemInstance: { + user: {}, + team: {}, + supervisor: [], + auditor: [], + region: {}, + progressiveCount: 1, + chatCount: 1, + taskCount: 1, + isSupervisor: false, + greetingMedia: {}, + }, +}; + +const actions = { + RESET_ITEM_STATE: async (context) => { + context.commit('RESET_ITEM_STATE'); + }, +}; + +const PERMISSIONS_API_URL = '/contacts/groups'; +const permissions = new PermissionsStoreModule() + .generateAPIActions(PERMISSIONS_API_URL) + .getModule(); + +const history = new HistoryStoreModule() + .generateGetListAction(ContactGroupsAPI.getAgentHistory) + .getModule(); + +const contactGroups = new ObjectStoreModule({ resettableState, headers }) + .attachAPIModule(ContactGroupsAPI) + .generateAPIActions() + .setChildModules({ + history, + permissions, + }) + .getModule({ actions }); + +export default contactGroups; diff --git a/src/modules/lookups/modules/contact-groups/store/cgroups.js b/src/modules/lookups/modules/contact-groups/store/cgroups.js new file mode 100644 index 00000000..995603d6 --- /dev/null +++ b/src/modules/lookups/modules/contact-groups/store/cgroups.js @@ -0,0 +1,10 @@ +import contactGroups from '../modules/cgroups/store/contact-groups'; + +const modules = { + groups: contactGroups, +}; + +export default { + namespaced: true, + modules, +}; diff --git a/src/modules/permissions/modules/objects/api/objects.js b/src/modules/permissions/modules/objects/api/objects.js new file mode 100644 index 00000000..9af250ac --- /dev/null +++ b/src/modules/permissions/modules/objects/api/objects.js @@ -0,0 +1,78 @@ +import { getDefaultGetListResponse, getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults/index.js'; +import applyTransform, { + camelToSnake, + generateUrl, + merge, + mergeEach, + notify, + sanitize, + snakeToCamel, + starToSearch, +} from '@webitel/ui-sdk/src/api/transformers/index.js'; +import instance from '../../../../../app/api/instance'; + +const baseUrl = '/objclass'; + +const getObjectList = async (params) => { + const fieldsToSend = ['page', 'size', 'q', 'sort', 'fields', 'id']; + + const defaultObject = { + // default object prototype, to merge response with it to get all fields + class: '', + obac: false, + rbac: false, + id: 0, + }; + + const url = applyTransform(params, [ + merge(getDefaultGetParams()), + starToSearch('search'), + (params) => ({ ...params, q: params.search }), + sanitize(fieldsToSend), + camelToSnake(), + generateUrl(baseUrl), + ]); + try { + const response = await instance.get(url); + const { items, next } = applyTransform(response.data, [ + snakeToCamel(), + merge(getDefaultGetListResponse()), + ]); + return { + items: applyTransform(items, [mergeEach(defaultObject)]), + next, + }; + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const getObject = async ({ itemId: id }) => { + const url = `${baseUrl}/${id}`; + + try { + const response = await instance.get(url); + return applyTransform(response.data, [snakeToCamel()]); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const patchObject = async ({ changes, id }) => { + const body = applyTransform(changes, [camelToSnake()]); + const url = `${baseUrl}/${id}`; + try { + const response = await instance.patch(url, body); + return applyTransform(response.data, [snakeToCamel()]); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const ObjectsAPI = { + getList: getObjectList, + get: getObject, + patch: patchObject, +}; + +export default ObjectsAPI; diff --git a/src/modules/permissions/modules/objects/store/_internals/enums/AccessMode.enum.js b/src/modules/permissions/modules/objects/store/_internals/enums/AccessMode.enum.js new file mode 100644 index 00000000..6b0e849a --- /dev/null +++ b/src/modules/permissions/modules/objects/store/_internals/enums/AccessMode.enum.js @@ -0,0 +1,5 @@ +export default Object.freeze({ + FORBIDDEN: 1, + ALLOW: 2, + MANAGE: 3, +}); diff --git a/src/modules/permissions/modules/objects/store/objects.js b/src/modules/permissions/modules/objects/store/objects.js new file mode 100644 index 00000000..b9da4dd4 --- /dev/null +++ b/src/modules/permissions/modules/objects/store/objects.js @@ -0,0 +1,15 @@ +import ObjectStoreModule from '../../../../../app/store/BaseStoreModules/StoreModules/ObjectStoreModule'; +import ObjectsAPI from '../api/objects'; + +const actions = { + RESET_ITEM_STATE: (context) => { + context.commit('RESET_ITEM_STATE'); + }, +}; + +const objects = new ObjectStoreModule() + .attachAPIModule(ObjectsAPI) + .generateAPIActions() + .getModule({ actions }); + +export default objects; diff --git a/src/modules/permissions/modules/roles/api/roles.js b/src/modules/permissions/modules/roles/api/roles.js new file mode 100644 index 00000000..9a742e48 --- /dev/null +++ b/src/modules/permissions/modules/roles/api/roles.js @@ -0,0 +1,192 @@ +import { getDefaultGetListResponse, getDefaultGetParams } from '@webitel/ui-sdk/src/api/defaults/index.js'; +import applyTransform, { + camelToSnake, + generateUrl, + merge, + notify, + sanitize, + snakeToCamel, + starToSearch, +} from '@webitel/ui-sdk/src/api/transformers/index.js'; +import ApplicationsAccess from '@webitel/ui-sdk/src/modules/Userinfo/classes/ApplicationsAccess'; +import deepCopy from 'deep-copy'; +import instance from '../../../../../app/api/instance'; + +const baseUrl = '/roles'; +const fieldsToSend = ['name', 'description', 'permissions', 'metadata']; + +const preRequestHandler = (item) => { + const copy = deepCopy(item); + copy.metadata.access = ApplicationsAccess.minify(copy.metadata.access); + return copy; +}; + +const getRoleList = async (params) => { + const fieldsToSend = ['page', 'size', 'q', 'sort', 'fields', 'id']; + + const url = applyTransform(params, [ + merge(getDefaultGetParams()), + starToSearch('search'), + (params) => ({ ...params, q: params.search }), + sanitize(fieldsToSend), + camelToSnake(), + generateUrl(baseUrl), + ]); + try { + const response = await instance.get(url); + const { items, next } = applyTransform(response.data, [ + snakeToCamel(), + merge(getDefaultGetListResponse()), + ]); + return { + items, + next, + }; + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const getRole = async ({ itemId: id }) => { + const defaultObject = { + name: '', + description: '', + permissions: [], + metadata: {}, + }; + + const itemResponseHandler = (response) => { + const copy = deepCopy(response); + copy.metadata.access = new ApplicationsAccess({ + access: copy.metadata.access, + }).getAccess(); + return copy; + }; + + const url = `${baseUrl}/${id}?fields=metadata&fields=permissions&fields=name&fields=description`; + + try { + const response = await instance.get(url); + return applyTransform(response.data, [ + snakeToCamel(), + merge(defaultObject), + itemResponseHandler, + ]); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const getExtendedRoles = async (params) => { + const fieldsToSend = ['page', 'size', 'q', 'sort', 'fields', 'id']; + + const url = applyTransform(params, [ + merge(getDefaultGetParams()), + starToSearch('search'), + (params) => ({ ...params, q: params.search }), + sanitize(fieldsToSend), + camelToSnake(), + generateUrl(baseUrl), + ]); + try { + const response = await instance.get(url); + const { items, next } = applyTransform(response.data, [ + snakeToCamel(), + merge(getDefaultGetListResponse()), + ]); + return { + items, + next, + }; + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const addRole = async ({ itemInstance }) => { + const item = applyTransform(itemInstance, [ + preRequestHandler, + sanitize(fieldsToSend), + camelToSnake(), + ]); + try { + const response = await instance.post(baseUrl, item); + return applyTransform(response.data, [snakeToCamel()]); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const updateRole = async ({ itemInstance, itemId: id }) => { + const item = applyTransform(itemInstance, [ + preRequestHandler, + sanitize(fieldsToSend), + camelToSnake(), + ]); + + const url = `${baseUrl}/${id}`; + try { + const response = await instance.put(url, item); + return applyTransform(response.data, [snakeToCamel()]); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const deleteRole = async ({ id }) => { + const url = `${baseUrl}/${id}`; + try { + const response = await instance.delete(url); + return applyTransform(response.data, []); + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const getRolesLookup = (params) => + getRoleList({ + ...params, + fields: params.fields || ['id', 'name'], + }); + +const PERMISSIONS_LIST_URL = '/permissions'; + +const getPermissionsOptions = async (params) => { + const fieldsToSend = ['page', 'size', 'q', 'sort', 'fields', 'id']; + + const url = applyTransform(params, [ + merge(getDefaultGetParams()), + starToSearch('search'), + (params) => ({ ...params, q: params.search }), + sanitize(fieldsToSend), + camelToSnake(), + generateUrl(PERMISSIONS_LIST_URL), + ]); + try { + const response = await instance.get(url); + const { items, next } = applyTransform(response.data, [ + snakeToCamel(), + merge(getDefaultGetListResponse()), + ]); + return { + items, + next, + }; + } catch (err) { + throw applyTransform(err, [notify]); + } +}; + +const RolesAPI = { + getList: getRoleList, + get: getRole, + add: addRole, + update: updateRole, + delete: deleteRole, + getLookup: getRolesLookup, + + getExtendedRoles, + getPermissionsOptions, +}; + +export default RolesAPI; diff --git a/src/modules/permissions/modules/roles/store/_internals/headers.js b/src/modules/permissions/modules/roles/store/_internals/headers.js new file mode 100644 index 00000000..ff2ea4fd --- /dev/null +++ b/src/modules/permissions/modules/roles/store/_internals/headers.js @@ -0,0 +1,16 @@ +import { SortSymbols } from '@webitel/ui-sdk/src/scripts/sortQueryAdapters'; + +export default [ + { + value: 'name', + locale: 'objects.name', + field: 'name', + sort: SortSymbols.NONE, + }, + { + value: 'description', + locale: 'objects.description', + field: 'description', + sort: SortSymbols.NONE, + }, +]; diff --git a/src/modules/permissions/modules/roles/store/roles.js b/src/modules/permissions/modules/roles/store/roles.js new file mode 100644 index 00000000..30c8672b --- /dev/null +++ b/src/modules/permissions/modules/roles/store/roles.js @@ -0,0 +1,113 @@ +import ApplicationsAccess from '@webitel/ui-sdk/src/modules/Userinfo/classes/ApplicationsAccess'; +import deepCopy from 'deep-copy'; +import ObjectStoreModule from '../../../../../app/store/BaseStoreModules/StoreModules/ObjectStoreModule'; +import RolesAPI from '../api/roles'; +import headers from './_internals/headers'; + +const resettableState = { + itemInstance: { + name: '', + description: '', + permissions: [], + metadata: { + access: new ApplicationsAccess().getAccess(), + }, + }, +}; + +const actions = { + GET_ITEM: (context) => + RolesAPI.get({ + ...context.state, + query: { + fields: '*,metadata.access', + }, + }), + ADD_ROLE_PERMISSION: (context, permission) => { + const value = context.state.itemInstance.permissions.concat(permission); + return context.dispatch('SET_ITEM_PROPERTY', { + prop: 'permissions', + value, + }); + }, + UPDATE_ROLE_PERMISSION: (context, { index, permission }) => { + const value = [...context.state.itemInstance.permissions]; + value.splice(index, 1, permission); + return context.dispatch('SET_ITEM_PROPERTY', { + prop: 'permissions', + value, + }); + }, + DELETE_ROLE_PERMISSION: (context, deleted) => { + let action = 'DELETE_SINGLE_PERMISSION'; + if (Array.isArray(deleted)) { + if (deleted.length) action = 'DELETE_BULK_PERMISSIONS'; + else action = 'DELETE_ALL_PERMISSIONS'; + } + return context.dispatch(action, deleted); + }, + DELETE_SINGLE_PERMISSION: (context, deleted) => { + const permissions = [...context.state.itemInstance.permissions]; + permissions.splice( + permissions.findIndex(({ id }) => id === deleted.id), + 1, + ); + return context.dispatch('SET_ITEM_PROPERTY', { + prop: 'permissions', + value: permissions, + }); + }, + DELETE_BULK_PERMISSIONS: (context, deleted) => { + const permissions = context.state.itemInstance.permissions.filter(({ id: permId }) => { + return !deleted.some(({ id: deletedPermId }) => { + return deletedPermId === permId; + }); + }); + return context.dispatch('SET_ITEM_PROPERTY', { + prop: 'permissions', + value: permissions, + }); + }, + DELETE_ALL_PERMISSIONS: (context) => { + const permissions = []; + return context.dispatch('SET_ITEM_PROPERTY', { + prop: 'permissions', + value: permissions, + }); + }, + UPDATE_APPLICATION_ACCESS: async (context, { app, value }) => { + const metadata = deepCopy(context.state.itemInstance.metadata); + metadata.access[app]._enabled = value; + await context.dispatch('SET_ITEM_PROPERTY', { + prop: 'metadata', + value: metadata, + }); + const appSections = Object.keys(metadata.access[app]).filter( + (section) => section.at(0) !== '_', + ); + return Promise.allSettled( + appSections.map((section) => + context.dispatch('UPDATE_APPLICATION_SECTION_ACCESS', { + app, + section, + value, + }), + ), + ); + }, + UPDATE_APPLICATION_SECTION_ACCESS: (context, { app, section, value }) => { + const metadata = deepCopy(context.state.itemInstance.metadata); + metadata.access[app][section]._enabled = value; + return context.dispatch('SET_ITEM_PROPERTY', { + prop: 'metadata', + value: metadata, + }); + }, +}; + +const roles = new ObjectStoreModule({ resettableState, headers }) + .attachAPIModule(RolesAPI) + .generateAPIActions() + .getModule({ actions }); + +export default roles; diff --git a/src/modules/permissions/store/permissions.js b/src/modules/permissions/store/permissions.js new file mode 100644 index 00000000..1970efbd --- /dev/null +++ b/src/modules/permissions/store/permissions.js @@ -0,0 +1,10 @@ +import objects from '../modules/objects/store/objects'; + +const modules = { + objects, +}; + +export default { + namespaced: true, + modules, +}; diff --git a/vite.config.js.timestamp-1716888890686-6c8de6262eaca.mjs b/vite.config.js.timestamp-1716888890686-6c8de6262eaca.mjs deleted file mode 100644 index 8de4a075..00000000 --- a/vite.config.js.timestamp-1716888890686-6c8de6262eaca.mjs +++ /dev/null @@ -1,75 +0,0 @@ -// vite.config.js -import vue - from 'file:///Users/admin/Projects/crm/node_modules/@vitejs/plugin-vue/dist/index.mjs'; -import { - nodePolyfills, -} from 'file:///Users/admin/Projects/crm/node_modules/vite-plugin-node-polyfills/dist/index.js'; -import createSvgSpritePlugin - from 'file:///Users/admin/Projects/crm/node_modules/vite-plugin-svg-sprite/esm/index.js'; -import { - defineConfig, - loadEnv, -} from 'file:///Users/admin/Projects/crm/node_modules/vite/dist/node/index.js'; - -var vite_config_default = ({ mode }) => { - const env = loadEnv(mode, process.cwd(), ''); - return defineConfig({ - base: '/crm', - define: { - 'process.env': JSON.parse(JSON.stringify(env) - .replaceAll('VITE_', 'VUE_APP_')), - }, - server: { - host: true, - port: 8080, - }, - css: { - preprocessorOptions: { - scss: { - additionalData: `@import "./src/app/css/main.scss";`, - }, - }, - }, - resolve: { - alias: { - vue: '@vue/compat', - }, - }, - plugins: [ - vue({ - template: { - compilerOptions: { - compatConfig: { - MODE: 2, - }, - }, - }, - }), - // https://www.npmjs.com/package/vite-plugin-node-polyfills - nodePolyfills({ - // are needed for csv-parse - include: ['buffer', 'stream'], - globals: { - Buffer: true, - // can also be 'build', 'dev', or false - }, - }), - createSvgSpritePlugin({ - include: '**/sprite/*.svg', - }), - ], - test: { - globals: true, - coverage: { - enabled: true, - reporter: 'json', - }, - environment: 'happy-dom', - setupFiles: ['./tests/config/config.js'], - }, - }); -}; -export { - vite_config_default as default, -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYWRtaW4vUHJvamVjdHMvY3JtXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYWRtaW4vUHJvamVjdHMvY3JtL3ZpdGUuY29uZmlnLmpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9hZG1pbi9Qcm9qZWN0cy9jcm0vdml0ZS5jb25maWcuanNcIjtpbXBvcnQgdnVlIGZyb20gJ0B2aXRlanMvcGx1Z2luLXZ1ZSc7XG5pbXBvcnQgeyBkZWZpbmVDb25maWcsIGxvYWRFbnYgfSBmcm9tICd2aXRlJztcbmltcG9ydCB7IG5vZGVQb2x5ZmlsbHMgfSBmcm9tICd2aXRlLXBsdWdpbi1ub2RlLXBvbHlmaWxscyc7XG5pbXBvcnQgY3JlYXRlU3ZnU3ByaXRlUGx1Z2luIGZyb20gJ3ZpdGUtcGx1Z2luLXN2Zy1zcHJpdGUnO1xuXG4vLyBUT0RPXG4vLyBpbXBvcnQubWV0YS5lbnYuVlVFX0FQUF9QQUNLQUdFX1ZFUlNJT04gPSByZXF1aXJlKCcuL3BhY2thZ2UuanNvbicpLnZlcnNpb247XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCAoeyBtb2RlIH0pID0+IHtcbiAgY29uc3QgZW52ID0gbG9hZEVudihtb2RlLCBwcm9jZXNzLmN3ZCgpLCAnJyk7XG5cbiAgcmV0dXJuIGRlZmluZUNvbmZpZyh7XG4gICAgYmFzZTogJy9jcm0nLFxuICAgIGRlZmluZToge1xuICAgICAgJ3Byb2Nlc3MuZW52JzogSlNPTi5wYXJzZShKU09OLnN0cmluZ2lmeShlbnYpXG4gICAgICAucmVwbGFjZUFsbCgnVklURV8nLCAnVlVFX0FQUF8nKSksXG4gICAgfSxcbiAgICBzZXJ2ZXI6IHtcbiAgICAgIGhvc3Q6IHRydWUsXG4gICAgICBwb3J0OiA4MDgwLFxuICAgIH0sXG4gICAgY3NzOiB7XG4gICAgICBwcmVwcm9jZXNzb3JPcHRpb25zOiB7XG4gICAgICAgIHNjc3M6IHtcbiAgICAgICAgICBhZGRpdGlvbmFsRGF0YTogYEBpbXBvcnQgXCIuL3NyYy9hcHAvY3NzL21haW4uc2Nzc1wiO2AsXG4gICAgICAgIH0sXG4gICAgICB9LFxuICAgIH0sXG4gICAgcmVzb2x2ZToge1xuICAgICAgYWxpYXM6IHtcbiAgICAgICAgdnVlOiAnQHZ1ZS9jb21wYXQnLFxuICAgICAgfSxcbiAgICB9LFxuICAgIHBsdWdpbnM6IFtcbiAgICAgIHZ1ZSh7XG4gICAgICAgIHRlbXBsYXRlOiB7XG4gICAgICAgICAgY29tcGlsZXJPcHRpb25zOiB7XG4gICAgICAgICAgICBjb21wYXRDb25maWc6IHtcbiAgICAgICAgICAgICAgTU9ERTogMixcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgfSxcbiAgICAgICAgfSxcbiAgICAgIH0pLFxuICAgICAgLy8gaHR0cHM6Ly93d3cubnBtanMuY29tL3BhY2thZ2Uvdml0ZS1wbHVnaW4tbm9kZS1wb2x5ZmlsbHNcbiAgICAgIG5vZGVQb2x5ZmlsbHMoe1xuICAgICAgICAvLyBhcmUgbmVlZGVkIGZvciBjc3YtcGFyc2VcbiAgICAgICAgaW5jbHVkZTogWydidWZmZXInLCAnc3RyZWFtJ10sXG4gICAgICAgIGdsb2JhbHM6IHtcbiAgICAgICAgICBCdWZmZXI6IHRydWUsIC8vIGNhbiBhbHNvIGJlICdidWlsZCcsICdkZXYnLCBvciBmYWxzZVxuICAgICAgICB9LFxuICAgICAgfSksXG4gICAgICBjcmVhdGVTdmdTcHJpdGVQbHVnaW4oe1xuICAgICAgICBpbmNsdWRlOiAnKiovc3ByaXRlLyouc3ZnJyxcbiAgICAgIH0pLFxuICAgIF0sXG4gICAgdGVzdDoge1xuICAgICAgZ2xvYmFsczogdHJ1ZSxcbiAgICAgIGNvdmVyYWdlOiB7XG4gICAgICAgIGVuYWJsZWQ6IHRydWUsXG4gICAgICAgIHJlcG9ydGVyOiAnanNvbicsXG4gICAgICB9LFxuICAgICAgZW52aXJvbm1lbnQ6ICdoYXBweS1kb20nLFxuICAgICAgc2V0dXBGaWxlczogWycuL3Rlc3RzL2NvbmZpZy9jb25maWcuanMnXSxcbiAgICB9LFxuICB9KTtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBNlAsT0FBTyxTQUFTO0FBQzdRLFNBQVMsY0FBYyxlQUFlO0FBQ3RDLFNBQVMscUJBQXFCO0FBQzlCLE9BQU8sMkJBQTJCO0FBTWxDLElBQU8sc0JBQVEsQ0FBQyxFQUFFLEtBQUssTUFBTTtBQUMzQixRQUFNLE1BQU0sUUFBUSxNQUFNLFFBQVEsSUFBSSxHQUFHLEVBQUU7QUFFM0MsU0FBTyxhQUFhO0FBQUEsSUFDbEIsTUFBTTtBQUFBLElBQ04sUUFBUTtBQUFBLE1BQ04sZUFBZSxLQUFLLE1BQU0sS0FBSyxVQUFVLEdBQUcsRUFDM0MsV0FBVyxTQUFTLFVBQVUsQ0FBQztBQUFBLElBQ2xDO0FBQUEsSUFDQSxRQUFRO0FBQUEsTUFDTixNQUFNO0FBQUEsTUFDTixNQUFNO0FBQUEsSUFDUjtBQUFBLElBQ0EsS0FBSztBQUFBLE1BQ0gscUJBQXFCO0FBQUEsUUFDbkIsTUFBTTtBQUFBLFVBQ0osZ0JBQWdCO0FBQUEsUUFDbEI7QUFBQSxNQUNGO0FBQUEsSUFDRjtBQUFBLElBQ0EsU0FBUztBQUFBLE1BQ1AsT0FBTztBQUFBLFFBQ0wsS0FBSztBQUFBLE1BQ1A7QUFBQSxJQUNGO0FBQUEsSUFDQSxTQUFTO0FBQUEsTUFDUCxJQUFJO0FBQUEsUUFDRixVQUFVO0FBQUEsVUFDUixpQkFBaUI7QUFBQSxZQUNmLGNBQWM7QUFBQSxjQUNaLE1BQU07QUFBQSxZQUNSO0FBQUEsVUFDRjtBQUFBLFFBQ0Y7QUFBQSxNQUNGLENBQUM7QUFBQTtBQUFBLE1BRUQsY0FBYztBQUFBO0FBQUEsUUFFWixTQUFTLENBQUMsVUFBVSxRQUFRO0FBQUEsUUFDNUIsU0FBUztBQUFBLFVBQ1AsUUFBUTtBQUFBO0FBQUEsUUFDVjtBQUFBLE1BQ0YsQ0FBQztBQUFBLE1BQ0Qsc0JBQXNCO0FBQUEsUUFDcEIsU0FBUztBQUFBLE1BQ1gsQ0FBQztBQUFBLElBQ0g7QUFBQSxJQUNBLE1BQU07QUFBQSxNQUNKLFNBQVM7QUFBQSxNQUNULFVBQVU7QUFBQSxRQUNSLFNBQVM7QUFBQSxRQUNULFVBQVU7QUFBQSxNQUNaO0FBQUEsTUFDQSxhQUFhO0FBQUEsTUFDYixZQUFZLENBQUMsMEJBQTBCO0FBQUEsSUFDekM7QUFBQSxFQUNGLENBQUM7QUFDSDsiLAogICJuYW1lcyI6IFtdCn0K