Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25cdb2d265 | ||
|
|
31bcdaa3dd | ||
|
|
6ae606c981 | ||
|
|
aaf47b4c1f | ||
|
|
cc4851b19d | ||
|
|
5520bf68f3 | ||
|
|
46621b0983 | ||
|
|
2c94c89077 | ||
|
|
97de283103 | ||
|
|
7ed7c1b618 | ||
|
|
8f3234cf4b | ||
|
|
b2de6843f6 | ||
|
|
73c4180e64 | ||
|
|
96f4f50d2d | ||
|
|
f7f687ce53 | ||
|
|
5716bb29b8 | ||
|
|
e72a343612 | ||
|
|
ee2844877c | ||
|
|
66c6b8cd4f | ||
|
|
ef606204a2 | ||
|
|
3dee50d2b1 | ||
|
|
1f09af270c |
@@ -7,7 +7,7 @@ defaults: &defaults
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: cimg/ruby:3.2.2-browsers
|
||||
- image: cimg/ruby:3.3.3-browsers
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
args:
|
||||
VARIANT: "ubuntu-22.04"
|
||||
NODE_VERSION: "20.9.0"
|
||||
RUBY_VERSION: "3.2.2"
|
||||
RUBY_VERSION: "3.3.3"
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: "1000"
|
||||
USER_GID: "1000"
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
args:
|
||||
VARIANT: "ubuntu-22.04"
|
||||
NODE_VERSION: "20.9.0"
|
||||
RUBY_VERSION: "3.2.2"
|
||||
RUBY_VERSION: "3.3.3"
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: "1000"
|
||||
USER_GID: "1000"
|
||||
|
||||
@@ -24,10 +24,12 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64,amd64'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
@@ -54,10 +56,11 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_TAG }}
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.2.2
|
||||
3.3.3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.2.2'
|
||||
ruby '3.3.3'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', '2.0.0', require: 'rack/cors'
|
||||
@@ -111,12 +111,12 @@ gem 'elastic-apm', require: false
|
||||
gem 'newrelic_rpm', require: false
|
||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
gem 'scout_apm', require: false
|
||||
gem 'sentry-rails', '>= 5.14.0', require: false
|
||||
gem 'sentry-rails', '>= 5.18.0', require: false
|
||||
gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.15.0', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.18.0', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '>= 7.2.4'
|
||||
gem 'sidekiq', '>= 7.3.0'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron', '>= 1.12.0'
|
||||
|
||||
@@ -228,7 +228,7 @@ group :development, :test do
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec_junit_formatter'
|
||||
gem 'rspec-rails', '>= 6.0.3'
|
||||
gem 'rspec-rails', '>= 6.1.3'
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
|
||||
+41
-36
@@ -150,7 +150,7 @@ GEM
|
||||
statsd-ruby (~> 1.1)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
bigdecimal (3.1.7)
|
||||
bigdecimal (3.1.8)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
@@ -183,13 +183,16 @@ GEM
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.3.4)
|
||||
ddtrace (1.11.1)
|
||||
debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
|
||||
libdatadog (~> 2.0.0.1.0)
|
||||
libddwaf (~> 1.8.2.0.0)
|
||||
datadog-ci (0.8.3)
|
||||
msgpack
|
||||
debase-ruby_core_source (3.2.0)
|
||||
date (3.3.4)
|
||||
ddtrace (1.23.2)
|
||||
datadog-ci (~> 0.8.1)
|
||||
debase-ruby_core_source (= 3.3.1)
|
||||
libdatadog (~> 7.0.0.1.0)
|
||||
libddwaf (~> 1.14.0.0.0)
|
||||
msgpack
|
||||
debase-ruby_core_source (3.3.1)
|
||||
debug (1.8.0)
|
||||
irb (>= 1.5.0)
|
||||
reline (>= 0.3.1)
|
||||
@@ -223,7 +226,7 @@ GEM
|
||||
http (>= 3.0)
|
||||
ruby2_keywords
|
||||
email_reply_trimmer (0.1.13)
|
||||
erubi (1.12.0)
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.7)
|
||||
tzinfo
|
||||
execjs (2.8.1)
|
||||
@@ -416,15 +419,15 @@ GEM
|
||||
addressable (~> 2.8)
|
||||
letter_opener (1.8.1)
|
||||
launchy (>= 2.2, < 3)
|
||||
libdatadog (2.0.0.1.0)
|
||||
libdatadog (2.0.0.1.0-x86_64-linux)
|
||||
libddwaf (1.8.2.0.0)
|
||||
libdatadog (7.0.0.1.0)
|
||||
libdatadog (7.0.0.1.0-x86_64-linux)
|
||||
libddwaf (1.14.0.0.0)
|
||||
ffi (~> 1.0)
|
||||
libddwaf (1.8.2.0.0-arm64-darwin)
|
||||
libddwaf (1.14.0.0.0-arm64-darwin)
|
||||
ffi (~> 1.0)
|
||||
libddwaf (1.8.2.0.0-x86_64-darwin)
|
||||
libddwaf (1.14.0.0.0-x86_64-darwin)
|
||||
ffi (~> 1.0)
|
||||
libddwaf (1.8.2.0.0-x86_64-linux)
|
||||
libddwaf (1.14.0.0.0-x86_64-linux)
|
||||
ffi (~> 1.0)
|
||||
line-bot-api (1.28.0)
|
||||
liquid (5.4.0)
|
||||
@@ -434,6 +437,7 @@ GEM
|
||||
llhttp-ffi (0.4.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
logger (1.6.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
@@ -460,7 +464,7 @@ GEM
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.23.1)
|
||||
minitest (5.24.0)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
msgpack (1.7.0)
|
||||
@@ -489,14 +493,14 @@ GEM
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.5)
|
||||
nokogiri (1.16.6)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-arm64-darwin)
|
||||
nokogiri (1.16.6-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-x86_64-darwin)
|
||||
nokogiri (1.16.6-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-x86_64-linux)
|
||||
nokogiri (1.16.6-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
@@ -603,7 +607,7 @@ GEM
|
||||
ffi (~> 1.0)
|
||||
redis (5.0.6)
|
||||
redis-client (>= 0.9.0)
|
||||
redis-client (0.22.1)
|
||||
redis-client (0.22.2)
|
||||
connection_pool
|
||||
redis-namespace (1.10.0)
|
||||
redis (>= 4)
|
||||
@@ -631,13 +635,13 @@ GEM
|
||||
strscan (>= 3.0.9)
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.0)
|
||||
rspec-expectations (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.2)
|
||||
rspec-rails (6.1.3)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
@@ -703,23 +707,24 @@ GEM
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.17.3)
|
||||
sentry-rails (5.18.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-ruby (5.17.3)
|
||||
sentry-ruby (~> 5.18.0)
|
||||
sentry-ruby (5.18.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.17.3)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-sidekiq (5.18.0)
|
||||
sentry-ruby (~> 5.18.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (7.2.4)
|
||||
sidekiq (7.3.0)
|
||||
concurrent-ruby (< 2)
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.19.0)
|
||||
redis-client (>= 0.22.2)
|
||||
sidekiq-cron (1.12.0)
|
||||
fugit (~> 1.8)
|
||||
globalid (>= 1.0.1)
|
||||
@@ -819,7 +824,7 @@ GEM
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.6.15)
|
||||
zeitwerk (2.6.16)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -922,7 +927,7 @@ DEPENDENCIES
|
||||
responders (>= 3.1.1)
|
||||
rest-client
|
||||
reverse_markdown
|
||||
rspec-rails (>= 6.0.3)
|
||||
rspec-rails (>= 6.1.3)
|
||||
rspec_junit_formatter
|
||||
rubocop
|
||||
rubocop-performance
|
||||
@@ -931,11 +936,11 @@ DEPENDENCIES
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-rails (>= 5.14.0)
|
||||
sentry-rails (>= 5.18.0)
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.15.0)
|
||||
sentry-sidekiq (>= 5.18.0)
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.2.4)
|
||||
sidekiq (>= 7.3.0)
|
||||
sidekiq-cron (>= 1.12.0)
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client (~> 2.2.0)
|
||||
@@ -960,7 +965,7 @@ DEPENDENCIES
|
||||
working_hours
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.2p185
|
||||
ruby 3.3.3p89
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.6
|
||||
2.5.14
|
||||
|
||||
@@ -37,7 +37,7 @@ class DashboardController < ActionController::Base
|
||||
end
|
||||
|
||||
def set_dashboard_scripts
|
||||
@dashboard_scripts = GlobalConfig.get_value('DASHBOARD_SCRIPTS')
|
||||
@dashboard_scripts = sensitive_path? ? nil : GlobalConfig.get_value('DASHBOARD_SCRIPTS')
|
||||
end
|
||||
|
||||
def ensure_installation_onboarding
|
||||
@@ -75,4 +75,14 @@ class DashboardController < ActionController::Base
|
||||
'application'
|
||||
end
|
||||
end
|
||||
|
||||
def sensitive_path?
|
||||
# dont load dashboard scripts on sensitive paths like password reset
|
||||
sensitive_paths = [edit_user_password_path].freeze
|
||||
|
||||
# remove app prefix
|
||||
current_path = request.path.gsub(%r{^/app}, '')
|
||||
|
||||
sensitive_paths.include?(current_path)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -111,15 +111,6 @@
|
||||
@updateFolder="onUpdateSavedFilter"
|
||||
/>
|
||||
</woot-modal>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="chooseSnoozeTime"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -152,10 +143,6 @@ import {
|
||||
isOnUnattendedView,
|
||||
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { getUnixTime } from 'date-fns';
|
||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||
import IntersectionObserver from './IntersectionObserver.vue';
|
||||
|
||||
export default {
|
||||
@@ -170,7 +157,6 @@ export default {
|
||||
ConversationBulkActions,
|
||||
IntersectionObserver,
|
||||
VirtualList,
|
||||
CustomSnoozeModal,
|
||||
},
|
||||
mixins: [
|
||||
timeMixin,
|
||||
@@ -247,7 +233,6 @@ export default {
|
||||
root: this.$refs.conversationList,
|
||||
rootMargin: '100px 0px 100px 0px',
|
||||
},
|
||||
showCustomSnoozeModal: false,
|
||||
|
||||
itemComponent: ConversationItem,
|
||||
// virtualListExtraProps is to pass the props to the conversationItem component.
|
||||
@@ -283,7 +268,6 @@ export default {
|
||||
campaigns: 'campaigns/getAllCampaigns',
|
||||
labels: 'labels/getLabels',
|
||||
selectedConversations: 'bulkActions/getSelectedConversationIds',
|
||||
contextMenuChatId: 'getContextMenuChatId',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.appliedFilters.length !== 0;
|
||||
@@ -517,11 +501,6 @@ export default {
|
||||
this.$emitter.on('fetch_conversation_stats', () => {
|
||||
this.$store.dispatch('conversationStats/get', this.conversationFilters);
|
||||
});
|
||||
|
||||
this.$emitter.on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$emitter.off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
methods: {
|
||||
updateVirtualListProps(key, value) {
|
||||
@@ -999,43 +978,6 @@ export default {
|
||||
onContextMenuToggle(state) {
|
||||
this.isContextMenuOpen = state;
|
||||
},
|
||||
onCmdSnoozeConversation(snoozeType) {
|
||||
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
this.showCustomSnoozeModal = true;
|
||||
} else {
|
||||
this.toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
findSnoozeTime(snoozeType) || null
|
||||
);
|
||||
}
|
||||
},
|
||||
chooseSnoozeTime(customSnoozeTime) {
|
||||
this.showCustomSnoozeModal = false;
|
||||
if (customSnoozeTime) {
|
||||
this.toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
getUnixTime(customSnoozeTime)
|
||||
);
|
||||
}
|
||||
},
|
||||
toggleStatus(status, snoozedUntil) {
|
||||
this.$store
|
||||
.dispatch('toggleStatus', {
|
||||
conversationId: this.currentChat?.id || this.contextMenuChatId,
|
||||
status,
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
});
|
||||
},
|
||||
hideCustomSnoozeModal() {
|
||||
// if we select custom snooze and then the custom snooze modal is open
|
||||
// Then if the custom snooze modal is closed and set the context menu chat id to null
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
this.showCustomSnoozeModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
:teams="teams"
|
||||
:custom-views="customViews"
|
||||
:menu-config="activeSecondaryMenu"
|
||||
:current-role="currentRole"
|
||||
:current-user="currentUser"
|
||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||
@add-label="showAddLabelPopup"
|
||||
@toggle-accounts="toggleAccountModal"
|
||||
@@ -37,7 +37,8 @@ import alertMixin from 'shared/mixins/alertMixin';
|
||||
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
||||
import SecondarySidebar from './sidebarComponents/Secondary.vue';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import router from '../../routes';
|
||||
import router, { routesWithPermissions } from '../../routes';
|
||||
import { hasPermissions } from '../../helper/permissionsHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -98,9 +99,13 @@ export default {
|
||||
return getSidebarItems(this.accountId);
|
||||
},
|
||||
primaryMenuItems() {
|
||||
const userPermissions = this.currentUser.permissions;
|
||||
const menuItems = this.sideMenuConfig.primaryMenu;
|
||||
return menuItems.filter(menuItem => {
|
||||
const isAvailableForTheUser = menuItem.roles.includes(this.currentRole);
|
||||
const isAvailableForTheUser = hasPermissions(
|
||||
routesWithPermissions[menuItem.toStateName],
|
||||
userPermissions
|
||||
);
|
||||
|
||||
if (!isAvailableForTheUser) {
|
||||
return false;
|
||||
|
||||
@@ -9,7 +9,6 @@ const primaryMenuItems = accountId => [
|
||||
featureFlag: FEATURE_FLAGS.INBOX_VIEW,
|
||||
toState: frontendURL(`accounts/${accountId}/inbox-view`),
|
||||
toStateName: 'inbox_view',
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
{
|
||||
icon: 'chat',
|
||||
@@ -17,7 +16,6 @@ const primaryMenuItems = accountId => [
|
||||
label: 'CONVERSATIONS',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
toStateName: 'home',
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
{
|
||||
icon: 'book-contacts',
|
||||
@@ -26,7 +24,6 @@ const primaryMenuItems = accountId => [
|
||||
featureFlag: FEATURE_FLAGS.CRM,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
{
|
||||
icon: 'arrow-trending-lines',
|
||||
@@ -34,8 +31,7 @@ const primaryMenuItems = accountId => [
|
||||
label: 'REPORTS',
|
||||
featureFlag: FEATURE_FLAGS.REPORTS,
|
||||
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||
toStateName: 'settings_account_reports',
|
||||
roles: ['administrator'],
|
||||
toStateName: 'account_overview_reports',
|
||||
},
|
||||
{
|
||||
icon: 'megaphone',
|
||||
@@ -44,7 +40,6 @@ const primaryMenuItems = accountId => [
|
||||
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||
toStateName: 'ongoing_campaigns',
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
icon: 'library',
|
||||
@@ -54,7 +49,6 @@ const primaryMenuItems = accountId => [
|
||||
alwaysVisibleOnChatwootInstances: true,
|
||||
toState: frontendURL(`accounts/${accountId}/portals`),
|
||||
toStateName: 'default_portal_articles',
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
icon: 'settings',
|
||||
@@ -62,7 +56,6 @@ const primaryMenuItems = accountId => [
|
||||
label: 'SETTINGS',
|
||||
toState: frontendURL(`accounts/${accountId}/settings`),
|
||||
toStateName: 'settings_home',
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ const settings = accountId => ({
|
||||
'settings_inbox_list',
|
||||
'settings_inbox_new',
|
||||
'settings_inbox_show',
|
||||
'settings_inbox',
|
||||
'settings_inboxes_add_agents',
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_integrations_dashboard_apps',
|
||||
@@ -46,6 +45,9 @@ const settings = accountId => ({
|
||||
icon: 'briefcase',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||
toStateName: 'general_settings_index',
|
||||
},
|
||||
@@ -53,6 +55,9 @@ const settings = accountId => ({
|
||||
icon: 'people',
|
||||
label: 'AGENTS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
||||
toStateName: 'agent_list',
|
||||
featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT,
|
||||
@@ -61,6 +66,9 @@ const settings = accountId => ({
|
||||
icon: 'people-team',
|
||||
label: 'TEAMS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
||||
toStateName: 'settings_teams_list',
|
||||
featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT,
|
||||
@@ -69,6 +77,9 @@ const settings = accountId => ({
|
||||
icon: 'mail-inbox-all',
|
||||
label: 'INBOXES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
||||
toStateName: 'settings_inbox_list',
|
||||
featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT,
|
||||
@@ -77,6 +88,9 @@ const settings = accountId => ({
|
||||
icon: 'tag',
|
||||
label: 'LABELS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
||||
toStateName: 'labels_list',
|
||||
featureFlag: FEATURE_FLAGS.LABELS,
|
||||
@@ -85,6 +99,9 @@ const settings = accountId => ({
|
||||
icon: 'code',
|
||||
label: 'CUSTOM_ATTRIBUTES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(
|
||||
`accounts/${accountId}/settings/custom-attributes/list`
|
||||
),
|
||||
@@ -95,6 +112,9 @@ const settings = accountId => ({
|
||||
icon: 'automation',
|
||||
label: 'AUTOMATION',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
|
||||
toStateName: 'automation_list',
|
||||
featureFlag: FEATURE_FLAGS.AUTOMATIONS,
|
||||
@@ -103,6 +123,9 @@ const settings = accountId => ({
|
||||
icon: 'bot',
|
||||
label: 'AGENT_BOTS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
globalConfigFlag: 'csmlEditorHost',
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
|
||||
toStateName: 'agent_bots',
|
||||
@@ -112,6 +135,9 @@ const settings = accountId => ({
|
||||
icon: 'flash-settings',
|
||||
label: 'MACROS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/macros`),
|
||||
toStateName: 'macros_wrapper',
|
||||
featureFlag: FEATURE_FLAGS.MACROS,
|
||||
@@ -120,6 +146,9 @@ const settings = accountId => ({
|
||||
icon: 'chat-multiple',
|
||||
label: 'CANNED_RESPONSES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
toState: frontendURL(
|
||||
`accounts/${accountId}/settings/canned-response/list`
|
||||
),
|
||||
@@ -130,6 +159,9 @@ const settings = accountId => ({
|
||||
icon: 'flash-on',
|
||||
label: 'INTEGRATIONS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
@@ -138,6 +170,9 @@ const settings = accountId => ({
|
||||
icon: 'star-emphasis',
|
||||
label: 'APPLICATIONS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
@@ -146,6 +181,9 @@ const settings = accountId => ({
|
||||
icon: 'key',
|
||||
label: 'AUDIT_LOGS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
|
||||
toStateName: 'auditlogs_list',
|
||||
isEnterpriseOnly: true,
|
||||
@@ -156,6 +194,9 @@ const settings = accountId => ({
|
||||
icon: 'document-list-clock',
|
||||
label: 'SLA',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/sla/list`),
|
||||
toStateName: 'sla_list',
|
||||
isEnterpriseOnly: true,
|
||||
@@ -166,6 +207,9 @@ const settings = accountId => ({
|
||||
icon: 'credit-card-person',
|
||||
label: 'BILLING',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/billing`),
|
||||
toStateName: 'billing_settings_index',
|
||||
showOnlyOnCloud: true,
|
||||
|
||||
@@ -29,6 +29,8 @@ import SecondaryNavItem from './SecondaryNavItem.vue';
|
||||
import AccountContext from './AccountContext.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import { hasPermissions } from '../../../helper/permissionsHelper';
|
||||
import { routesWithPermissions } from '../../../routes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -60,9 +62,9 @@ export default {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
currentRole: {
|
||||
type: String,
|
||||
default: '',
|
||||
currentUser: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
isOnChatwootCloud: {
|
||||
type: Boolean,
|
||||
@@ -80,16 +82,16 @@ export default {
|
||||
return this.customViews.filter(view => view.filter_type === 'contact');
|
||||
},
|
||||
accessibleMenuItems() {
|
||||
if (!this.currentRole) {
|
||||
return [];
|
||||
}
|
||||
const menuItemsFilteredByRole = this.menuConfig.menuItems.filter(
|
||||
menuItem =>
|
||||
window.roleWiseRoutes[this.currentRole].indexOf(
|
||||
menuItem.toStateName
|
||||
) > -1
|
||||
const menuItemsFilteredByPermissions = this.menuConfig.menuItems.filter(
|
||||
menuItem => {
|
||||
const { permissions: userPermissions = [] } = this.currentUser;
|
||||
return hasPermissions(
|
||||
routesWithPermissions[menuItem.toStateName],
|
||||
userPermissions
|
||||
);
|
||||
}
|
||||
);
|
||||
return menuItemsFilteredByRole.filter(item => {
|
||||
return menuItemsFilteredByPermissions.filter(item => {
|
||||
if (item.showOnlyOnCloud) {
|
||||
return this.isOnChatwootCloud;
|
||||
}
|
||||
|
||||
@@ -65,27 +65,29 @@
|
||||
:show-child-count="showChildCount(child.count)"
|
||||
:child-item-count="child.count"
|
||||
/>
|
||||
<router-link
|
||||
v-if="showItem(menuItem)"
|
||||
v-slot="{ href, navigate }"
|
||||
:to="menuItem.toState"
|
||||
custom
|
||||
>
|
||||
<li class="pl-1">
|
||||
<a :href="href">
|
||||
<woot-button
|
||||
size="tiny"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="add"
|
||||
:data-testid="menuItem.dataTestid"
|
||||
@click="e => newLinkClick(e, navigate)"
|
||||
>
|
||||
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
|
||||
</woot-button>
|
||||
</a>
|
||||
</li>
|
||||
</router-link>
|
||||
<Policy :permissions="['administrator']">
|
||||
<router-link
|
||||
v-if="menuItem.newLink"
|
||||
v-slot="{ href, navigate }"
|
||||
:to="menuItem.toState"
|
||||
custom
|
||||
>
|
||||
<li class="pl-1">
|
||||
<a :href="href">
|
||||
<woot-button
|
||||
size="tiny"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="add"
|
||||
:data-testid="menuItem.dataTestid"
|
||||
@click="e => newLinkClick(e, navigate)"
|
||||
>
|
||||
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
|
||||
</woot-button>
|
||||
</a>
|
||||
</li>
|
||||
</router-link>
|
||||
</Policy>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
@@ -105,9 +107,10 @@ import {
|
||||
isOnMentionsView,
|
||||
isOnUnattendedView,
|
||||
} from '../../../store/modules/conversations/helpers/actionHelpers';
|
||||
import Policy from '../../policy.vue';
|
||||
|
||||
export default {
|
||||
components: { SecondaryChildNavItem },
|
||||
components: { SecondaryChildNavItem, Policy },
|
||||
mixins: [adminMixin, configMixin],
|
||||
props: {
|
||||
menuItem: {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { computed } from 'vue';
|
||||
import { hasPermissions } from '../helper/permissionsHelper';
|
||||
const props = defineProps({
|
||||
permissions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const user = getters.getCurrentUser.value;
|
||||
const hasPermission = computed(() =>
|
||||
hasPermissions(props.permissions, user.permissions)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasPermission">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -257,7 +257,16 @@ export default {
|
||||
html_content: { full: fullHTMLContent } = {},
|
||||
text_content: { full: fullTextContent } = {},
|
||||
} = this.contentAttributes.email || {};
|
||||
return fullHTMLContent || fullTextContent || '';
|
||||
|
||||
if (fullHTMLContent) {
|
||||
return fullHTMLContent;
|
||||
}
|
||||
|
||||
if (fullTextContent) {
|
||||
return fullTextContent.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
displayQuotedButton() {
|
||||
if (this.emailMessageContent.includes('<blockquote')) {
|
||||
|
||||
@@ -121,7 +121,7 @@ export const generateConditionOptions = (options, key = 'id') => {
|
||||
};
|
||||
|
||||
// Add the "None" option to the agent list
|
||||
export const agentList = agents => [
|
||||
export const addNoneToList = agents => [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
@@ -137,8 +137,8 @@ export const getActionOptions = ({
|
||||
type,
|
||||
}) => {
|
||||
const actionsMap = {
|
||||
assign_agent: agentList(agents),
|
||||
assign_team: teams,
|
||||
assign_agent: addNoneToList(agents),
|
||||
assign_team: addNoneToList(teams),
|
||||
send_email_to_team: teams,
|
||||
add_label: generateConditionOptions(labels, 'title'),
|
||||
remove_label: generateConditionOptions(labels, 'title'),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
export const hasPermissions = (
|
||||
requiredPermissions = [],
|
||||
availablePermissions = []
|
||||
) => {
|
||||
return requiredPermissions.some(permission =>
|
||||
availablePermissions.includes(permission)
|
||||
);
|
||||
};
|
||||
|
||||
const isPermissionsPresentInRoute = route =>
|
||||
route.meta && route.meta.permissions;
|
||||
|
||||
export const buildPermissionsFromRouter = (routes = []) =>
|
||||
routes.reduce((acc, route) => {
|
||||
if (route.name) {
|
||||
if (!isPermissionsPresentInRoute(route)) {
|
||||
// eslint-disable-next-line
|
||||
console.error(route);
|
||||
throw new Error(
|
||||
"The route doesn't have the required permissions defined"
|
||||
);
|
||||
}
|
||||
acc[route.name] = route.meta.permissions;
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
acc = {
|
||||
...acc,
|
||||
...buildPermissionsFromRouter(route.children),
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -1,19 +1,16 @@
|
||||
import { hasPermissions } from './permissionsHelper';
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
export const getCurrentAccount = ({ accounts } = {}, accountId) => {
|
||||
return accounts.find(account => account.id === accountId);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
export const getUserRole = ({ accounts } = {}, accountId) => {
|
||||
const currentAccount = getCurrentAccount({ accounts }, accountId) || {};
|
||||
return currentAccount.role || null;
|
||||
export const routeIsAccessibleFor = (route, userPermissions = []) => {
|
||||
const { meta: { permissions: routePermissions = [] } = {} } = route;
|
||||
return hasPermissions(routePermissions, userPermissions);
|
||||
};
|
||||
|
||||
export const routeIsAccessibleFor = (route, role, roleWiseRoutes) => {
|
||||
return roleWiseRoutes[role].includes(route);
|
||||
};
|
||||
|
||||
const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
|
||||
const validateActiveAccountRoutes = (to, user) => {
|
||||
// If the current account is active, then check for the route permissions
|
||||
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
|
||||
|
||||
@@ -22,15 +19,13 @@ const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
|
||||
return accountDashboardURL;
|
||||
}
|
||||
|
||||
const userRole = getUserRole(user, Number(to.params.accountId));
|
||||
const isAccessible = routeIsAccessibleFor(to.name, userRole, roleWiseRoutes);
|
||||
const isAccessible = routeIsAccessibleFor(to, user.permissions);
|
||||
// If the route is not accessible for the user, return to dashboard screen
|
||||
return isAccessible ? null : accountDashboardURL;
|
||||
};
|
||||
|
||||
export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
|
||||
export const validateLoggedInRoutes = (to, user) => {
|
||||
const currentAccount = getCurrentAccount(user, Number(to.params.accountId));
|
||||
|
||||
// If current account is missing, either user does not have
|
||||
// access to the account or the account is deleted, return to login screen
|
||||
if (!currentAccount) {
|
||||
@@ -40,7 +35,7 @@ export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
|
||||
const isCurrentAccountActive = currentAccount.status === 'active';
|
||||
|
||||
if (isCurrentAccountActive) {
|
||||
return validateActiveAccountRoutes(to, user, roleWiseRoutes);
|
||||
return validateActiveAccountRoutes(to, user);
|
||||
}
|
||||
|
||||
// If the current account is not active, then redirect the user to the suspended screen
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
buildPermissionsFromRouter,
|
||||
hasPermissions,
|
||||
} from '../permissionsHelper';
|
||||
|
||||
describe('hasPermissions', () => {
|
||||
it('returns true if permission is present', () => {
|
||||
expect(
|
||||
hasPermissions(['contact_manage'], ['team_manage', 'contact_manage'])
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true if permission is not present', () => {
|
||||
expect(
|
||||
hasPermissions(['contact_manage'], ['team_manage', 'user_manage'])
|
||||
).toBe(false);
|
||||
expect(hasPermissions()).toBe(false);
|
||||
expect(hasPermissions([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildPermissionsFromRouter', () => {
|
||||
it('returns a valid object when routes have permissions defined', () => {
|
||||
expect(
|
||||
buildPermissionsFromRouter([
|
||||
{
|
||||
path: 'agent',
|
||||
name: 'agent_list',
|
||||
meta: { permissions: ['agent_admin'] },
|
||||
},
|
||||
{
|
||||
path: 'inbox',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'inbox_list',
|
||||
meta: { permissions: ['inbox_admin'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'conversations',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
children: [
|
||||
{
|
||||
path: 'attachments',
|
||||
name: 'attachments_list',
|
||||
meta: { permissions: ['conversation_admin'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
agent_list: ['agent_admin'],
|
||||
inbox_list: ['inbox_admin'],
|
||||
attachments_list: ['conversation_admin'],
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if a named routed does not have permissions defined', () => {
|
||||
expect(() => {
|
||||
buildPermissionsFromRouter([
|
||||
{
|
||||
path: 'agent',
|
||||
name: 'agent_list',
|
||||
},
|
||||
]);
|
||||
}).toThrow("The route doesn't have the required permissions defined");
|
||||
|
||||
expect(() => {
|
||||
buildPermissionsFromRouter([
|
||||
{
|
||||
path: 'agent',
|
||||
name: 'agent_list',
|
||||
meta: {},
|
||||
},
|
||||
]);
|
||||
}).toThrow("The route doesn't have the required permissions defined");
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
getConversationDashboardRoute,
|
||||
getCurrentAccount,
|
||||
getUserRole,
|
||||
isAConversationRoute,
|
||||
routeIsAccessibleFor,
|
||||
validateLoggedInRoutes,
|
||||
@@ -15,24 +14,11 @@ describe('#getCurrentAccount', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserRole', () => {
|
||||
it('should return the current role', () => {
|
||||
expect(
|
||||
getUserRole({ accounts: [{ id: 1, role: 'administrator' }] }, 1)
|
||||
).toEqual('administrator');
|
||||
expect(getUserRole({ accounts: [] }, 1)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#routeIsAccessibleFor', () => {
|
||||
it('should return the correct access', () => {
|
||||
const roleWiseRoutes = { agent: ['conversations'], admin: ['billing'] };
|
||||
expect(routeIsAccessibleFor('billing', 'agent', roleWiseRoutes)).toEqual(
|
||||
false
|
||||
);
|
||||
expect(routeIsAccessibleFor('billing', 'admin', roleWiseRoutes)).toEqual(
|
||||
true
|
||||
);
|
||||
let route = { meta: { permissions: ['administrator'] } };
|
||||
expect(routeIsAccessibleFor(route, ['agent'])).toEqual(false);
|
||||
expect(routeIsAccessibleFor(route, ['administrator'])).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,11 +26,7 @@ describe('#validateLoggedInRoutes', () => {
|
||||
describe('when account access is missing', () => {
|
||||
it('should return the login route', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ params: { accountId: 1 } },
|
||||
{ accounts: [] },
|
||||
{}
|
||||
)
|
||||
validateLoggedInRoutes({ params: { accountId: 1 } }, { accounts: [] })
|
||||
).toEqual(`app/login`);
|
||||
});
|
||||
});
|
||||
@@ -53,9 +35,12 @@ describe('#validateLoggedInRoutes', () => {
|
||||
it('return suspended route', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ name: 'conversations', params: { accountId: 1 } },
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] },
|
||||
{ agent: ['conversations'] }
|
||||
{
|
||||
name: 'conversations',
|
||||
params: { accountId: 1 },
|
||||
meta: { permissions: ['agent'] },
|
||||
},
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] }
|
||||
)
|
||||
).toEqual(`accounts/1/suspended`);
|
||||
});
|
||||
@@ -65,9 +50,22 @@ describe('#validateLoggedInRoutes', () => {
|
||||
it('returns null (no action required)', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ name: 'conversations', params: { accountId: 1 } },
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
|
||||
{ agent: ['conversations'] }
|
||||
{
|
||||
name: 'conversations',
|
||||
params: { accountId: 1 },
|
||||
meta: { permissions: ['agent'] },
|
||||
},
|
||||
{
|
||||
permissions: ['agent'],
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
role: 'agent',
|
||||
permissions: ['agent'],
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
).toEqual(null);
|
||||
});
|
||||
@@ -76,9 +74,12 @@ describe('#validateLoggedInRoutes', () => {
|
||||
it('returns dashboard url', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ name: 'conversations', params: { accountId: 1 } },
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
|
||||
{ admin: ['conversations'], agent: [] }
|
||||
{
|
||||
name: 'billing',
|
||||
params: { accountId: 1 },
|
||||
meta: { permissions: ['administrator'] },
|
||||
},
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
|
||||
)
|
||||
).toEqual(`accounts/1/dashboard`);
|
||||
});
|
||||
@@ -88,8 +89,7 @@ describe('#validateLoggedInRoutes', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ name: 'account_suspended', params: { accountId: 1 } },
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
|
||||
{ agent: ['account_suspended'] }
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
|
||||
)
|
||||
).toEqual(`accounts/1/dashboard`);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,9 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/search'),
|
||||
name: 'search',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: SearchView,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,13 +7,17 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts'),
|
||||
name: 'contacts_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactsView,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
|
||||
name: 'contacts_segments_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactsView,
|
||||
props: route => {
|
||||
return { segmentsId: route.params.id };
|
||||
@@ -22,7 +26,9 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/labels/:label/contacts'),
|
||||
name: 'contacts_labels_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactsView,
|
||||
props: route => {
|
||||
return { label: route.params.label };
|
||||
@@ -31,7 +37,9 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts/:contactId'),
|
||||
name: 'contact_profile_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactManageView,
|
||||
props: route => {
|
||||
return { contactId: route.params.contactId };
|
||||
|
||||
@@ -22,25 +22,40 @@
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="chooseSnoozeTime"
|
||||
/>
|
||||
</woot-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getUnixTime } from 'date-fns';
|
||||
import ChatList from '../../../components/ChatList.vue';
|
||||
import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue';
|
||||
import PopOverSearch from './search/PopOverSearch.vue';
|
||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatList,
|
||||
ConversationBox,
|
||||
PopOverSearch,
|
||||
CustomSnoozeModal,
|
||||
},
|
||||
mixins: [uiSettingsMixin],
|
||||
mixins: [uiSettingsMixin, alertMixin],
|
||||
props: {
|
||||
inboxId: {
|
||||
type: [String, Number],
|
||||
@@ -70,12 +85,14 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
showSearchModal: false,
|
||||
showCustomSnoozeModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
chatList: 'getAllConversations',
|
||||
currentChat: 'getSelectedChat',
|
||||
contextMenuChatId: 'getContextMenuChatId',
|
||||
}),
|
||||
showConversationList() {
|
||||
return this.isOnExpandedLayout ? !this.conversationId : true;
|
||||
@@ -112,6 +129,10 @@ export default {
|
||||
this.$watch('chatList.length', () => {
|
||||
this.setActiveChat();
|
||||
});
|
||||
this.$emitter.on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$emitter.off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -186,6 +207,43 @@ export default {
|
||||
closeSearch() {
|
||||
this.showSearchModal = false;
|
||||
},
|
||||
onCmdSnoozeConversation(snoozeType) {
|
||||
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
this.showCustomSnoozeModal = true;
|
||||
} else {
|
||||
this.toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
findSnoozeTime(snoozeType) || null
|
||||
);
|
||||
}
|
||||
},
|
||||
chooseSnoozeTime(customSnoozeTime) {
|
||||
this.showCustomSnoozeModal = false;
|
||||
if (customSnoozeTime) {
|
||||
this.toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
getUnixTime(customSnoozeTime)
|
||||
);
|
||||
}
|
||||
},
|
||||
toggleStatus(status, snoozedUntil) {
|
||||
this.$store
|
||||
.dispatch('toggleStatus', {
|
||||
conversationId: this.currentChat?.id || this.contextMenuChatId,
|
||||
status,
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
});
|
||||
},
|
||||
hideCustomSnoozeModal() {
|
||||
// if we select custom snooze and the custom snooze modal is open
|
||||
// Then if the custom snooze modal is closed then set the context menu chat id to null
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
this.showCustomSnoozeModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/dashboard'),
|
||||
name: 'home',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => {
|
||||
return { inboxId: 0 };
|
||||
@@ -16,7 +18,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/conversations/:conversation_id'),
|
||||
name: 'inbox_conversation',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
return { inboxId: 0, conversationId: route.params.conversation_id };
|
||||
@@ -25,7 +29,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
|
||||
name: 'inbox_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
return { inboxId: route.params.inbox_id };
|
||||
@@ -36,7 +42,9 @@ export default {
|
||||
'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id'
|
||||
),
|
||||
name: 'conversation_through_inbox',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
return {
|
||||
@@ -48,7 +56,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/label/:label'),
|
||||
name: 'label_conversations',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ label: route.params.label }),
|
||||
},
|
||||
@@ -57,7 +67,9 @@ export default {
|
||||
'accounts/:accountId/label/:label/conversations/:conversation_id'
|
||||
),
|
||||
name: 'conversations_through_label',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversation_id,
|
||||
@@ -67,7 +79,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/team/:teamId'),
|
||||
name: 'team_conversations',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ teamId: route.params.teamId }),
|
||||
},
|
||||
@@ -76,7 +90,9 @@ export default {
|
||||
'accounts/:accountId/team/:teamId/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversations_through_team',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
@@ -86,7 +102,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/custom_view/:id'),
|
||||
name: 'folder_conversations',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ foldersId: route.params.id }),
|
||||
},
|
||||
@@ -95,7 +113,9 @@ export default {
|
||||
'accounts/:accountId/custom_view/:id/conversations/:conversation_id'
|
||||
),
|
||||
name: 'conversations_through_folders',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversation_id,
|
||||
@@ -105,7 +125,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/mentions/conversations'),
|
||||
name: 'conversation_mentions',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'mention' }),
|
||||
},
|
||||
@@ -114,7 +136,9 @@ export default {
|
||||
'accounts/:accountId/mentions/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversation_through_mentions',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
@@ -124,7 +148,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/unattended/conversations'),
|
||||
name: 'conversation_unattended',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'unattended' }),
|
||||
},
|
||||
@@ -133,7 +159,9 @@ export default {
|
||||
'accounts/:accountId/unattended/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversation_through_unattended',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
@@ -143,7 +171,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/participating/conversations'),
|
||||
name: 'conversation_participating',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'participating' }),
|
||||
},
|
||||
@@ -152,7 +182,9 @@ export default {
|
||||
'accounts/:accountId/participating/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversation_through_participating',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
|
||||
@@ -28,7 +28,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/suspended'),
|
||||
name: 'account_suspended',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: Suspended,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -30,13 +30,17 @@ const portalRoutes = [
|
||||
{
|
||||
path: getPortalRoute(''),
|
||||
name: 'default_portal_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: DefaultPortalArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute('all'),
|
||||
name: 'list_all_portals',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllPortals,
|
||||
},
|
||||
{
|
||||
@@ -47,55 +51,73 @@ const portalRoutes = [
|
||||
path: '',
|
||||
name: 'new_portal_information',
|
||||
component: PortalDetails,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':portalSlug/customization',
|
||||
name: 'portal_customization',
|
||||
component: PortalCustomization,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':portalSlug/finish',
|
||||
name: 'portal_finish',
|
||||
component: PortalSettingsFinish,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug'),
|
||||
name: 'portalSlug',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ShowPortal,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/edit'),
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: EditPortal,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'edit_portal_information',
|
||||
component: EditPortalBasic,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'customizations',
|
||||
name: 'edit_portal_customization',
|
||||
component: EditPortalCustomization,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'locales',
|
||||
name: 'edit_portal_locales',
|
||||
component: EditPortalLocales,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
name: 'list_all_locale_categories',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllCategories,
|
||||
},
|
||||
],
|
||||
@@ -106,39 +128,51 @@ const articleRoutes = [
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles'),
|
||||
name: 'list_all_locale_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/new'),
|
||||
name: 'new_article',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: NewArticle,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/mine'),
|
||||
name: 'list_mine_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/archived'),
|
||||
name: 'list_archived_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/draft'),
|
||||
name: 'list_draft_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'),
|
||||
name: 'edit_article',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: EditArticle,
|
||||
},
|
||||
];
|
||||
@@ -147,19 +181,25 @@ const categoryRoutes = [
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories'),
|
||||
name: 'all_locale_categories',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllCategories,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/new'),
|
||||
name: 'new_category_in_locale',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: NewCategory,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
|
||||
name: 'show_category',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
{
|
||||
@@ -167,13 +207,17 @@ const categoryRoutes = [
|
||||
':portalSlug/:locale/categories/:categorySlug/articles'
|
||||
),
|
||||
name: 'show_category_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListCategoryArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
|
||||
name: 'edit_category',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: EditCategory,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -12,13 +12,17 @@ export const routes = [
|
||||
path: '',
|
||||
name: 'inbox_view',
|
||||
component: InboxEmptyStateView,
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':notification_id',
|
||||
name: 'inbox_view_conversation',
|
||||
component: InboxDetailView,
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -18,7 +18,9 @@ export const routes = [
|
||||
path: '',
|
||||
name: 'notifications_index',
|
||||
component: NotificationsView,
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,7 +6,9 @@ export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/general'),
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'GENERAL_SETTINGS.TITLE',
|
||||
@@ -18,7 +20,9 @@ export default {
|
||||
path: '',
|
||||
name: 'general_settings_index',
|
||||
component: Index,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,7 +8,9 @@ export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/agent-bots'),
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'AGENT_BOTS.HEADER',
|
||||
@@ -20,19 +22,25 @@ export default {
|
||||
path: '',
|
||||
name: 'agent_bots',
|
||||
component: Bot,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'csml/new',
|
||||
name: 'agent_bots_csml_new',
|
||||
component: CsmlNewBot,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'csml/:botId',
|
||||
name: 'agent_bots_csml_edit',
|
||||
component: CsmlEditBot,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -15,14 +15,15 @@ export default {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'agents_wrapper',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'agent_list',
|
||||
component: AgentHome,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -15,14 +15,15 @@ export default {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'attributes_wrapper',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'attributes_list',
|
||||
component: AttributesHome,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -16,13 +16,14 @@ export default {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'auditlogs_wrapper',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'auditlogs_list',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: AuditLogsHome,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -15,14 +15,15 @@ export default {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'automation_wrapper',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'automation_list',
|
||||
component: Automation,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,7 +6,9 @@ export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/billing'),
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'BILLING_SETTINGS.TITLE',
|
||||
@@ -18,7 +20,9 @@ export default {
|
||||
path: '',
|
||||
name: 'billing_settings_index',
|
||||
component: Index,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -19,7 +19,9 @@ export default {
|
||||
{
|
||||
path: 'ongoing',
|
||||
name: 'ongoing_campaigns',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: Index,
|
||||
},
|
||||
],
|
||||
@@ -35,7 +37,9 @@ export default {
|
||||
{
|
||||
path: 'one_off',
|
||||
name: 'one_off',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: Index,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -16,13 +16,14 @@ export default {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'canned_wrapper',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'canned_list',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: CannedHome,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -28,14 +28,15 @@ export default {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'settings_inbox',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'settings_inbox_list',
|
||||
component: InboxHome,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
@@ -45,19 +46,25 @@ export default {
|
||||
path: '',
|
||||
name: 'settings_inbox_new',
|
||||
component: ChannelList,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':inbox_id/finish',
|
||||
name: 'settings_inbox_finish',
|
||||
component: FinishSetup,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':sub_page',
|
||||
name: 'settings_inboxes_page_channel',
|
||||
component: channelFactory.create(),
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
props: route => {
|
||||
return { channel_name: route.params.sub_page };
|
||||
},
|
||||
@@ -65,7 +72,9 @@ export default {
|
||||
{
|
||||
path: ':inbox_id/agents',
|
||||
name: 'settings_inboxes_add_agents',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: AddAgents,
|
||||
},
|
||||
],
|
||||
@@ -74,7 +83,9 @@ export default {
|
||||
path: ':inboxId',
|
||||
name: 'settings_inbox_show',
|
||||
component: Settings,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+6
-2
@@ -26,13 +26,17 @@ export default {
|
||||
path: '',
|
||||
name: 'settings_applications',
|
||||
component: Index,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':integration_id',
|
||||
name: 'settings_applications_integration',
|
||||
component: IntegrationHooks,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
props: route => ({
|
||||
integrationId: route.params.integration_id,
|
||||
}),
|
||||
|
||||
+15
-5
@@ -30,32 +30,42 @@ export default {
|
||||
path: '',
|
||||
name: 'settings_integrations',
|
||||
component: Index,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'webhook',
|
||||
component: Webhook,
|
||||
name: 'settings_integrations_webhook',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'dashboard-apps',
|
||||
component: DashboardApps,
|
||||
name: 'settings_integrations_dashboard_apps',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'slack',
|
||||
name: 'settings_integrations_slack',
|
||||
component: Slack,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
props: route => ({ code: route.query.code }),
|
||||
},
|
||||
{
|
||||
path: ':integration_id',
|
||||
name: 'settings_integrations_integration',
|
||||
component: ShowIntegration,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
props: route => {
|
||||
return {
|
||||
integrationId: route.params.integration_id,
|
||||
|
||||
@@ -17,13 +17,17 @@ export default {
|
||||
{
|
||||
path: '',
|
||||
name: 'labels_wrapper',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'labels_list',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: Index,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -23,19 +23,25 @@ export default {
|
||||
path: '',
|
||||
name: 'macros_wrapper',
|
||||
component: Macros,
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
name: 'macros_new',
|
||||
component: MacroEditor,
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':macroId/edit',
|
||||
name: 'macros_edit',
|
||||
component: MacroEditor,
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,14 +8,18 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/profile'),
|
||||
name: 'profile_settings',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: SettingsContent,
|
||||
children: [
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'profile_settings_index',
|
||||
component: Index,
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -29,7 +29,9 @@ export default {
|
||||
{
|
||||
path: 'overview',
|
||||
name: 'account_overview_reports',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: LiveReports,
|
||||
},
|
||||
],
|
||||
@@ -46,7 +48,9 @@ export default {
|
||||
{
|
||||
path: 'conversation',
|
||||
name: 'conversation_reports',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: Index,
|
||||
},
|
||||
],
|
||||
@@ -63,7 +67,9 @@ export default {
|
||||
{
|
||||
path: 'csat',
|
||||
name: 'csat_reports',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: CsatResponses,
|
||||
},
|
||||
],
|
||||
@@ -80,7 +86,9 @@ export default {
|
||||
{
|
||||
path: 'bot',
|
||||
name: 'bot_reports',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: BotReports,
|
||||
},
|
||||
],
|
||||
@@ -97,7 +105,9 @@ export default {
|
||||
{
|
||||
path: 'agent',
|
||||
name: 'agent_reports',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: AgentReports,
|
||||
},
|
||||
],
|
||||
@@ -114,7 +124,9 @@ export default {
|
||||
{
|
||||
path: 'label',
|
||||
name: 'label_reports',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: LabelReports,
|
||||
},
|
||||
],
|
||||
@@ -131,7 +143,9 @@ export default {
|
||||
{
|
||||
path: 'inboxes',
|
||||
name: 'inbox_reports',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: InboxReports,
|
||||
},
|
||||
],
|
||||
@@ -147,7 +161,9 @@ export default {
|
||||
{
|
||||
path: 'teams',
|
||||
name: 'team_reports',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: TeamReports,
|
||||
},
|
||||
],
|
||||
@@ -164,7 +180,9 @@ export default {
|
||||
{
|
||||
path: 'sla',
|
||||
name: 'sla_reports',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: SLAReports,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -24,7 +24,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings'),
|
||||
name: 'settings_home',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
redirect: () => {
|
||||
if (store.getters.getCurrentRole === 'administrator') {
|
||||
return frontendURL('accounts/:accountId/settings/general');
|
||||
|
||||
@@ -13,13 +13,17 @@ export default {
|
||||
{
|
||||
path: '',
|
||||
name: 'sla_wrapper',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'sla_list',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: Index,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -29,14 +29,15 @@ export default {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'settings_teams',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'settings_teams_list',
|
||||
component: TeamsHome,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
@@ -46,18 +47,24 @@ export default {
|
||||
path: '',
|
||||
name: 'settings_teams_new',
|
||||
component: CreateTeam,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':teamId/finish',
|
||||
name: 'settings_teams_finish',
|
||||
component: FinishSetup,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':teamId/agents',
|
||||
name: 'settings_teams_add_agents',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: AddAgents,
|
||||
},
|
||||
],
|
||||
@@ -70,18 +77,24 @@ export default {
|
||||
path: '',
|
||||
name: 'settings_teams_edit',
|
||||
component: EditTeam,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'agents',
|
||||
name: 'settings_teams_edit_members',
|
||||
component: EditAgents,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'finish',
|
||||
name: 'settings_teams_edit_finish',
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: FinishSetup,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -5,33 +5,12 @@ import dashboard from './dashboard/dashboard.routes';
|
||||
import store from '../store';
|
||||
import { validateLoggedInRoutes } from '../helper/routeHelpers';
|
||||
import AnalyticsHelper from '../helper/AnalyticsHelper';
|
||||
import { buildPermissionsFromRouter } from '../helper/permissionsHelper';
|
||||
|
||||
const routes = [...dashboard.routes];
|
||||
|
||||
window.roleWiseRoutes = {
|
||||
agent: [],
|
||||
administrator: [],
|
||||
};
|
||||
|
||||
// generateRoleWiseRoute - updates window object with agent/admin route
|
||||
const generateRoleWiseRoute = route => {
|
||||
route.forEach(element => {
|
||||
if (element.children) {
|
||||
generateRoleWiseRoute(element.children);
|
||||
}
|
||||
if (element.roles) {
|
||||
element.roles.forEach(roleEl => {
|
||||
window.roleWiseRoutes[roleEl].push(element.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
// Create a object of routes
|
||||
// accessible by each role.
|
||||
// returns an object with roles as keys and routeArr as values
|
||||
generateRoleWiseRoute(routes);
|
||||
|
||||
export const router = new VueRouter({ mode: 'history', routes });
|
||||
export const routesWithPermissions = buildPermissionsFromRouter(routes);
|
||||
|
||||
export const validateAuthenticateRoutePermission = (to, next, { getters }) => {
|
||||
const { isLoggedIn, getCurrentUser: user } = getters;
|
||||
@@ -45,11 +24,7 @@ export const validateAuthenticateRoutePermission = (to, next, { getters }) => {
|
||||
return next(frontendURL(`accounts/${user.account_id}/dashboard`));
|
||||
}
|
||||
|
||||
const nextRoute = validateLoggedInRoutes(
|
||||
to,
|
||||
getters.getCurrentUser,
|
||||
window.roleWiseRoutes
|
||||
);
|
||||
const nextRoute = validateLoggedInRoutes(to, getters.getCurrentUser);
|
||||
return nextRoute ? next(frontendURL(nextRoute)) : next();
|
||||
};
|
||||
|
||||
|
||||
@@ -6,12 +6,16 @@ import { validateRouteAccess } from '../helpers/RouteHelper';
|
||||
|
||||
export const router = new VueRouter({ mode: 'history', routes });
|
||||
|
||||
const sensitiveRouteNames = ['auth_password_edit'];
|
||||
|
||||
export const initalizeRouter = () => {
|
||||
router.beforeEach((to, _, next) => {
|
||||
AnalyticsHelper.page(to.name || '', {
|
||||
path: to.path,
|
||||
name: to.name,
|
||||
});
|
||||
if (!sensitiveRouteNames.includes(to.name)) {
|
||||
AnalyticsHelper.page(to.name || '', {
|
||||
path: to.path,
|
||||
name: to.name,
|
||||
});
|
||||
}
|
||||
|
||||
return validateRouteAccess(to, next, window.chatwootConfig);
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
/>
|
||||
<div
|
||||
v-if="hasAttachments"
|
||||
class="chat-bubble has-attachment agent"
|
||||
class="chat-bubble has-attachment space-y-2 agent"
|
||||
:class="(wrapClass, $dm('bg-white', 'dark:bg-slate-700'))"
|
||||
>
|
||||
<div
|
||||
@@ -44,6 +44,14 @@
|
||||
:readable-time="readableTime"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
|
||||
<video-bubble
|
||||
v-if="attachment.file_type === 'video' && !hasVideoError"
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
@error="onVideoLoadError"
|
||||
/>
|
||||
|
||||
<audio v-else-if="attachment.file_type === 'audio'" controls>
|
||||
<source :src="attachment.data_url" />
|
||||
</audio>
|
||||
@@ -84,6 +92,7 @@ import AgentMessageBubble from 'widget/components/AgentMessageBubble.vue';
|
||||
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import ImageBubble from 'widget/components/ImageBubble.vue';
|
||||
import VideoBubble from 'widget/components/VideoBubble.vue';
|
||||
import FileBubble from 'widget/components/FileBubble.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
@@ -100,6 +109,7 @@ export default {
|
||||
components: {
|
||||
AgentMessageBubble,
|
||||
ImageBubble,
|
||||
VideoBubble,
|
||||
Thumbnail,
|
||||
UserMessage,
|
||||
FileBubble,
|
||||
@@ -120,6 +130,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
hasVideoError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -215,15 +226,20 @@ export default {
|
||||
watch: {
|
||||
message() {
|
||||
this.hasImageError = false;
|
||||
this.hasVideoError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
this.hasVideoError = false;
|
||||
},
|
||||
methods: {
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
onVideoLoadError() {
|
||||
this.hasVideoError = true;
|
||||
},
|
||||
toggleReply() {
|
||||
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message);
|
||||
},
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
:readable-time="readableTime"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
|
||||
<video-bubble
|
||||
v-if="attachment.file_type === 'video' && !hasVideoError"
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
@error="onVideoLoadError"
|
||||
/>
|
||||
|
||||
<file-bubble
|
||||
v-else
|
||||
:url="attachment.data_url"
|
||||
@@ -72,6 +80,7 @@
|
||||
import UserMessageBubble from 'widget/components/UserMessageBubble.vue';
|
||||
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
|
||||
import ImageBubble from 'widget/components/ImageBubble.vue';
|
||||
import VideoBubble from 'widget/components/VideoBubble.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import FileBubble from 'widget/components/FileBubble.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
@@ -88,6 +97,7 @@ export default {
|
||||
UserMessageBubble,
|
||||
MessageReplyButton,
|
||||
ImageBubble,
|
||||
VideoBubble,
|
||||
FileBubble,
|
||||
FluentIcon,
|
||||
ReplyToChip,
|
||||
@@ -107,6 +117,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
hasVideoError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -143,10 +154,12 @@ export default {
|
||||
watch: {
|
||||
message() {
|
||||
this.hasImageError = false;
|
||||
this.hasVideoError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
this.hasVideoError = false;
|
||||
},
|
||||
methods: {
|
||||
async retrySendMessage() {
|
||||
@@ -158,6 +171,9 @@ export default {
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
onVideoLoadError() {
|
||||
this.hasVideoError = true;
|
||||
},
|
||||
toggleReply() {
|
||||
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
url: { type: String, default: '' },
|
||||
readableTime: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const emits = defineEmits(['error']);
|
||||
|
||||
const onVideoError = () => {
|
||||
emits('error');
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="block relative max-w-full">
|
||||
<video
|
||||
class="w-full max-w-[250px] h-auto"
|
||||
:src="url"
|
||||
controls
|
||||
@error="onVideoError"
|
||||
/>
|
||||
<span
|
||||
class="text-xs absolute text-white dark:text-white right-3 bottom-1 whitespace-nowrap"
|
||||
>
|
||||
{{ readableTime }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -49,6 +49,10 @@ class AccountUser < ApplicationRecord
|
||||
::Agents::DestroyJob.perform_later(account, user)
|
||||
end
|
||||
|
||||
def permissions
|
||||
administrator? ? ['administrator'] : ['agent']
|
||||
end
|
||||
|
||||
def push_event_data
|
||||
{
|
||||
id: id,
|
||||
|
||||
@@ -48,7 +48,6 @@ class JsonSchemaValidator < ActiveModel::Validator
|
||||
|
||||
# Add validation errors to the record with a formatted statement
|
||||
validation_errors.each do |error|
|
||||
# byebug
|
||||
format_and_append_error(error, record)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,7 +50,11 @@ class ActionService
|
||||
end
|
||||
|
||||
def assign_team(team_ids = [])
|
||||
return unassign_team if team_ids[0]&.zero?
|
||||
# FIXME: The explicit checks for zero or nil (string) is bad. Move
|
||||
# this to a separate unassign action.
|
||||
should_unassign = team_ids.blank? || %w[nil 0].include?(team_ids[0].to_s)
|
||||
return @conversation.update!(team_id: nil) if should_unassign
|
||||
|
||||
# check if team belongs to account only if team_id is present
|
||||
# if team_id is nil, then it means that the team is being unassigned
|
||||
return unless !team_ids[0].nil? && team_belongs_to_account?(team_ids)
|
||||
|
||||
@@ -42,14 +42,12 @@ class Notification::PushNotificationService
|
||||
app_account_conversation_url(account_id: conversation.account_id, id: conversation.display_id)
|
||||
end
|
||||
|
||||
def send_browser_push?(subscription)
|
||||
def can_send_browser_push?(subscription)
|
||||
VapidService.public_key && subscription.browser_push?
|
||||
end
|
||||
|
||||
def send_browser_push(subscription)
|
||||
return unless send_browser_push?(subscription)
|
||||
|
||||
WebPush.payload_send(
|
||||
def browser_push_payload(subscription)
|
||||
{
|
||||
message: JSON.generate(push_message),
|
||||
endpoint: subscription.subscription_attributes['endpoint'],
|
||||
p256dh: subscription.subscription_attributes['p256dh'],
|
||||
@@ -62,11 +60,22 @@ class Notification::PushNotificationService
|
||||
ssl_timeout: 5,
|
||||
open_timeout: 5,
|
||||
read_timeout: 5
|
||||
)
|
||||
rescue WebPush::ExpiredSubscription
|
||||
}
|
||||
end
|
||||
|
||||
def send_browser_push(subscription)
|
||||
return unless can_send_browser_push?(subscription)
|
||||
|
||||
WebPush.payload_send(**browser_push_payload(subscription))
|
||||
Rails.logger.info("Browser push sent to #{user.email} with title #{push_message[:title]}")
|
||||
rescue WebPush::ExpiredSubscription, WebPush::InvalidSubscription, WebPush::Unauthorized => e
|
||||
Rails.logger.info "WebPush subscription expired: #{e.message}"
|
||||
subscription.destroy!
|
||||
rescue Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e
|
||||
Rails.logger.error "WebPush operation error: #{e.message}"
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e, account: notification.account).capture_exception
|
||||
true
|
||||
end
|
||||
|
||||
def send_fcm_push(subscription)
|
||||
@@ -98,7 +107,11 @@ class Notification::PushNotificationService
|
||||
end
|
||||
|
||||
def remove_subscription_if_error(subscription, response)
|
||||
subscription.destroy! if JSON.parse(response[:body])['results']&.first&.keys&.include?('error')
|
||||
if JSON.parse(response[:body])['results']&.first&.keys&.include?('error')
|
||||
subscription.destroy!
|
||||
else
|
||||
Rails.logger.info("FCM push sent to #{user.email} with title #{push_message[:title]}")
|
||||
end
|
||||
end
|
||||
|
||||
def fcm_options(subscription)
|
||||
|
||||
@@ -14,6 +14,7 @@ json.provider resource.provider
|
||||
json.pubsub_token resource.pubsub_token
|
||||
json.custom_attributes resource.custom_attributes if resource.custom_attributes.present?
|
||||
json.role resource.active_account_user&.role
|
||||
json.permissions resource.active_account_user&.permissions
|
||||
json.ui_settings resource.ui_settings
|
||||
json.uid resource.uid
|
||||
json.type resource.type
|
||||
@@ -24,6 +25,7 @@ json.accounts do
|
||||
json.status account_user.account.status
|
||||
json.active_at account_user.active_at
|
||||
json.role account_user.role
|
||||
json.permissions account_user.permissions
|
||||
# the actual availability user has configured
|
||||
json.availability account_user.availability
|
||||
# availability derived from presence
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
shared: &shared
|
||||
version: '3.10.0'
|
||||
version: '3.10.2'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
||||
+40
-40
@@ -31,7 +31,7 @@ vi:
|
||||
disposable_email: Chúng tôi không cho phép các email dùng một lần
|
||||
invalid_email: Bạn đã nhập một email không hợp lệ
|
||||
email_already_exists: "Bạn đã đăng ký một tài khoản với %{email}"
|
||||
invalid_params: 'Invalid, please check the signup paramters and try again'
|
||||
invalid_params: 'Không hợp lệ, vui lòng kiểm tra thông số đăng ký và thử lại'
|
||||
failed: Đăng ký thât bại
|
||||
data_import:
|
||||
data_type:
|
||||
@@ -51,7 +51,7 @@ vi:
|
||||
dyte:
|
||||
invalid_message_type: "Loại tin nhắn không hợp lệ. Hành động không được phép"
|
||||
slack:
|
||||
invalid_channel_id: "Invalid slack channel. Please try again"
|
||||
invalid_channel_id: "Kênh chùng không hợp lệ. Vui lòng thử lại"
|
||||
inboxes:
|
||||
imap:
|
||||
socket_error: Vui lòng kiểm tra kết nối mạng, địa chỉ IMAP và thử lại.
|
||||
@@ -63,43 +63,43 @@ vi:
|
||||
name: không nên bắt đầu hoặc kết thúc bằng các ký hiệu và không nên có kí tự < > / \ @.
|
||||
custom_filters:
|
||||
number_of_records: Đã đạt giới hạn. Số lượng tuỳ chọn lọc tối đa cho mỗi mỗi người dùng mỗi tài khoản là 50.
|
||||
invalid_attribute: Invalid attribute key - [%{key}]. The key should be one of [%{allowed_keys}] or a custom attribute defined in the account.
|
||||
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
|
||||
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
|
||||
invalid_attribute: Khóa thuộc tính không hợp lệ - [%{key}]. Chìa khóa phải là một trong [%{allowed_keys}] hoặc thuộc tính tùy chỉnh được xác định trong tài khoản.
|
||||
invalid_operator: Toán tử không hợp lệ. Các toán tử được phép cho %{attribute_name} là [%{allowed_keys}].
|
||||
invalid_value: Giá trị không hợp lệ. Các giá trị được cung cấp cho %{attribute_name} không hợp lệ
|
||||
reports:
|
||||
period: Thời gian báo cáo từ %{since} đến %{until}
|
||||
utc_warning: Báo cáo đã được tạo với múi giờ UTC
|
||||
agent_csv:
|
||||
agent_name: Tên tổng đài viên
|
||||
conversations_count: Assigned conversations
|
||||
avg_first_response_time: Avg first response time
|
||||
conversations_count: Cuộc trò chuyện được chỉ định
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Avg resolution time
|
||||
resolution_count: Số lượng giải quyết
|
||||
avg_customer_waiting_time: Avg customer waiting time
|
||||
avg_customer_waiting_time: Thời gian chờ đợi trung bình của khách hàng
|
||||
inbox_csv:
|
||||
inbox_name: Tên kênh
|
||||
inbox_type: Kiểu kênh
|
||||
conversations_count: Số hội thoại
|
||||
avg_first_response_time: Avg first response time
|
||||
avg_resolution_time: Avg resolution time
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Thời gian giải quyết trung bình
|
||||
label_csv:
|
||||
label_title: Nhãn
|
||||
conversations_count: Số hội thoại
|
||||
avg_first_response_time: Avg first response time
|
||||
avg_resolution_time: Avg resolution time
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Thời gian giải quyết trung bình
|
||||
team_csv:
|
||||
team_name: Tên nhóm
|
||||
conversations_count: Số hội thoại
|
||||
avg_first_response_time: Avg first response time
|
||||
avg_resolution_time: Avg resolution time
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Thời gian giải quyết trung bình
|
||||
resolution_count: Số lượng giải quyết
|
||||
avg_customer_waiting_time: Avg customer waiting time
|
||||
avg_customer_waiting_time: Thời gian chờ đợi trung bình của khách hàng
|
||||
conversation_traffic_csv:
|
||||
timezone: Múi giờ
|
||||
sla_csv:
|
||||
conversation_id: Conversation ID
|
||||
conversation_id: ID hội thoại
|
||||
sla_policy_breached: SLA Policy
|
||||
assignee: Assignee
|
||||
assignee: Đại lý được chỉ định
|
||||
team: Nhóm
|
||||
inbox: Hộp thư đến
|
||||
labels: Nhãn
|
||||
@@ -118,22 +118,22 @@ vi:
|
||||
recorded_at: Ngày nghi
|
||||
notifications:
|
||||
notification_title:
|
||||
conversation_creation: "A conversation (#%{display_id}) has been created in %{inbox_name}"
|
||||
conversation_assignment: "A conversation (#%{display_id}) has been assigned to you"
|
||||
assigned_conversation_new_message: "A new message is created in conversation (#%{display_id})"
|
||||
conversation_mention: "You have been mentioned in conversation (#%{display_id})"
|
||||
sla_missed_first_response: "SLA target first response missed for conversation (#%{display_id})"
|
||||
sla_missed_next_response: "SLA target next response missed for conversation (#%{display_id})"
|
||||
sla_missed_resolution: "SLA target resolution missed for conversation (#%{display_id})"
|
||||
attachment: "Attachment"
|
||||
no_content: "No content"
|
||||
conversation_creation: "Một cuộc trò chuyện (#%{display_id}) đã được tạo trong %{inbox_name}"
|
||||
conversation_assignment: "Một cuộc trò chuyện (#%{display_id}) đã được chỉ định cho bạn"
|
||||
assigned_conversation_new_message: "Một tin nhắn mới được tạo trong cuộc trò chuyện (#%{display_id})"
|
||||
conversation_mention: "Bạn đã được nhắc đến trong cuộc trò chuyện (#%{display_id})"
|
||||
sla_missed_first_response: "Mục tiêu SLA phản hồi đầu tiên bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})"
|
||||
sla_missed_next_response: "Mục tiêu SLA phản hồi tiếp theo bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})"
|
||||
sla_missed_resolution: "Độ phân giải mục tiêu SLA bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})"
|
||||
attachment: "Tập tin đính kèm"
|
||||
no_content: "Không có nội dung"
|
||||
conversations:
|
||||
messages:
|
||||
instagram_story_content: "%{story_sender} đã đề cập đến bạn trong hội thoại: "
|
||||
instagram_deleted_story_content: Hội thoại này không còn nữa.
|
||||
deleted: Tin nhắn đã bị xoá
|
||||
delivery_status:
|
||||
error_code: "Error code: %{error_code}"
|
||||
error_code: "Mã lỗi: %{error_code}"
|
||||
activity:
|
||||
status:
|
||||
resolved: "Cuộc trò chuyện được đánh dấu là đã giải quyết bởi %{user_name}"
|
||||
@@ -221,7 +221,7 @@ vi:
|
||||
common:
|
||||
home: Trang Chủ
|
||||
last_updated_on: 'Cập nhật lần cuối: %{last_updated_on}'
|
||||
view_all_articles: View all
|
||||
view_all_articles: Xem tất cả
|
||||
article: bài viết
|
||||
articles: bài viết
|
||||
author: tác giả
|
||||
@@ -233,17 +233,17 @@ vi:
|
||||
footer:
|
||||
made_with: Tạo bởi
|
||||
header:
|
||||
go_to_homepage: Website
|
||||
go_to_homepage: Trang web
|
||||
appearance:
|
||||
system: System
|
||||
light: Light
|
||||
dark: Dark
|
||||
featured_articles: Featured Articles
|
||||
system: Hệ thống
|
||||
light: Sáng
|
||||
dark: Tối
|
||||
featured_articles: Bài viết nổi bật
|
||||
uncategorized: Chưa được phân loại
|
||||
404:
|
||||
title: Page not found
|
||||
description: We couldn't find the page you were looking for.
|
||||
back_to_home: Go to home page
|
||||
title: Không tìm thấy trang
|
||||
description: Chúng tôi không thể tìm thấy trang bạn đang tìm kiếm.
|
||||
back_to_home: Tới trang chủ
|
||||
slack_unfurl:
|
||||
fields:
|
||||
name: Tên
|
||||
@@ -255,10 +255,10 @@ vi:
|
||||
button: Mở cuộc trò chuyện
|
||||
time_units:
|
||||
days:
|
||||
other: "%{count} days"
|
||||
other: "%{count} ngày"
|
||||
hours:
|
||||
other: "%{count} hours"
|
||||
other: "%{count} giờ"
|
||||
minutes:
|
||||
other: "%{count} minutes"
|
||||
other: "%{count} phút"
|
||||
seconds:
|
||||
other: "%{count} seconds"
|
||||
other: "%{count} giây"
|
||||
|
||||
@@ -16,10 +16,10 @@ KillMode=mixed
|
||||
StandardInput=null
|
||||
SyslogIdentifier=%p
|
||||
|
||||
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2/bin:/home/chatwoot/.rvm/gems/ruby-3.2.2@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.2.2/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
|
||||
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3/bin:/home/chatwoot/.rvm/gems/ruby-3.3.3@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.3.3/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
|
||||
Environment="PORT=3000"
|
||||
Environment="RAILS_ENV=production"
|
||||
Environment="NODE_ENV=production"
|
||||
Environment="RAILS_LOG_TO_STDOUT=true"
|
||||
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.2.2"
|
||||
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2:/home/chatwoot/.rvm/gems/ruby-3.2.2@global"
|
||||
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.3.3"
|
||||
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3:/home/chatwoot/.rvm/gems/ruby-3.3.3@global"
|
||||
|
||||
@@ -16,10 +16,10 @@ KillMode=mixed
|
||||
StandardInput=null
|
||||
SyslogIdentifier=%p
|
||||
|
||||
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2/bin:/home/chatwoot/.rvm/gems/ruby-3.2.2@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.2.2/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
|
||||
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3/bin:/home/chatwoot/.rvm/gems/ruby-3.3.3@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.3.3/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
|
||||
Environment="PORT=3000"
|
||||
Environment="RAILS_ENV=production"
|
||||
Environment="NODE_ENV=production"
|
||||
Environment="RAILS_LOG_TO_STDOUT=true"
|
||||
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.2.2"
|
||||
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2:/home/chatwoot/.rvm/gems/ruby-3.2.2@global"
|
||||
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.3.3"
|
||||
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3:/home/chatwoot/.rvm/gems/ruby-3.3.3@global"
|
||||
|
||||
@@ -336,8 +336,8 @@ function setup_chatwoot() {
|
||||
sudo -i -u chatwoot << EOF
|
||||
rvm --version
|
||||
rvm autolibs disable
|
||||
rvm install "ruby-3.2.2"
|
||||
rvm use 3.2.2 --default
|
||||
rvm install "ruby-3.3.3"
|
||||
rvm use 3.3.3 --default
|
||||
|
||||
git clone https://github.com/chatwoot/chatwoot.git
|
||||
cd chatwoot
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
# pre-build stage
|
||||
FROM ruby:3.2.2-alpine3.18 AS pre-builder
|
||||
FROM ruby:3.3.3-alpine3.19 AS pre-builder
|
||||
|
||||
# ARG default to production settings
|
||||
# For development docker-compose file overrides ARGS
|
||||
@@ -25,7 +25,7 @@ RUN apk update && apk add --no-cache \
|
||||
tzdata \
|
||||
postgresql-dev \
|
||||
postgresql-client \
|
||||
nodejs-current \
|
||||
nodejs=20.12.1-r0 \
|
||||
yarn \
|
||||
git \
|
||||
&& mkdir -p /var/app \
|
||||
@@ -67,13 +67,13 @@ RUN if [ "$RAILS_ENV" = "production" ]; then \
|
||||
RUN git rev-parse HEAD > /app/.git_sha
|
||||
|
||||
# Remove unnecessary files
|
||||
RUN rm -rf /gems/ruby/3.2.0/cache/*.gem \
|
||||
&& find /gems/ruby/3.2.0/gems/ \( -name "*.c" -o -name "*.o" \) -delete \
|
||||
RUN rm -rf /gems/ruby/3.3.0/cache/*.gem \
|
||||
&& find /gems/ruby/3.3.0/gems/ \( -name "*.c" -o -name "*.o" \) -delete \
|
||||
&& rm -rf .git \
|
||||
&& rm .gitignore
|
||||
|
||||
# final build stage
|
||||
FROM ruby:3.2.2-alpine3.18
|
||||
FROM ruby:3.3.3-alpine3.19
|
||||
|
||||
|
||||
ARG BUNDLE_WITHOUT="development:test"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "3.10.0",
|
||||
"version": "3.10.2",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/**/*.{js,vue}",
|
||||
|
||||
@@ -41,6 +41,17 @@ RSpec.describe 'Session', type: :request do
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(user_with_new_pwd.email)
|
||||
end
|
||||
|
||||
it 'returns the permission of the user' do
|
||||
params = { email: user.email, password: 'Password1!' }
|
||||
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['data']['permissions']).to eq(['agent'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is invalid sso auth token' do
|
||||
|
||||
@@ -16,5 +16,17 @@ FactoryBot.define do
|
||||
conversation.contact ||= create(:contact, :with_email, account: conversation.account)
|
||||
conversation.contact_inbox ||= create(:contact_inbox, contact: conversation.contact, inbox: conversation.inbox)
|
||||
end
|
||||
|
||||
trait :with_team do
|
||||
after(:build) do |conversation|
|
||||
conversation.team ||= create(:team, account: conversation.account)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_assignee do
|
||||
after(:build) do |conversation|
|
||||
conversation.assignee ||= create(:user, account: conversation.account, role: :agent)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,5 +6,13 @@ FactoryBot.define do
|
||||
identifier { 'test' }
|
||||
subscription_type { 'browser_push' }
|
||||
subscription_attributes { { endpoint: 'test', auth: 'test' } }
|
||||
|
||||
trait :browser_push do
|
||||
subscription_type { 'browser_push' }
|
||||
end
|
||||
|
||||
trait :fcm do
|
||||
subscription_type { 'fcm' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,6 +17,17 @@ RSpec.describe AccountUser do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'permissions' do
|
||||
it 'returns the right permissions' do
|
||||
expect(account_user.permissions).to eq(['agent'])
|
||||
end
|
||||
|
||||
it 'returns the right permissions for administrator' do
|
||||
account_user.administrator!
|
||||
expect(account_user.permissions).to eq(['administrator'])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'destroy call agent::destroy service' do
|
||||
it 'gets created with the right default settings' do
|
||||
create(:conversation, account: account_user.account, assignee: account_user.user, inbox: inbox)
|
||||
|
||||
@@ -31,18 +31,54 @@ describe ActionService do
|
||||
|
||||
describe '#assign_agent' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:inbox_member) { create(:inbox_member, inbox: conversation.inbox, user: agent) }
|
||||
let(:conversation) { create(:conversation, :with_assignee, account: account) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
|
||||
it 'unassigns the conversation if agent id is nil' do
|
||||
action_service.assign_agent(['nil'])
|
||||
expect(conversation.reload.assignee).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'unassigns the team if team_id is nil' do
|
||||
action_service.assign_team([nil])
|
||||
expect(conversation.reload.team).to be_nil
|
||||
describe '#assign_team' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:inbox_member) { create(:inbox_member, inbox: conversation.inbox, user: agent) }
|
||||
let(:team) { create(:team, name: 'ConversationTeam', account: account) }
|
||||
let(:conversation) { create(:conversation, :with_team, account: account) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
|
||||
context 'when team_id is not present' do
|
||||
it 'unassign the if team_id is "nil"' do
|
||||
expect do
|
||||
action_service.assign_team(['nil'])
|
||||
end.not_to raise_error
|
||||
expect(conversation.reload.team).to be_nil
|
||||
end
|
||||
|
||||
it 'unassign the if team_id is 0' do
|
||||
expect do
|
||||
action_service.assign_team([0])
|
||||
end.not_to raise_error
|
||||
expect(conversation.reload.team).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when team_id is present' do
|
||||
it 'assign the team if the team is part of the account' do
|
||||
original_team = conversation.team
|
||||
expect do
|
||||
action_service.assign_team([team.id])
|
||||
end.to change { conversation.reload.team }.from(original_team)
|
||||
end
|
||||
|
||||
it 'does not assign the team if the team is part of the account' do
|
||||
original_team = conversation.team
|
||||
invalid_team_id = 999_999_999
|
||||
expect do
|
||||
action_service.assign_team([invalid_team_id])
|
||||
end.not_to change { conversation.reload.team }.from(original_team)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,33 +7,58 @@ describe Notification::PushNotificationService do
|
||||
let(:fcm_double) { instance_double(FCM) }
|
||||
let(:fcm_service_double) { instance_double(Notification::FcmService, fcm_client: fcm_double) }
|
||||
|
||||
before do
|
||||
allow(WebPush).to receive(:payload_send).and_return(true)
|
||||
allow(Notification::FcmService).to receive(:new).and_return(fcm_service_double)
|
||||
allow(fcm_double).to receive(:send_v1).and_return({ body: { 'results': [] }.to_json })
|
||||
allow(GlobalConfigService).to receive(:load).with('FIREBASE_PROJECT_ID', nil).and_return('test_project_id')
|
||||
allow(GlobalConfigService).to receive(:load).with('FIREBASE_CREDENTIALS', nil).and_return('test_credentials')
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'sends webpush notifications for webpush subscription' do
|
||||
with_modified_env VAPID_PUBLIC_KEY: 'test' do
|
||||
create(:notification_subscription, user: notification.user)
|
||||
context 'when the push server returns success' do
|
||||
before do
|
||||
allow(WebPush).to receive(:payload_send).and_return(true)
|
||||
allow(Rails.logger).to receive(:info)
|
||||
allow(Notification::FcmService).to receive(:new).and_return(fcm_service_double)
|
||||
allow(fcm_double).to receive(:send_v1).and_return({ body: { 'results': [] }.to_json })
|
||||
allow(GlobalConfigService).to receive(:load).with('FIREBASE_PROJECT_ID', nil).and_return('test_project_id')
|
||||
allow(GlobalConfigService).to receive(:load).with('FIREBASE_CREDENTIALS', nil).and_return('test_credentials')
|
||||
end
|
||||
|
||||
described_class.new(notification: notification).perform
|
||||
expect(WebPush).to have_received(:payload_send)
|
||||
expect(Notification::FcmService).not_to have_received(:new)
|
||||
it 'sends webpush notifications for webpush subscription' do
|
||||
with_modified_env VAPID_PUBLIC_KEY: 'test' do
|
||||
create(:notification_subscription, user: notification.user)
|
||||
|
||||
described_class.new(notification: notification).perform
|
||||
expect(WebPush).to have_received(:payload_send)
|
||||
expect(Notification::FcmService).not_to have_received(:new)
|
||||
expect(Rails.logger).to have_received(:info).with("Browser push sent to #{user.email} with title #{notification.push_message_title}")
|
||||
end
|
||||
end
|
||||
|
||||
it 'sends a fcm notification for firebase subscription' do
|
||||
with_modified_env ENABLE_PUSH_RELAY_SERVER: 'false' do
|
||||
create(:notification_subscription, user: notification.user, subscription_type: 'fcm')
|
||||
|
||||
described_class.new(notification: notification).perform
|
||||
expect(Notification::FcmService).to have_received(:new)
|
||||
expect(fcm_double).to have_received(:send_v1)
|
||||
expect(WebPush).not_to have_received(:payload_send)
|
||||
expect(Rails.logger).to have_received(:info).with("FCM push sent to #{user.email} with title #{notification.push_message_title}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'sends a fcm notification for firebase subscription' do
|
||||
with_modified_env ENABLE_PUSH_RELAY_SERVER: 'false' do
|
||||
create(:notification_subscription, user: notification.user, subscription_type: 'fcm')
|
||||
context 'when the push server returns error' do
|
||||
it 'sends webpush notifications for webpush subscription' do
|
||||
with_modified_env VAPID_PUBLIC_KEY: 'test' do
|
||||
mock_response = instance_double(Net::HTTPResponse, body: 'Subscription is invalid')
|
||||
mock_host = 'fcm.googleapis.com'
|
||||
|
||||
allow(WebPush).to receive(:payload_send).and_raise(WebPush::InvalidSubscription.new(mock_response, mock_host))
|
||||
allow(Rails.logger).to receive(:info)
|
||||
|
||||
create(:notification_subscription, :browser_push, user: notification.user)
|
||||
|
||||
expect(Rails.logger).to receive(:info) do |message|
|
||||
expect(message).to include('WebPush subscription expired:')
|
||||
end
|
||||
|
||||
described_class.new(notification: notification).perform
|
||||
expect(Notification::FcmService).to have_received(:new)
|
||||
expect(fcm_double).to have_received(:send_v1)
|
||||
expect(WebPush).not_to have_received(:payload_send)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21315,21 +21315,16 @@ write-file-atomic@^4.0.2:
|
||||
signal-exit "^3.0.7"
|
||||
|
||||
ws@^6.2.1:
|
||||
version "6.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
|
||||
integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
|
||||
version "6.2.3"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee"
|
||||
integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
|
||||
ws@^8.11.0:
|
||||
version "8.14.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f"
|
||||
integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
|
||||
|
||||
ws@^8.2.3:
|
||||
version "8.13.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
||||
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
||||
ws@^8.11.0, ws@^8.2.3:
|
||||
version "8.17.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
|
||||
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
|
||||
|
||||
x-default-browser@^0.4.0:
|
||||
version "0.4.0"
|
||||
|
||||
Reference in New Issue
Block a user