Merge branch 'develop' into feat/5913-search-improvements
This commit is contained in:
commit
ff8c0654c0
31 changed files with 429 additions and 55 deletions
|
@ -18,6 +18,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
head :ok
|
||||
end
|
||||
|
||||
def auto_offline
|
||||
@user.account_users.find_by!(account_id: auto_offline_params[:account_id]).update!(auto_offline: auto_offline_params[:auto_offline] || false)
|
||||
end
|
||||
|
||||
def availability
|
||||
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
||||
end
|
||||
|
@ -37,6 +41,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
params.require(:profile).permit(:account_id, :availability)
|
||||
end
|
||||
|
||||
def auto_offline_params
|
||||
params.require(:profile).permit(:account_id, :auto_offline)
|
||||
end
|
||||
|
||||
def profile_params
|
||||
params.require(:profile).permit(
|
||||
:email,
|
||||
|
|
|
@ -144,6 +144,12 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
updateAutoOffline(accountId, autoOffline = false) {
|
||||
return axios.post(endPoints('autoOffline').url, {
|
||||
profile: { account_id: accountId, auto_offline: autoOffline },
|
||||
});
|
||||
},
|
||||
|
||||
deleteAvatar() {
|
||||
return axios.delete(endPoints('deleteAvatar').url);
|
||||
},
|
||||
|
|
|
@ -16,6 +16,9 @@ const endPoints = {
|
|||
availabilityUpdate: {
|
||||
url: '/api/v1/profile/availability',
|
||||
},
|
||||
autoOffline: {
|
||||
url: '/api/v1/profile/auto_offline',
|
||||
},
|
||||
logout: {
|
||||
url: 'auth/sign_out',
|
||||
},
|
||||
|
|
|
@ -18,12 +18,35 @@
|
|||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-divider />
|
||||
<woot-dropdown-item class="auto-offline--toggle">
|
||||
<div class="info-wrap">
|
||||
<fluent-icon
|
||||
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
|
||||
icon="info"
|
||||
size="14"
|
||||
class="info-icon"
|
||||
/>
|
||||
|
||||
<span class="auto-offline--text">
|
||||
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<woot-switch
|
||||
size="small"
|
||||
class="auto-offline--switch"
|
||||
:value="currentUserAutoOffline"
|
||||
@input="updateAutoOffline"
|
||||
/>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-divider />
|
||||
</woot-dropdown-menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
|
||||
|
@ -41,7 +64,7 @@ export default {
|
|||
AvailabilityStatusBadge,
|
||||
},
|
||||
|
||||
mixins: [clickaway],
|
||||
mixins: [clickaway, alertMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
@ -54,6 +77,7 @@ export default {
|
|||
...mapGetters({
|
||||
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
currentUserAutoOffline: 'getCurrentUserAutoOffline',
|
||||
}),
|
||||
availabilityDisplayLabel() {
|
||||
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
||||
|
@ -85,21 +109,30 @@ export default {
|
|||
closeStatusMenu() {
|
||||
this.isStatusMenuOpened = false;
|
||||
},
|
||||
updateAutoOffline(autoOffline) {
|
||||
this.$store.dispatch('updateAutoOffline', {
|
||||
accountId: this.currentAccountId,
|
||||
autoOffline,
|
||||
});
|
||||
},
|
||||
changeAvailabilityStatus(availability) {
|
||||
const accountId = this.currentAccountId;
|
||||
if (this.isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdating = true;
|
||||
this.$store
|
||||
.dispatch('updateAvailability', {
|
||||
availability: availability,
|
||||
account_id: accountId,
|
||||
})
|
||||
.finally(() => {
|
||||
this.isUpdating = false;
|
||||
try {
|
||||
this.$store.dispatch('updateAvailability', {
|
||||
availability,
|
||||
account_id: this.currentAccountId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
|
||||
);
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -143,4 +176,32 @@ export default {
|
|||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.auto-offline--toggle {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-smaller) 0 var(--space-smaller) var(--space-small);
|
||||
margin: 0;
|
||||
|
||||
.info-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.auto-offline--switch {
|
||||
margin: -1px var(--space-micro) 0;
|
||||
}
|
||||
|
||||
.auto-offline--text {
|
||||
margin: 0 var(--space-smaller);
|
||||
font-size: var(--font-size-mini);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--s-700);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -135,7 +135,7 @@ export default {
|
|||
.dropdown-pane {
|
||||
left: var(--space-slab);
|
||||
bottom: var(--space-larger);
|
||||
min-width: 16.8rem;
|
||||
z-index: var(--z-index-much-higher);
|
||||
min-width: 22rem;
|
||||
z-index: var(--z-index-low);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -199,8 +199,8 @@ export default {
|
|||
|
||||
&.smooth {
|
||||
background: transparent;
|
||||
border: 1px solid var(--s-75);
|
||||
color: var(--s-800);
|
||||
border: 1px solid var(--s-100);
|
||||
color: var(--s-700);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
:class="{ active: value }"
|
||||
:class="{ active: value, small: size === 'small' }"
|
||||
role="switch"
|
||||
:aria-checked="value.toString()"
|
||||
@click="onClick"
|
||||
|
@ -15,6 +15,7 @@
|
|||
export default {
|
||||
props: {
|
||||
value: { type: Boolean, default: false },
|
||||
size: { type: String, default: '' },
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
|
@ -45,6 +46,20 @@ export default {
|
|||
background-color: var(--w-500);
|
||||
}
|
||||
|
||||
&.small {
|
||||
width: 22px;
|
||||
height: 14px;
|
||||
|
||||
span {
|
||||
height: var(--space-one);
|
||||
width: var(--space-one);
|
||||
|
||||
&.active {
|
||||
transform: translate(var(--space-small), var(--space-zero));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
--space-one-point-five: 1.5rem;
|
||||
background-color: var(--white);
|
||||
|
|
|
@ -91,6 +91,7 @@
|
|||
</span>
|
||||
<span class="unread">{{ unreadCount > 9 ? '9+' : unreadCount }}</span>
|
||||
</div>
|
||||
<card-labels :conversation-id="chat.id" />
|
||||
</div>
|
||||
<woot-context-menu
|
||||
v-if="showContextMenu"
|
||||
|
@ -125,8 +126,8 @@ import InboxName from '../InboxName';
|
|||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import ConversationContextMenu from './contextMenu/Index.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import timeAgo from 'dashboard/components/ui/TimeAgo';
|
||||
|
||||
import TimeAgo from 'dashboard/components/ui/TimeAgo';
|
||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||
const ATTACHMENT_ICONS = {
|
||||
image: 'image',
|
||||
audio: 'headphones-sound-wave',
|
||||
|
@ -138,10 +139,11 @@ const ATTACHMENT_ICONS = {
|
|||
|
||||
export default {
|
||||
components: {
|
||||
CardLabels,
|
||||
InboxName,
|
||||
Thumbnail,
|
||||
ConversationContextMenu,
|
||||
timeAgo,
|
||||
TimeAgo,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
|
@ -370,11 +372,15 @@ export default {
|
|||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.conversation {
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-light);
|
||||
}
|
||||
|
||||
&::v-deep .user-thumbnail-box {
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-selected {
|
||||
|
@ -383,8 +389,10 @@ export default {
|
|||
|
||||
.has-inbox-name {
|
||||
&::v-deep .user-thumbnail-box {
|
||||
margin-top: var(--space-normal);
|
||||
align-items: flex-start;
|
||||
margin-top: var(--space-large);
|
||||
}
|
||||
.checkbox-wrapper {
|
||||
margin-top: var(--space-large);
|
||||
}
|
||||
.conversation--meta {
|
||||
margin-top: var(--space-normal);
|
||||
|
@ -429,6 +437,7 @@ export default {
|
|||
margin-top: var(--space-minus-micro);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
|
@ -438,6 +447,7 @@ export default {
|
|||
border-radius: 100%;
|
||||
margin-top: var(--space-normal);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--w-100);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="activeLabels.length"
|
||||
ref="labelContainer"
|
||||
class="label-container"
|
||||
>
|
||||
<div class="labels-wrap" :class="{ expand: showAllLabels }">
|
||||
<woot-label
|
||||
v-for="(label, index) in activeLabels"
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:color="label.color"
|
||||
variant="smooth"
|
||||
small
|
||||
:class="{ hidden: !showAllLabels && index > labelPosition }"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showExpandLabelButton"
|
||||
:title="
|
||||
showAllLabels
|
||||
? $t('CONVERSATION.CARD.HIDE_LABELS')
|
||||
: $t('CONVERSATION.CARD.SHOW_LABELS')
|
||||
"
|
||||
class="show-more--button"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
:icon="showAllLabels ? 'chevron-left' : 'chevron-right'"
|
||||
size="tiny"
|
||||
@click="onShowLabels"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||
export default {
|
||||
mixins: [conversationLabelMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAllLabels: false,
|
||||
showExpandLabelButton: false,
|
||||
labelPosition: -1,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
activeLabels() {
|
||||
this.$nextTick(() => this.computeVisibleLabelPosition());
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.computeVisibleLabelPosition();
|
||||
},
|
||||
methods: {
|
||||
onShowLabels(e) {
|
||||
e.stopPropagation();
|
||||
this.showAllLabels = !this.showAllLabels;
|
||||
},
|
||||
computeVisibleLabelPosition() {
|
||||
const labelContainer = this.$refs.labelContainer;
|
||||
const labels = this.$refs.labelContainer.querySelectorAll('.label');
|
||||
let labelOffset = 0;
|
||||
Array.from(labels).forEach((label, index) => {
|
||||
labelOffset += label.offsetWidth + 8;
|
||||
|
||||
if (labelOffset < labelContainer.clientWidth - 16) {
|
||||
this.labelPosition = index;
|
||||
} else {
|
||||
this.showExpandLabelButton = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.show-more--button {
|
||||
height: var(--space-medium);
|
||||
position: sticky;
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--space-medium);
|
||||
|
||||
&.secondary:focus {
|
||||
color: var(--s-700);
|
||||
border-color: var(--s-300);
|
||||
}
|
||||
}
|
||||
|
||||
.label-container {
|
||||
margin-top: var(--space-micro);
|
||||
}
|
||||
|
||||
.labels-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
&.expand {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
flex-flow: row wrap;
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--space-smaller);
|
||||
}
|
||||
|
||||
.show-more--button {
|
||||
margin-bottom: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary {
|
||||
border: 1px solid var(--s-100);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
|
@ -41,6 +41,10 @@
|
|||
"NO_RESPONSE": "No response",
|
||||
"RATING_TITLE": "Rating",
|
||||
"FEEDBACK_TITLE": "Feedback",
|
||||
"CARD": {
|
||||
"SHOW_LABELS": "Show labels",
|
||||
"HIDE_LABELS": "Hide labels"
|
||||
},
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resolve",
|
||||
"REOPEN_ACTION": "Reopen",
|
||||
|
|
|
@ -99,7 +99,9 @@
|
|||
},
|
||||
"AVAILABILITY": {
|
||||
"LABEL": "Availability",
|
||||
"STATUSES_LIST": ["Online", "Busy", "Offline"]
|
||||
"STATUSES_LIST": ["Online", "Busy", "Offline"],
|
||||
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
||||
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Your email address",
|
||||
|
@ -222,6 +224,10 @@
|
|||
"CATEGORY": "Category",
|
||||
"CATEGORY_EMPTY_MESSAGE": "No categories found"
|
||||
},
|
||||
"SET_AUTO_OFFLINE": {
|
||||
"TEXT": "Mark offline automatically",
|
||||
"INFO_TEXT": "Let the system automatically mark you offline when you aren't using the app or dashboard."
|
||||
},
|
||||
"DOCS": "Read docs"
|
||||
},
|
||||
"BILLING_SETTINGS": {
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
:portals="portals"
|
||||
:active-portal-slug="selectedPortalSlug"
|
||||
:active-locale="selectedLocaleInPortal"
|
||||
@fetch-portal="fetchPortalAndItsCategories"
|
||||
@close-popover="closePortalPopover"
|
||||
/>
|
||||
<add-category
|
||||
|
@ -226,12 +227,6 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.params.portalSlug'() {
|
||||
this.fetchPortalsAndItsCategories();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
|
@ -240,7 +235,7 @@ export default {
|
|||
const slug = this.$route.params.portalSlug;
|
||||
if (slug) this.lastActivePortalSlug = slug;
|
||||
|
||||
this.fetchPortalsAndItsCategories();
|
||||
this.fetchPortalAndItsCategories();
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
|
@ -267,7 +262,7 @@ export default {
|
|||
toggleSidebar() {
|
||||
this.isSidebarOpen = !this.isSidebarOpen;
|
||||
},
|
||||
async fetchPortalsAndItsCategories() {
|
||||
async fetchPortalAndItsCategories() {
|
||||
await this.$store.dispatch('portals/index');
|
||||
const selectedPortalParam = {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
|
|
|
@ -248,7 +248,7 @@ export default {
|
|||
this.$emit('open-site', this.portal.slug);
|
||||
},
|
||||
openSettings() {
|
||||
this.fetchPortalsAndItsCategories();
|
||||
this.fetchPortalAndItsCategories();
|
||||
this.navigateToPortalEdit();
|
||||
},
|
||||
onClickOpenDeleteModal(portal) {
|
||||
|
@ -258,12 +258,18 @@ export default {
|
|||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
fetchPortalsAndItsCategories() {
|
||||
this.$store.dispatch('portals/index').then(() => {
|
||||
this.$store.dispatch('categories/index', {
|
||||
portalSlug: this.portal.slug,
|
||||
});
|
||||
});
|
||||
async fetchPortalAndItsCategories() {
|
||||
await this.$store.dispatch('portals/index');
|
||||
const {
|
||||
slug,
|
||||
config: { allowed_locales: allowedLocales },
|
||||
} = this.portal;
|
||||
const selectedPortalParam = {
|
||||
portalSlug: slug,
|
||||
locale: allowedLocales[0].code,
|
||||
};
|
||||
this.$store.dispatch('portals/show', selectedPortalParam);
|
||||
this.$store.dispatch('categories/index', selectedPortalParam);
|
||||
},
|
||||
async onClickDeletePortal() {
|
||||
const { slug } = this.selectedPortalForDelete;
|
||||
|
|
|
@ -36,7 +36,8 @@
|
|||
:active-portal-slug="activePortalSlug"
|
||||
:active-locale="activeLocale"
|
||||
:active="portal.slug === activePortalSlug"
|
||||
@open-portal-page="onPortalSelect"
|
||||
@open-portal-page="closePortalPopover"
|
||||
@fetch-portal="fetchPortalAndItsCategories"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -69,15 +70,15 @@ export default {
|
|||
closePortalPopover() {
|
||||
this.$emit('close-popover');
|
||||
},
|
||||
onPortalSelect() {
|
||||
this.$emit('close-popover');
|
||||
},
|
||||
openPortalPage() {
|
||||
this.closePortalPopover();
|
||||
this.$router.push({
|
||||
name: 'list_all_portals',
|
||||
});
|
||||
},
|
||||
fetchPortalAndItsCategories() {
|
||||
this.$emit('fetch-portal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.LABEL') }}
|
||||
</label>
|
||||
<div class="logo-container">
|
||||
<thumbnail :username="name" size="56" variant="square" />
|
||||
<thumbnail :username="name" size="56px" variant="square" />
|
||||
<woot-button
|
||||
v-if="false"
|
||||
class="upload-button"
|
||||
|
|
|
@ -121,6 +121,7 @@ export default {
|
|||
locale: code,
|
||||
},
|
||||
});
|
||||
this.$emit('fetch-portal');
|
||||
this.$emit('open-portal-page');
|
||||
},
|
||||
isLocaleActive(code, slug) {
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
</header>
|
||||
<div class="category-list">
|
||||
<category-list-item
|
||||
:categories="categoryByLocaleCode"
|
||||
:categories="categoriesByLocaleCode"
|
||||
@delete="deleteCategory"
|
||||
@edit="openEditCategoryModal"
|
||||
/>
|
||||
|
@ -91,7 +91,7 @@ export default {
|
|||
currentPortalSlug() {
|
||||
return this.$route.params.portalSlug;
|
||||
},
|
||||
categoryByLocaleCode() {
|
||||
categoriesByLocaleCode() {
|
||||
return this.$store.getters['categories/categoriesByLocaleCode'](
|
||||
this.currentLocaleCode
|
||||
);
|
||||
|
@ -131,6 +131,12 @@ export default {
|
|||
closeEditCategoryModal() {
|
||||
this.showEditCategoryModal = false;
|
||||
},
|
||||
async fetchCategoriesByPortalSlugAndLocale(localeCode) {
|
||||
await this.$store.dispatch('categories/index', {
|
||||
portalSlug: this.currentPortalSlug,
|
||||
locale: localeCode,
|
||||
});
|
||||
},
|
||||
async deleteCategory(categoryId) {
|
||||
try {
|
||||
await this.$store.dispatch('categories/delete', {
|
||||
|
@ -152,6 +158,7 @@ export default {
|
|||
changeCurrentCategory(event) {
|
||||
const localeCode = event.target.value;
|
||||
this.currentLocaleCode = localeCode;
|
||||
this.fetchCategoriesByPortalSlugAndLocale(localeCode);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -48,6 +48,14 @@ export const getters = {
|
|||
return currentAccount.availability;
|
||||
},
|
||||
|
||||
getCurrentUserAutoOffline($state, $getters) {
|
||||
const { accounts = [] } = $state.currentUser;
|
||||
const [currentAccount = {}] = accounts.filter(
|
||||
account => account.id === $getters.getCurrentAccountId
|
||||
);
|
||||
return currentAccount.auto_offline;
|
||||
},
|
||||
|
||||
getCurrentAccountId(_, __, rootState) {
|
||||
if (rootState.route.params && rootState.route.params.accountId) {
|
||||
return Number(rootState.route.params.accountId);
|
||||
|
@ -174,6 +182,15 @@ export const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
updateAutoOffline: async ({ commit }, { accountId, autoOffline }) => {
|
||||
try {
|
||||
const response = await authAPI.updateAutoOffline(accountId, autoOffline);
|
||||
commit(types.SET_CURRENT_USER, response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentUserAvailability({ commit, state: $state }, data) {
|
||||
if (data[$state.currentUser.id]) {
|
||||
commit(types.SET_CURRENT_USER_AVAILABILITY, data[$state.currentUser.id]);
|
||||
|
|
|
@ -41,11 +41,11 @@ export const actions = {
|
|||
portalSlug,
|
||||
articleObj,
|
||||
});
|
||||
const { id: articleId, portal } = payload;
|
||||
const { id: articleId } = payload;
|
||||
commit(types.ADD_ARTICLE, payload);
|
||||
commit(types.ADD_ARTICLE_ID, articleId);
|
||||
commit(types.ADD_ARTICLE_FLAG, articleId);
|
||||
dispatch('portals/updatePortal', portal, { root: true });
|
||||
dispatch('portals/updatePortal', portalSlug, { root: true });
|
||||
return articleId;
|
||||
} catch (error) {
|
||||
return throwErrorMessage(error);
|
||||
|
|
|
@ -88,6 +88,38 @@ describe('#actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#updateAutoOffline', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({
|
||||
data: {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
accounts: [
|
||||
{
|
||||
account_id: 1,
|
||||
auto_offline: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: { expiry: 581842904 },
|
||||
});
|
||||
await actions.updateAutoOffline(
|
||||
{ commit, dispatch },
|
||||
{ autoOffline: false, accountId: 1 }
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
types.default.SET_CURRENT_USER,
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
accounts: [{ account_id: 1, auto_offline: false }],
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateUISettings', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.put.mockResolvedValue({
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<li class="dropdown-menu--header" :tabindex="null" :aria-disabled="true">
|
||||
<span class="title">{{ title }}</span>
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
|
|
1
app/views/api/v1/profiles/auto_offline.jbuilder
Normal file
1
app/views/api/v1/profiles/auto_offline.jbuilder
Normal file
|
@ -0,0 +1 @@
|
|||
json.partial! 'api/v1/models/user', formats: [:json], resource: @user
|
|
@ -77,4 +77,3 @@ fullcontact:
|
|||
settings_form_schema:
|
||||
[{ 'label': 'API Key', 'type': 'text', 'name': 'api_key',"validation": "required", }]
|
||||
visible_properties: ['api_key']
|
||||
|
||||
|
|
|
@ -183,6 +183,7 @@ Rails.application.routes.draw do
|
|||
delete :avatar, on: :collection
|
||||
member do
|
||||
post :availability
|
||||
post :auto_offline
|
||||
put :set_active_account
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,13 +22,23 @@ class Integrations::Slack::IncomingMessageBuilder
|
|||
private
|
||||
|
||||
def valid_event?
|
||||
supported_event_type? && supported_event?
|
||||
supported_event_type? && supported_event? && should_process_event?
|
||||
end
|
||||
|
||||
def supported_event_type?
|
||||
SUPPORTED_EVENT_TYPES.include?(params[:type])
|
||||
end
|
||||
|
||||
# Discard all the subtype of a message event
|
||||
# We are only considering the actual message sent by a Slack user
|
||||
# Any reactions or messages sent by the bot will be ignored.
|
||||
# https://api.slack.com/events/message#subtypes
|
||||
def should_process_event?
|
||||
return true if params[:type] != 'event_callback'
|
||||
|
||||
params[:event][:user].present? && params[:event][:subtype].blank?
|
||||
end
|
||||
|
||||
def supported_event?
|
||||
hook_verification? || SUPPORTED_EVENTS.include?(params[:event][:type])
|
||||
end
|
||||
|
|
|
@ -36,12 +36,13 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
|||
if conversation.identifier.present?
|
||||
"#{private_indicator}#{message.content}"
|
||||
else
|
||||
"*Inbox: #{message.inbox.name} [#{message.inbox.inbox_type}]* \n\n #{message.content}"
|
||||
"\n*Inbox:* #{message.inbox.name} (#{message.inbox.inbox_type})\n\n#{message.content}"
|
||||
end
|
||||
end
|
||||
|
||||
def avatar_url(sender)
|
||||
sender.try(:avatar_url) || "#{ENV.fetch('FRONTEND_URL', nil)}/admin/avatar_square.png"
|
||||
sender_type = sender.instance_of?(Contact) ? 'contact' : 'user'
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/integrations/slack/#{sender_type}.png"
|
||||
end
|
||||
|
||||
def send_message
|
||||
|
@ -86,7 +87,7 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
|||
end
|
||||
|
||||
def sender_name(sender)
|
||||
sender.try(:name) ? "#{sender_type(sender)}: #{sender.try(:name)}" : sender_type(sender)
|
||||
sender.try(:name) ? "#{sender.try(:name)} (#{sender_type(sender)})" : sender_type(sender)
|
||||
end
|
||||
|
||||
def sender_type(sender)
|
||||
|
|
BIN
public/integrations/slack/contact.png
Normal file
BIN
public/integrations/slack/contact.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
public/integrations/slack/user.png
Normal file
BIN
public/integrations/slack/user.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
|
@ -196,6 +196,30 @@ RSpec.describe 'Profile API', type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/profile/auto_offline' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/profile/auto_offline'
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) }
|
||||
|
||||
it 'updates the auto offline status' do
|
||||
post '/api/v1/profile/auto_offline',
|
||||
params: { profile: { auto_offline: false, account_id: account.id } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['accounts'].first['auto_offline']).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/profile/set_active_account' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
|
|
|
@ -12,6 +12,24 @@ describe Integrations::Slack::IncomingMessageBuilder do
|
|||
event_time: 1_588_623_033
|
||||
}
|
||||
end
|
||||
let(:sub_type_message) do
|
||||
{
|
||||
team_id: 'TLST3048H',
|
||||
api_app_id: 'A012S5UETV4',
|
||||
event: message_event.merge({ type: 'message', subtype: 'bot_message' }),
|
||||
type: 'event_callback',
|
||||
event_time: 1_588_623_033
|
||||
}
|
||||
end
|
||||
let(:message_without_user) do
|
||||
{
|
||||
team_id: 'TLST3048H',
|
||||
api_app_id: 'A012S5UETV4',
|
||||
event: message_event.merge({ type: 'message', user: nil }),
|
||||
type: 'event_callback',
|
||||
event_time: 1_588_623_033
|
||||
}
|
||||
end
|
||||
let(:message_with_attachments) { slack_attachment_stub }
|
||||
let(:message_without_thread_ts) { slack_message_stub_without_thread_ts }
|
||||
let(:verification_params) { slack_url_verification_stub }
|
||||
|
@ -81,6 +99,20 @@ describe Integrations::Slack::IncomingMessageBuilder do
|
|||
expect(conversation.messages.count).to eql(messages_count)
|
||||
end
|
||||
|
||||
it 'does not create message for message sub type events' do
|
||||
messages_count = conversation.messages.count
|
||||
builder = described_class.new(sub_type_message)
|
||||
builder.perform
|
||||
expect(conversation.messages.count).to eql(messages_count)
|
||||
end
|
||||
|
||||
it 'does not create message if user is missing' do
|
||||
messages_count = conversation.messages.count
|
||||
builder = described_class.new(message_without_user)
|
||||
builder.perform
|
||||
expect(conversation.messages.count).to eql(messages_count)
|
||||
end
|
||||
|
||||
it 'does not create message for invalid event type and event files is not present' do
|
||||
messages_count = conversation.messages.count
|
||||
message_with_attachments[:event][:files] = nil
|
||||
|
|
|
@ -28,8 +28,8 @@ describe Integrations::Slack::SendOnSlackService do
|
|||
|
||||
expect(slack_client).to receive(:chat_postMessage).with(
|
||||
channel: hook.reference_id,
|
||||
text: "*Inbox: #{inbox.name} [#{inbox.inbox_type}]* \n\n #{message.content}",
|
||||
username: "Contact: #{message.sender.name}",
|
||||
text: "\n*Inbox:* #{inbox.name} (#{inbox.inbox_type})\n\n#{message.content}",
|
||||
username: "#{message.sender.name} (Contact)",
|
||||
thread_ts: nil,
|
||||
icon_url: anything
|
||||
).and_return(slack_message)
|
||||
|
@ -49,7 +49,7 @@ describe Integrations::Slack::SendOnSlackService do
|
|||
expect(slack_client).to receive(:chat_postMessage).with(
|
||||
channel: hook.reference_id,
|
||||
text: message.content,
|
||||
username: "Contact: #{message.sender.name}",
|
||||
username: "#{message.sender.name} (Contact)",
|
||||
thread_ts: conversation.identifier,
|
||||
icon_url: anything
|
||||
).and_return(slack_message)
|
||||
|
@ -63,7 +63,7 @@ describe Integrations::Slack::SendOnSlackService do
|
|||
expect(slack_client).to receive(:chat_postMessage).with(
|
||||
channel: hook.reference_id,
|
||||
text: message.content,
|
||||
username: "Contact: #{message.sender.name}",
|
||||
username: "#{message.sender.name} (Contact)",
|
||||
thread_ts: conversation.identifier,
|
||||
icon_url: anything
|
||||
).and_return(slack_message)
|
||||
|
@ -93,7 +93,7 @@ describe Integrations::Slack::SendOnSlackService do
|
|||
expect(slack_client).to receive(:chat_postMessage).with(
|
||||
channel: hook.reference_id,
|
||||
text: message.content,
|
||||
username: "Contact: #{message.sender.name}",
|
||||
username: "#{message.sender.name} (Contact)",
|
||||
thread_ts: conversation.identifier,
|
||||
icon_url: anything
|
||||
).and_raise(Slack::Web::Api::Errors::AccountInactive.new('Account disconnected'))
|
||||
|
|
Loading…
Reference in a new issue