Merge branch 'develop' into chore/rename_private
This commit is contained in:
commit
49ca3b09ca
44 changed files with 678 additions and 100 deletions
8
SECURITY.md
Normal file
8
SECURITY.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose).
|
||||
This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
|
||||
|
||||
If you have any questions about the process, feel free to reach out to hello@chatwoot.com.
|
|
@ -43,6 +43,8 @@ class ContactBuilder
|
|||
|
||||
contact ||= account.contacts.find_by(email: contact_attributes[:email]) if contact_attributes[:email].present?
|
||||
|
||||
contact ||= account.contacts.find_by(phone_number: contact_attributes[:phone_number]) if contact_attributes[:phone_number].present?
|
||||
|
||||
contact
|
||||
end
|
||||
|
||||
|
|
|
@ -7,6 +7,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
|
||||
end
|
||||
|
||||
def assignable_agents
|
||||
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
channel = create_channel
|
||||
|
|
44
app/controllers/super_admin/platform_apps_controller.rb
Normal file
44
app/controllers/super_admin/platform_apps_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
class SuperAdmin::PlatformAppsController < SuperAdmin::ApplicationController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
|
@ -57,8 +57,8 @@ class AccessTokenDashboard < Administrate::BaseDashboard
|
|||
# }.freeze
|
||||
COLLECTION_FILTERS = {
|
||||
user: ->(resources) { resources.where(owner_type: 'User') },
|
||||
super_admin: ->(resources) { resources.where(owner_type: 'SuperAdmin') },
|
||||
agent_bot: ->(resources) { resources.where(owner_type: 'AgentBot') }
|
||||
agent_bot: ->(resources) { resources.where(owner_type: 'AgentBot') },
|
||||
platform_app: ->(resources) { resources.where(owner_type: 'PlatformApp') }
|
||||
}.freeze
|
||||
|
||||
# Overwrite this method to customize how access tokens are displayed
|
||||
|
|
62
app/dashboards/platform_app_dashboard.rb
Normal file
62
app/dashboards/platform_app_dashboard.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
require 'administrate/base_dashboard'
|
||||
|
||||
class PlatformAppDashboard < Administrate::BaseDashboard
|
||||
# ATTRIBUTE_TYPES
|
||||
# a hash that describes the type of each of the model's fields.
|
||||
#
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
access_token: Field::HasOne,
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
#
|
||||
# By default, it's limited to four items to reduce clutter on index pages.
|
||||
# Feel free to add, remove, or rearrange items.
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
name
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
name
|
||||
created_at
|
||||
updated_at
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
name
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
# field of the dashboard.
|
||||
#
|
||||
# For example to add an option to search for open resources by typing "open:"
|
||||
# in the search field:
|
||||
#
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how platform apps are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
#
|
||||
# def display_resource(platform_app)
|
||||
# "PlatformApp ##{platform_app.id}"
|
||||
# end
|
||||
end
|
|
@ -1,9 +1,14 @@
|
|||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class Inboxes extends ApiClient {
|
||||
constructor() {
|
||||
super('inboxes', { accountScoped: true });
|
||||
}
|
||||
|
||||
getAssignableAgents(inboxId) {
|
||||
return axios.get(`${this.url}/${inboxId}/assignable_agents`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Inboxes();
|
||||
|
|
|
@ -29,14 +29,24 @@
|
|||
|
||||
&::before {
|
||||
right: 0;
|
||||
top: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__content .multiselect__option {
|
||||
.multiselect__content {
|
||||
max-width: 100%;
|
||||
|
||||
.multiselect__option {
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-normal;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.multiselect__option--highlight {
|
||||
background: var(--white);
|
||||
color: var(--color-body);
|
||||
|
@ -67,8 +77,9 @@
|
|||
&::after {
|
||||
background: transparent;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after:hover {
|
||||
color: var(--color-body);
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +137,6 @@
|
|||
}
|
||||
|
||||
.sidebar-labels-wrap {
|
||||
|
||||
&.has-edited,
|
||||
&:hover {
|
||||
.multiselect {
|
||||
|
@ -149,7 +159,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.multiselect-wrap--small {
|
||||
$multiselect-height: 3.8rem;
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ $resolve-button-width: 13.2rem;
|
|||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--space-smaller);
|
||||
margin-right: var(--space-small);
|
||||
width: 20.2rem;
|
||||
width: 21.6rem;
|
||||
|
||||
.icon {
|
||||
color: $medium-gray;
|
||||
|
@ -25,11 +25,12 @@ $resolve-button-width: 13.2rem;
|
|||
}
|
||||
|
||||
.multiselect {
|
||||
border-radius: var(--border-radius-small);
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
|
||||
.multiselect__tags {
|
||||
border: 0;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ export default {
|
|||
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
|
||||
},
|
||||
currentPage() {
|
||||
return this.$store.getters['conversationPage/getCurrentPage'](
|
||||
return this.$store.getters['conversationPage/getCurrentPageFilter'](
|
||||
this.activeAssigneeTab
|
||||
);
|
||||
},
|
||||
|
@ -199,6 +199,7 @@ export default {
|
|||
},
|
||||
updateAssigneeTab(selectedTab) {
|
||||
if (this.activeAssigneeTab !== selectedTab) {
|
||||
bus.$emit('clearSearchInput');
|
||||
this.activeAssigneeTab = selectedTab;
|
||||
if (!this.currentPage) {
|
||||
this.fetchConversations();
|
||||
|
|
|
@ -2,14 +2,13 @@
|
|||
<transition-group name="toast-fade" tag="div" class="ui-snackbar-container">
|
||||
<woot-snackbar
|
||||
v-for="snackMessage in snackMessages"
|
||||
:key="snackMessage"
|
||||
:message="snackMessage"
|
||||
:key="snackMessage.key"
|
||||
:message="snackMessage.message"
|
||||
/>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
import WootSnackbar from './Snackbar';
|
||||
|
||||
export default {
|
||||
|
@ -31,7 +30,7 @@ export default {
|
|||
|
||||
mounted() {
|
||||
bus.$on('newToastMessage', message => {
|
||||
this.snackMessages.push(message);
|
||||
this.snackMessages.push({ key: new Date().getTime(), message });
|
||||
window.setTimeout(() => {
|
||||
this.snackMessages.splice(0, 1);
|
||||
}, this.duration);
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
</woot-button>
|
||||
<woot-button
|
||||
v-if="showDropDown"
|
||||
class="icon--small"
|
||||
:color-scheme="buttonClass"
|
||||
:disabled="isLoading"
|
||||
icon="ion-arrow-down-b"
|
||||
|
@ -136,6 +135,7 @@ export default {
|
|||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dropdown-pane {
|
||||
left: unset;
|
||||
top: 4.2rem;
|
||||
|
@ -143,7 +143,4 @@ export default {
|
|||
right: 0;
|
||||
max-width: 20rem;
|
||||
}
|
||||
.icon--small::v-deep .icon {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
<template>
|
||||
<div class="status">
|
||||
<div class="status-view">
|
||||
<div
|
||||
:class="`status-badge status-badge__${currentUserAvailabilityStatus}`"
|
||||
/>
|
||||
|
||||
<availability-status-badge :status="currentUserAvailabilityStatus" />
|
||||
<div class="status-view--title">
|
||||
{{ availabilityDisplayLabel }}
|
||||
</div>
|
||||
|
@ -28,7 +25,7 @@
|
|||
:disabled="status.disabled"
|
||||
@click="changeAvailabilityStatus(status.value)"
|
||||
>
|
||||
<span :class="`status-badge status-badge__${status.value}`" />
|
||||
<availability-status-badge :status="status.value" />
|
||||
{{ status.label }}
|
||||
</button>
|
||||
</woot-dropdown-item>
|
||||
|
@ -48,6 +45,7 @@ import { mapGetters } from 'vuex';
|
|||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge';
|
||||
|
||||
const AVAILABILITY_STATUS_KEYS = ['online', 'busy', 'offline'];
|
||||
|
||||
|
@ -55,6 +53,7 @@ export default {
|
|||
components: {
|
||||
WootDropdownMenu,
|
||||
WootDropdownItem,
|
||||
AvailabilityStatusBadge,
|
||||
},
|
||||
|
||||
mixins: [clickaway],
|
||||
|
@ -147,26 +146,6 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
width: var(--space-one);
|
||||
height: var(--space-one);
|
||||
margin-right: var(--space-micro);
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
|
||||
&__online {
|
||||
background: var(--g-400);
|
||||
}
|
||||
|
||||
&__offline {
|
||||
background: var(--b-600);
|
||||
}
|
||||
|
||||
&__busy {
|
||||
background: var(--y-700);
|
||||
}
|
||||
}
|
||||
|
||||
.status-change {
|
||||
.dropdown-pane {
|
||||
top: -132px;
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div :class="`status-badge status-badge__${status}`" />
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
status: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
.status-badge {
|
||||
width: var(--space-one);
|
||||
height: var(--space-one);
|
||||
margin-right: var(--space-micro);
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
&__online {
|
||||
background: var(--g-400);
|
||||
}
|
||||
&__offline {
|
||||
background: var(--b-600);
|
||||
}
|
||||
&__busy {
|
||||
background: var(--y-700);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -63,7 +63,7 @@ export default {
|
|||
watch: {
|
||||
'currentChat.inbox_id'(inboxId) {
|
||||
if (inboxId) {
|
||||
this.$store.dispatch('inboxMembers/fetch', { inboxId });
|
||||
this.$store.dispatch('inboxAssignableAgents/fetch', { inboxId });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -36,16 +36,24 @@
|
|||
v-model="currentChat.meta.assignee"
|
||||
:loading="uiFlags.isFetching"
|
||||
:allow-empty="true"
|
||||
:deselect-label="$t('CONVERSATION.ASSIGNMENT.REMOVE')"
|
||||
deselect-label=""
|
||||
:options="agentList"
|
||||
:placeholder="$t('CONVERSATION.ASSIGNMENT.SELECT_AGENT')"
|
||||
:select-label="$t('CONVERSATION.ASSIGNMENT.ASSIGN')"
|
||||
select-label=""
|
||||
label="name"
|
||||
selected-label
|
||||
track-by="id"
|
||||
@select="assignAgent"
|
||||
@remove="removeAgent"
|
||||
>
|
||||
<template slot="option" slot-scope="props">
|
||||
<div class="option__desc">
|
||||
<availability-status-badge
|
||||
:status="props.option.availability_status"
|
||||
/>
|
||||
<span class="option__title">{{ props.option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span slot="noResult">{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
|
@ -57,11 +65,13 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
import MoreActions from './MoreActions';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import AvailabilityStatusBadge from '../conversation/AvailabilityStatusBadge';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MoreActions,
|
||||
Thumbnail,
|
||||
AvailabilityStatusBadge,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -83,8 +93,8 @@ export default {
|
|||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAgents: 'inboxMembers/getMembersByInbox',
|
||||
uiFlags: 'inboxMembers/getUIFlags',
|
||||
getAgents: 'inboxAssignableAgents/getAssignableAgents',
|
||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
|
||||
|
@ -126,7 +136,6 @@ export default {
|
|||
bus.$emit('newToastMessage', this.$t('CONVERSATION.CHANGE_AGENT'));
|
||||
});
|
||||
},
|
||||
|
||||
removeAgent() {},
|
||||
},
|
||||
};
|
||||
|
@ -142,4 +151,17 @@ export default {
|
|||
.conv-header {
|
||||
flex: 0 0 var(--space-jumbo);
|
||||
}
|
||||
|
||||
.option__desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.option__desc {
|
||||
&::v-deep .status-badge {
|
||||
margin-right: var(--space-small);
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,6 +17,7 @@ import ja from './locale/ja';
|
|||
import ko from './locale/ko';
|
||||
import ml from './locale/ml';
|
||||
import nl from './locale/nl';
|
||||
import no from './locale/no';
|
||||
import pl from './locale/pl';
|
||||
import pt from './locale/pt';
|
||||
import pt_BR from './locale/pt_BR';
|
||||
|
@ -28,7 +29,7 @@ import ta from './locale/ta';
|
|||
import tr from './locale/tr';
|
||||
import uk from './locale/uk';
|
||||
import vi from './locale/vi';
|
||||
import zh from './locale/zh';
|
||||
import zh_CN from './locale/zh_CN';
|
||||
import zh_TW from './locale/zh_TW';
|
||||
|
||||
export default {
|
||||
|
@ -51,6 +52,7 @@ export default {
|
|||
ko,
|
||||
ml,
|
||||
nl,
|
||||
no,
|
||||
pl,
|
||||
pt,
|
||||
pt_BR,
|
||||
|
@ -62,6 +64,6 @@ export default {
|
|||
tr,
|
||||
uk,
|
||||
vi,
|
||||
zh,
|
||||
zh_CN,
|
||||
zh_TW,
|
||||
};
|
||||
|
|
33
app/javascript/dashboard/i18n/locale/no/index.js
Normal file
33
app/javascript/dashboard/i18n/locale/no/index.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { default as _agentMgmt } from './agentMgmt.json';
|
||||
import { default as _labelsMgmt } from './labelsMgmt.json';
|
||||
import { default as _cannedMgmt } from './cannedMgmt.json';
|
||||
import { default as _chatlist } from './chatlist.json';
|
||||
import { default as _contact } from './contact.json';
|
||||
import { default as _conversation } from './conversation.json';
|
||||
import { default as _inboxMgmt } from './inboxMgmt.json';
|
||||
import { default as _login } from './login.json';
|
||||
import { default as _report } from './report.json';
|
||||
import { default as _resetPassword } from './resetPassword.json';
|
||||
import { default as _setNewPassword } from './setNewPassword.json';
|
||||
import { default as _settings } from './settings.json';
|
||||
import { default as _signup } from './signup.json';
|
||||
import { default as _integrations } from './integrations.json';
|
||||
import { default as _generalSettings } from './generalSettings.json';
|
||||
|
||||
export default {
|
||||
..._agentMgmt,
|
||||
..._cannedMgmt,
|
||||
..._chatlist,
|
||||
..._contact,
|
||||
..._conversation,
|
||||
..._inboxMgmt,
|
||||
..._login,
|
||||
..._report,
|
||||
..._labelsMgmt,
|
||||
..._resetPassword,
|
||||
..._setNewPassword,
|
||||
..._settings,
|
||||
..._signup,
|
||||
..._integrations,
|
||||
..._generalSettings,
|
||||
};
|
33
app/javascript/dashboard/i18n/locale/zh_CN/index.js
Normal file
33
app/javascript/dashboard/i18n/locale/zh_CN/index.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { default as _agentMgmt } from './agentMgmt.json';
|
||||
import { default as _labelsMgmt } from './labelsMgmt.json';
|
||||
import { default as _cannedMgmt } from './cannedMgmt.json';
|
||||
import { default as _chatlist } from './chatlist.json';
|
||||
import { default as _contact } from './contact.json';
|
||||
import { default as _conversation } from './conversation.json';
|
||||
import { default as _inboxMgmt } from './inboxMgmt.json';
|
||||
import { default as _login } from './login.json';
|
||||
import { default as _report } from './report.json';
|
||||
import { default as _resetPassword } from './resetPassword.json';
|
||||
import { default as _setNewPassword } from './setNewPassword.json';
|
||||
import { default as _settings } from './settings.json';
|
||||
import { default as _signup } from './signup.json';
|
||||
import { default as _integrations } from './integrations.json';
|
||||
import { default as _generalSettings } from './generalSettings.json';
|
||||
|
||||
export default {
|
||||
..._agentMgmt,
|
||||
..._cannedMgmt,
|
||||
..._chatlist,
|
||||
..._contact,
|
||||
..._conversation,
|
||||
..._inboxMgmt,
|
||||
..._login,
|
||||
..._report,
|
||||
..._labelsMgmt,
|
||||
..._resetPassword,
|
||||
..._setNewPassword,
|
||||
..._settings,
|
||||
..._signup,
|
||||
..._integrations,
|
||||
..._generalSettings,
|
||||
};
|
|
@ -28,7 +28,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import Auth from '../../api/auth';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
@ -74,8 +73,12 @@ export default {
|
|||
this.showAlert(successMessage);
|
||||
window.location = frontendURL('login');
|
||||
})
|
||||
.catch(() => {
|
||||
this.showAlert(this.$t('RESET_PASSWORD.API.ERROR_MESSAGE'));
|
||||
.catch(error => {
|
||||
let errorMessage = this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
|
||||
if (error?.response?.data?.message) {
|
||||
errorMessage = error.response.data.message;
|
||||
}
|
||||
this.showAlert(errorMessage);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
@ -22,7 +22,17 @@
|
|||
selected-label=""
|
||||
:placeholder="$t('CONVERSATION_SIDEBAR.SELECT.PLACEHOLDER')"
|
||||
:allow-empty="true"
|
||||
>
|
||||
<template slot="option" slot-scope="props">
|
||||
<div class="option__desc">
|
||||
<availability-status-badge
|
||||
:status="props.option.availability_status"
|
||||
/>
|
||||
<span class="option__title">{{ props.option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span slot="noResult">{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div class="multiselect-wrap--small">
|
||||
<label class="multiselect__label">
|
||||
|
@ -38,7 +48,9 @@
|
|||
selected-label=""
|
||||
:placeholder="$t('CONVERSATION_SIDEBAR.SELECT.PLACEHOLDER')"
|
||||
:allow-empty="true"
|
||||
/>
|
||||
>
|
||||
<span slot="noResult">{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="browser.browser_name" class="conversation--details">
|
||||
|
@ -111,6 +123,8 @@ import ContactDetailsItem from './ContactDetailsItem.vue';
|
|||
import ContactInfo from './contact/ContactInfo';
|
||||
import ConversationLabels from './labels/LabelBox.vue';
|
||||
import ContactCustomAttributes from './ContactCustomAttributes';
|
||||
import AvailabilityStatusBadge from 'dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue';
|
||||
|
||||
import flag from 'country-code-emoji';
|
||||
|
||||
export default {
|
||||
|
@ -120,6 +134,7 @@ export default {
|
|||
ContactDetailsItem,
|
||||
ContactInfo,
|
||||
ConversationLabels,
|
||||
AvailabilityStatusBadge,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
|
@ -140,8 +155,8 @@ export default {
|
|||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
teams: 'teams/getTeams',
|
||||
getAgents: 'inboxMembers/getMembersByInbox',
|
||||
uiFlags: 'inboxMembers/getUIFlags',
|
||||
getAgents: 'inboxAssignableAgents/getAssignableAgents',
|
||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
}),
|
||||
currentConversationMetaData() {
|
||||
return this.$store.getters[
|
||||
|
@ -292,6 +307,14 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.multiselect-wrap--small {
|
||||
&::v-deep .multiselect__element {
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
right: $space-normal;
|
||||
|
@ -342,4 +365,14 @@ export default {
|
|||
.multiselect__label {
|
||||
margin-bottom: var(--space-smaller);
|
||||
}
|
||||
.option__desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::v-deep .status-badge {
|
||||
margin-right: var(--space-small);
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -82,6 +82,7 @@ export default {
|
|||
...mapGetters({
|
||||
conversations: 'conversationSearch/getConversations',
|
||||
uiFlags: 'conversationSearch/getUIFlags',
|
||||
currentPage: 'conversationPage/getCurrentPage',
|
||||
}),
|
||||
resultsCount() {
|
||||
return this.conversations.length;
|
||||
|
@ -111,10 +112,16 @@ export default {
|
|||
this.$store.dispatch('conversationSearch/get', { q: newValue });
|
||||
}, 1000);
|
||||
},
|
||||
currentPage() {
|
||||
this.clearSearchTerm();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$store.dispatch('conversationSearch/get', { q: '' });
|
||||
bus.$on('clearSearchInput', () => {
|
||||
this.clearSearchTerm();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -124,6 +131,9 @@ export default {
|
|||
closeSearch() {
|
||||
this.showSearchBox = false;
|
||||
},
|
||||
clearSearchTerm() {
|
||||
this.searchTerm = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -18,6 +18,7 @@ import conversationTypingStatus from './modules/conversationTypingStatus';
|
|||
import globalConfig from 'shared/store/globalConfig';
|
||||
import inboxes from './modules/inboxes';
|
||||
import inboxMembers from './modules/inboxMembers';
|
||||
import inboxAssignableAgents from './modules/inboxAssignableAgents';
|
||||
import integrations from './modules/integrations';
|
||||
import labels from './modules/labels';
|
||||
import reports from './modules/reports';
|
||||
|
@ -46,6 +47,7 @@ export default new Vuex.Store({
|
|||
globalConfig,
|
||||
inboxes,
|
||||
inboxMembers,
|
||||
inboxAssignableAgents,
|
||||
integrations,
|
||||
labels,
|
||||
reports,
|
||||
|
|
|
@ -18,9 +18,12 @@ export const getters = {
|
|||
getHasEndReached: $state => filter => {
|
||||
return $state.hasEndReached[filter];
|
||||
},
|
||||
getCurrentPage: $state => filter => {
|
||||
getCurrentPageFilter: $state => filter => {
|
||||
return $state.currentPage[filter];
|
||||
},
|
||||
getCurrentPage: $state => {
|
||||
return $state.currentPage;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
|
|
@ -13,7 +13,7 @@ export const applyPageFilters = (conversation, filters) => {
|
|||
labels: chatLabels = [],
|
||||
meta = {},
|
||||
} = conversation;
|
||||
const { team = {} } = meta;
|
||||
const team = meta.team || {};
|
||||
const { id: chatTeamId } = team;
|
||||
const filterByStatus = chatStatus === status;
|
||||
let shouldFilter = filterByStatus;
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import InboxesAPI from 'dashboard/api/inboxes.js';
|
||||
|
||||
const state = {
|
||||
records: {},
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const types = {
|
||||
SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG: 'SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG',
|
||||
SET_INBOX_ASSIGNABLE_AGENTS: 'SET_INBOX_ASSIGNABLE_AGENTS',
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getAssignableAgents: $state => inboxId => {
|
||||
const allAgents = $state.records[inboxId] || [];
|
||||
const verifiedAgents = allAgents.filter(record => record.confirmed);
|
||||
return verifiedAgents;
|
||||
},
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
async fetch({ commit }, { inboxId }) {
|
||||
commit(types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const {
|
||||
data: { payload },
|
||||
} = await InboxesAPI.getAssignableAgents(inboxId);
|
||||
commit(types.SET_INBOX_ASSIGNABLE_AGENTS, { inboxId, members: payload });
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
commit(types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG]($state, data) {
|
||||
$state.uiFlags = {
|
||||
...$state.uiFlags,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
[types.SET_INBOX_ASSIGNABLE_AGENTS]: ($state, { inboxId, members }) => {
|
||||
Vue.set($state.records, inboxId, members);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
|
@ -9,12 +9,25 @@ describe('#getters', () => {
|
|||
all: 3,
|
||||
},
|
||||
};
|
||||
expect(getters.getCurrentPage(state)('me')).toEqual(1);
|
||||
expect(getters.getCurrentPage(state)('unassigned')).toEqual(2);
|
||||
expect(getters.getCurrentPage(state)('all')).toEqual(3);
|
||||
expect(getters.getCurrentPage(state)).toHaveProperty('me');
|
||||
expect(getters.getCurrentPage(state)).toHaveProperty('unassigned');
|
||||
expect(getters.getCurrentPage(state)).toHaveProperty('all');
|
||||
});
|
||||
|
||||
it('getCurrentPage', () => {
|
||||
it('getCurrentPageFilter', () => {
|
||||
const state = {
|
||||
currentPage: {
|
||||
me: 1,
|
||||
unassigned: 2,
|
||||
all: 3,
|
||||
},
|
||||
};
|
||||
expect(getters.getCurrentPageFilter(state)('me')).toEqual(1);
|
||||
expect(getters.getCurrentPageFilter(state)('unassigned')).toEqual(2);
|
||||
expect(getters.getCurrentPageFilter(state)('all')).toEqual(3);
|
||||
});
|
||||
|
||||
it('getHasEndReached', () => {
|
||||
const state = {
|
||||
hasEndReached: {
|
||||
me: false,
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import axios from 'axios';
|
||||
import { actions, types } from '../../inboxAssignableAgents';
|
||||
import agentsData from './fixtures';
|
||||
|
||||
const commit = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#fetch', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: { payload: agentsData },
|
||||
});
|
||||
await actions.fetch({ commit }, { inboxId: 1 });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true }],
|
||||
[
|
||||
types.SET_INBOX_ASSIGNABLE_AGENTS,
|
||||
{ inboxId: 1, members: agentsData },
|
||||
],
|
||||
[types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.fetch({ commit }, { inboxId: 1 })).rejects.toThrow(
|
||||
Error
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true }],
|
||||
[types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
export default [
|
||||
{
|
||||
id: 1,
|
||||
provider: 'email',
|
||||
uid: 'agent1@chatwoot.com',
|
||||
name: 'Agent1',
|
||||
email: 'agent1@chatwoot.com',
|
||||
account_id: 1,
|
||||
created_at: '2019-11-18T02:21:06.225Z',
|
||||
updated_at: '2019-12-20T07:43:35.794Z',
|
||||
pubsub_token: 'random-1',
|
||||
role: 'agent',
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
provider: 'email',
|
||||
uid: 'agent2@chatwoot.com',
|
||||
name: 'Agent2',
|
||||
email: 'agent2@chatwoot.com',
|
||||
account_id: 1,
|
||||
created_at: '2019-11-18T02:21:06.225Z',
|
||||
updated_at: '2019-12-20T07:43:35.794Z',
|
||||
pubsub_token: 'random-2',
|
||||
role: 'agent',
|
||||
confirmed: true,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,24 @@
|
|||
import { getters } from '../../teamMembers';
|
||||
import agentsData from './fixtures';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getAssignableAgents', () => {
|
||||
const state = {
|
||||
records: {
|
||||
1: [agentsData[0]],
|
||||
},
|
||||
};
|
||||
expect(getters.getTeamMembers(state)(1)).toEqual([agentsData[0]]);
|
||||
});
|
||||
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
},
|
||||
};
|
||||
expect(getters.getUIFlags(state)).toEqual({
|
||||
isFetching: false,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import { mutations, types } from '../../inboxAssignableAgents';
|
||||
import agentsData from './fixtures';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_INBOX_ASSIGNABLE_AGENTS', () => {
|
||||
it('Adds inbox members to records', () => {
|
||||
const state = { records: {} };
|
||||
mutations[types.SET_INBOX_ASSIGNABLE_AGENTS](state, {
|
||||
members: [...agentsData],
|
||||
inboxId: 1,
|
||||
});
|
||||
|
||||
expect(state.records).toEqual({ 1: agentsData });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -17,6 +17,7 @@ import { default as ja } from './locale/ja.json';
|
|||
import { default as ko } from './locale/ko.json';
|
||||
import { default as ml } from './locale/ml.json';
|
||||
import { default as nl } from './locale/nl.json';
|
||||
import { default as no } from './locale/no.json';
|
||||
import { default as pl } from './locale/pl.json';
|
||||
import { default as pt } from './locale/pt.json';
|
||||
import { default as pt_BR } from './locale/pt_BR.json';
|
||||
|
@ -28,7 +29,8 @@ import { default as ta } from './locale/ta.json';
|
|||
import { default as tr } from './locale/tr.json';
|
||||
import { default as uk } from './locale/uk.json';
|
||||
import { default as vi } from './locale/vi.json';
|
||||
import { default as zh } from './locale/zh.json';
|
||||
import { default as zh_CN } from './locale/zh_CN.json';
|
||||
import { default as zh_TW } from './locale/zh_TW.json';
|
||||
|
||||
export default {
|
||||
ar,
|
||||
|
@ -50,6 +52,7 @@ export default {
|
|||
ko,
|
||||
ml,
|
||||
nl,
|
||||
no,
|
||||
pl,
|
||||
pt,
|
||||
pt_BR,
|
||||
|
@ -61,5 +64,6 @@ export default {
|
|||
tr,
|
||||
uk,
|
||||
vi,
|
||||
zh,
|
||||
zh_CN,
|
||||
zh_TW,
|
||||
};
|
||||
|
|
|
@ -23,6 +23,10 @@ class InboxPolicy < ApplicationPolicy
|
|||
true
|
||||
end
|
||||
|
||||
def assignable_agents?
|
||||
true
|
||||
end
|
||||
|
||||
def create?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
json.payload do
|
||||
json.array! @assignable_agents do |agent|
|
||||
json.partial! 'api/v1/models/agent.json.jbuilder', resource: agent
|
||||
end
|
||||
end
|
|
@ -16,6 +16,7 @@ as defined by the routes in the `admin/` namespace
|
|||
users: 'ion ion-person-stalker',
|
||||
super_admins: 'ion ion-unlocked',
|
||||
access_tokens: 'ion-key',
|
||||
platform_apps: 'ion ion-social-buffer',
|
||||
installation_configs: 'ion ion-settings'
|
||||
}
|
||||
%>
|
||||
|
@ -32,7 +33,7 @@ as defined by the routes in the `admin/` namespace
|
|||
<%= link_to "Dashboard", super_admin_root_url %>
|
||||
</li>
|
||||
<% Administrate::Namespace.new(namespace).resources.each do |resource| %>
|
||||
<% next if ["account_users", "agent_bots","dashboard", "devise/sessions"].include? resource.resource %>
|
||||
<% next if ["account_users", "agent_bots", "dashboard", "devise/sessions"].include? resource.resource %>
|
||||
<li class="navigation__link navigation__link--<%= nav_link_state(resource) %>">
|
||||
<i class="<%= sidebar_icons[resource.resource.to_sym] %>"></i>
|
||||
<%= link_to(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
shared: &shared
|
||||
version: '1.15.0'
|
||||
version: '1.15.1'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
|
|
@ -29,8 +29,10 @@ LANGUAGES_CONFIG = {
|
|||
24 => { name: 'čeština (cs)', iso_639_3_code: 'ces', iso_639_1_code: 'cs', enabled: true },
|
||||
25 => { name: 'suomi, suomen kieli (fi)', iso_639_3_code: 'fin', iso_639_1_code: 'fi', enabled: true },
|
||||
26 => { name: 'Bahasa Indonesia (id)', iso_639_3_code: 'ind', iso_639_1_code: 'id', enabled: true },
|
||||
27 => { name: 'Svenska (sv)', iso_639_3_code: 'swe', iso_639_1_code: 'sv', enabled: true }
|
||||
|
||||
27 => { name: 'Svenska (sv)', iso_639_3_code: 'swe', iso_639_1_code: 'sv', enabled: true },
|
||||
28 => { name: 'magyar nyelv (hu)', iso_639_3_code: 'hun', iso_639_1_code: 'hu', enabled: true },
|
||||
29 => { name: 'norsk (no)', iso_639_3_code: 'nor', iso_639_1_code: 'no', enabled: true },
|
||||
30 => { name: '中文 (zh-CN)', iso_639_3_code: 'zho', iso_639_1_code: 'zh_CN', enabled: true }
|
||||
}.filter { |_key, val| val[:enabled] }.freeze
|
||||
|
||||
Rails.configuration.i18n.available_locales = LANGUAGES_CONFIG.map { |_index, lang| lang[:iso_639_1_code].to_sym }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#Additional translations at https://github.com/plataformatec/devise/wiki/I18n
|
||||
zh-CN:
|
||||
zh_CN:
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: "您的电子邮件地址已成功确认。"
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
#'true': 'foo'
|
||||
#To learn more, please read the Rails Internationalization guide
|
||||
#available at https://guides.rubyonrails.org/i18n.html.
|
||||
zh-CN:
|
||||
zh_CN:
|
||||
hello: "您好世界"
|
||||
messages:
|
||||
reset_password_success: 哇!密码重置请求成功。请检查您的邮件获取说明。
|
||||
|
|
|
@ -88,6 +88,7 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
resources :inboxes, only: [:index, :create, :update, :destroy] do
|
||||
get :assignable_agents, on: :member
|
||||
post :set_agent_bot, on: :member
|
||||
end
|
||||
resources :inbox_members, only: [:create, :show], param: :inbox_id
|
||||
|
@ -241,6 +242,7 @@ Rails.application.routes.draw do
|
|||
# resources that doesn't appear in primary navigation in super admin
|
||||
resources :account_users, only: [:new, :create, :destroy]
|
||||
resources :agent_bots, only: [:index, :new, :create, :show, :edit, :update]
|
||||
resources :platform_apps, only: [:index, :new, :create, :show, :edit, :update]
|
||||
end
|
||||
authenticated :super_admin do
|
||||
mount Sidekiq::Web => '/monitoring/sidekiq'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "1.15.0",
|
||||
"version": "1.15.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/javascript --fix",
|
||||
|
|
|
@ -3,11 +3,11 @@ require 'rails_helper'
|
|||
describe ::ContactBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:contact) { create(:contact, account: account, identifier: '123') }
|
||||
let(:existing_contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
|
||||
|
||||
describe '#perform' do
|
||||
it 'doesnot create contact if it already exist' do
|
||||
it 'doesnot create contact if it already exist with source id' do
|
||||
contact_inbox = described_class.new(
|
||||
source_id: existing_contact_inbox.source_id,
|
||||
inbox: inbox,
|
||||
|
@ -21,7 +21,7 @@ describe ::ContactBuilder do
|
|||
expect(contact_inbox.contact.id).to be(contact.id)
|
||||
end
|
||||
|
||||
it 'creates contact if contact doesnot exist' do
|
||||
it 'creates contact if contact doesnot exist with source id' do
|
||||
contact_inbox = described_class.new(
|
||||
source_id: '123456',
|
||||
inbox: inbox,
|
||||
|
@ -36,5 +36,48 @@ describe ::ContactBuilder do
|
|||
expect(contact_inbox.contact.name).to eq('Contact')
|
||||
expect(contact_inbox.inbox_id).to eq(inbox.id)
|
||||
end
|
||||
|
||||
it 'doesnot create contact if it already exist with identifier' do
|
||||
contact_inbox = described_class.new(
|
||||
source_id: '123456',
|
||||
inbox: inbox,
|
||||
contact_attributes: {
|
||||
name: 'Contact',
|
||||
identifier: contact.identifier,
|
||||
phone_number: contact.phone_number,
|
||||
email: 'testemail@example.com'
|
||||
}
|
||||
).perform
|
||||
|
||||
expect(contact_inbox.contact.id).to be(contact.id)
|
||||
end
|
||||
|
||||
it 'doesnot create contact if it already exist with email' do
|
||||
contact_inbox = described_class.new(
|
||||
source_id: '123456',
|
||||
inbox: inbox,
|
||||
contact_attributes: {
|
||||
name: 'Contact',
|
||||
phone_number: '+1234567890',
|
||||
email: contact.email
|
||||
}
|
||||
).perform
|
||||
|
||||
expect(contact_inbox.contact.id).to be(contact.id)
|
||||
end
|
||||
|
||||
it 'doesnot create contact if it already exist with phone number' do
|
||||
contact_inbox = described_class.new(
|
||||
source_id: '123456',
|
||||
inbox: inbox,
|
||||
contact_attributes: {
|
||||
name: 'Contact',
|
||||
phone_number: contact.phone_number,
|
||||
email: 'testemail@example.com'
|
||||
}
|
||||
).perform
|
||||
|
||||
expect(contact_inbox.contact.id).to be(contact.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -42,6 +42,38 @@ RSpec.describe 'Inboxes API', type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/assignable_agents' do
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignable_agents"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
before do
|
||||
create(:inbox_member, user: agent, inbox: inbox)
|
||||
end
|
||||
|
||||
it 'returns all assignable inbox members along with administrators' do
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignable_agents",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body, symbolize_names: true)[:payload]
|
||||
expect(response_data.size).to eq(2)
|
||||
expect(response_data.pluck(:role)).to include('agent', 'administrator')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/inboxes/:id' do
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Super Admin platform app API', type: :request do
|
||||
let(:super_admin) { create(:super_admin) }
|
||||
|
||||
describe 'GET /super_admin/platform_apps' do
|
||||
context 'when it is an unauthenticated super admin' do
|
||||
it 'returns unauthorized' do
|
||||
get '/super_admin/platform_apps'
|
||||
expect(response).to have_http_status(:redirect)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated super admin' do
|
||||
let!(:platform_app) { create(:platform_app) }
|
||||
|
||||
it 'shows the list of users' do
|
||||
sign_in super_admin
|
||||
get '/super_admin/platform_apps'
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(platform_app.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue