Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bfacd5381 | ||
|
|
50344a282b | ||
|
|
65cd0717e6 | ||
|
|
73dcf539ed | ||
|
|
35d9dc925f | ||
|
|
2d14f27d5b | ||
|
|
70a65e2c34 |
@@ -94,11 +94,10 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
|
|||||||
def build_message
|
def build_message
|
||||||
# Duplicate webhook events may be sent for the same message
|
# Duplicate webhook events may be sent for the same message
|
||||||
# when a user is connected to the Instagram account through both Messenger and Instagram login.
|
# when a user is connected to the Instagram account through both Messenger and Instagram login.
|
||||||
|
# There is chance for echo events to be sent for the same message.
|
||||||
# Therefore, we need to check if the message already exists before creating it.
|
# Therefore, we need to check if the message already exists before creating it.
|
||||||
return if message_already_exists?
|
return if message_already_exists?
|
||||||
|
|
||||||
return if @outgoing_echo
|
|
||||||
|
|
||||||
return if message_content.blank? && all_unsupported_files?
|
return if message_content.blank? && all_unsupported_files?
|
||||||
|
|
||||||
@message = conversation.messages.create!(message_params)
|
@message = conversation.messages.create!(message_params)
|
||||||
|
|||||||
@@ -29,6 +29,19 @@
|
|||||||
--iris-11: 87 83 198;
|
--iris-11: 87 83 198;
|
||||||
--iris-12: 39 41 98;
|
--iris-12: 39 41 98;
|
||||||
|
|
||||||
|
--blue-1: 251 253 255;
|
||||||
|
--blue-2: 245 249 255;
|
||||||
|
--blue-3: 233 243 255;
|
||||||
|
--blue-4: 218 236 255;
|
||||||
|
--blue-5: 201 226 255;
|
||||||
|
--blue-6: 181 213 255;
|
||||||
|
--blue-7: 155 195 252;
|
||||||
|
--blue-8: 117 171 247;
|
||||||
|
--blue-9: 39 129 246;
|
||||||
|
--blue-10: 16 115 233;
|
||||||
|
--blue-11: 8 109 224;
|
||||||
|
--blue-12: 11 50 101;
|
||||||
|
|
||||||
--ruby-1: 255 252 253;
|
--ruby-1: 255 252 253;
|
||||||
--ruby-2: 255 247 248;
|
--ruby-2: 255 247 248;
|
||||||
--ruby-3: 254 234 237;
|
--ruby-3: 254 234 237;
|
||||||
@@ -131,6 +144,19 @@
|
|||||||
--iris-11: 158 177 255;
|
--iris-11: 158 177 255;
|
||||||
--iris-12: 224 223 254;
|
--iris-12: 224 223 254;
|
||||||
|
|
||||||
|
--blue-1: 10 17 28;
|
||||||
|
--blue-2: 15 24 38;
|
||||||
|
--blue-3: 15 39 72;
|
||||||
|
--blue-4: 10 49 99;
|
||||||
|
--blue-5: 18 61 117;
|
||||||
|
--blue-6: 29 84 134;
|
||||||
|
--blue-7: 40 89 156;
|
||||||
|
--blue-8: 48 106 186;
|
||||||
|
--blue-9: 39 129 246;
|
||||||
|
--blue-10: 21 116 231;
|
||||||
|
--blue-11: 126 182 255;
|
||||||
|
--blue-12: 205 227 255;
|
||||||
|
|
||||||
--ruby-1: 25 17 19;
|
--ruby-1: 25 17 19;
|
||||||
--ruby-2: 30 21 23;
|
--ruby-2: 30 21 23;
|
||||||
--ruby-3: 58 20 30;
|
--ruby-3: 58 20 30;
|
||||||
|
|||||||
+1
@@ -51,6 +51,7 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
|
|||||||
<Button
|
<Button
|
||||||
:label="t('DIALOG.BUTTONS.CANCEL')"
|
:label="t('DIALOG.BUTTONS.CANCEL')"
|
||||||
variant="link"
|
variant="link"
|
||||||
|
type="reset"
|
||||||
class="h-10 hover:!no-underline hover:text-n-brand"
|
class="h-10 hover:!no-underline hover:text-n-brand"
|
||||||
@click="closeDialog"
|
@click="closeDialog"
|
||||||
/>
|
/>
|
||||||
|
|||||||
-4
@@ -31,10 +31,6 @@ const sortMenus = [
|
|||||||
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.EMAIL'),
|
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.EMAIL'),
|
||||||
value: 'email',
|
value: 'email',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.PHONE_NUMBER'),
|
|
||||||
value: 'phone_number',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.COMPANY'),
|
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.COMPANY'),
|
||||||
value: 'company_name',
|
value: 'company_name',
|
||||||
|
|||||||
@@ -218,14 +218,14 @@ const emitDateRange = () => {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showDatePicker"
|
v-if="showDatePicker"
|
||||||
class="flex absolute top-9 ltr:left-0 rtl:right-0 z-30 shadow-md select-none w-[880px] h-[490px] rounded-2xl border border-slate-50 dark:border-slate-800 bg-white dark:bg-slate-800"
|
class="flex absolute top-9 ltr:left-0 rtl:right-0 z-30 shadow-md select-none w-[880px] h-[490px] rounded-2xl bg-n-alpha-3 backdrop-blur-[100px] border-0 outline outline-1 outline-n-container"
|
||||||
>
|
>
|
||||||
<CalendarDateRange
|
<CalendarDateRange
|
||||||
:selected-range="selectedRange"
|
:selected-range="selectedRange"
|
||||||
@set-range="setDateRange"
|
@set-range="setDateRange"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-slate-50 dark:border-slate-700/50"
|
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-n-strong"
|
||||||
>
|
>
|
||||||
<div class="flex justify-around h-fit">
|
<div class="flex justify-around h-fit">
|
||||||
<!-- Calendars for Start and End Dates -->
|
<!-- Calendars for Start and End Dates -->
|
||||||
@@ -251,12 +251,12 @@ const emitDateRange = () => {
|
|||||||
@validate="updateManualInput($event, calendar)"
|
@validate="updateManualInput($event, calendar)"
|
||||||
@error="handleManualInputError($event)"
|
@error="handleManualInputError($event)"
|
||||||
/>
|
/>
|
||||||
<div class="py-5 border-b border-slate-50 dark:border-slate-700/50">
|
<div class="py-5 border-b border-n-strong">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-center gap-2 px-5 min-w-[340px] max-h-[352px]"
|
class="flex flex-col items-center gap-2 px-5 min-w-[340px] max-h-[352px]"
|
||||||
:class="
|
:class="
|
||||||
calendar === START_CALENDAR &&
|
calendar === START_CALENDAR &&
|
||||||
'ltr:border-r rtl:border-l border-slate-50 dark:border-slate-700/50'
|
'ltr:border-r rtl:border-l border-n-strong'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<CalendarYear
|
<CalendarYear
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { CALENDAR_PERIODS } from '../helpers/DatePickerHelper';
|
import { CALENDAR_PERIODS } from '../helpers/DatePickerHelper';
|
||||||
|
|
||||||
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
calendarType: {
|
calendarType: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -38,42 +40,38 @@ const onClickSetView = (type, mode) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-start justify-between w-full h-9">
|
<div class="flex items-start justify-between w-full h-9">
|
||||||
<button
|
<NextButton
|
||||||
class="p-1 rounded-lg hover:bg-slate-75 dark:hover:bg-slate-700/50 rtl:rotate-180"
|
slate
|
||||||
|
ghost
|
||||||
|
xs
|
||||||
|
icon="i-lucide-chevron-left"
|
||||||
|
class="rtl:rotate-180"
|
||||||
@click="onClickPrev(calendarType)"
|
@click="onClickPrev(calendarType)"
|
||||||
>
|
/>
|
||||||
<fluent-icon
|
|
||||||
icon="chevron-left"
|
|
||||||
size="14"
|
|
||||||
class="text-slate-900 dark:text-slate-50"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
v-if="firstButtonLabel"
|
v-if="firstButtonLabel"
|
||||||
class="p-0 text-sm font-medium text-center text-slate-800 dark:text-slate-50 hover:text-woot-600 dark:hover:text-woot-600"
|
class="p-0 text-sm font-medium text-center text-n-slate-12 hover:text-n-brand"
|
||||||
@click="onClickSetView(calendarType, viewMode)"
|
@click="onClickSetView(calendarType, viewMode)"
|
||||||
>
|
>
|
||||||
{{ firstButtonLabel }}
|
{{ firstButtonLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="buttonLabel"
|
v-if="buttonLabel"
|
||||||
class="p-0 text-sm font-medium text-center text-slate-800 dark:text-slate-50"
|
class="p-0 text-sm font-medium text-center text-n-slate-12"
|
||||||
:class="{ 'hover:text-woot-600 dark:hover:text-woot-600': viewMode }"
|
:class="{ 'hover:text-n-brand': viewMode }"
|
||||||
@click="onClickSetView(calendarType, YEAR)"
|
@click="onClickSetView(calendarType, YEAR)"
|
||||||
>
|
>
|
||||||
{{ buttonLabel }}
|
{{ buttonLabel }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<NextButton
|
||||||
class="p-1 rounded-lg hover:bg-slate-75 dark:hover:bg-slate-700/50 rtl:rotate-180"
|
slate
|
||||||
|
ghost
|
||||||
|
xs
|
||||||
|
icon="i-lucide-chevron-right"
|
||||||
|
class="rtl:rotate-180"
|
||||||
@click="onClickNext(calendarType)"
|
@click="onClickNext(calendarType)"
|
||||||
>
|
/>
|
||||||
<fluent-icon
|
|
||||||
icon="chevron-right"
|
|
||||||
size="14"
|
|
||||||
class="text-slate-900 dark:text-slate-50"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const validateDate = () => {
|
|||||||
<input
|
<input
|
||||||
v-model="localDateValue"
|
v-model="localDateValue"
|
||||||
type="text"
|
type="text"
|
||||||
class="reset-base border bg-slate-25 dark:bg-slate-900 ring-offset-ash-900 border-slate-50 dark:border-slate-700/50 w-full disabled:text-slate-200 dark:disabled:text-slate-700 disabled:cursor-not-allowed text-slate-800 dark:text-slate-50 px-1.5 py-1 text-sm rounded-xl h-10"
|
class="!text-sm !mb-0 disabled:!outline-n-strong"
|
||||||
:placeholder="dateFormat"
|
:placeholder="dateFormat"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
@keypress.enter="validateDate"
|
@keypress.enter="validateDate"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const setDateRange = range => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-[200px] flex flex-col items-start">
|
<div class="w-[200px] flex flex-col items-start">
|
||||||
<h4
|
<h4
|
||||||
class="w-full px-5 py-4 text-sm font-medium capitalize text-start text-slate-600 dark:text-slate-200"
|
class="w-full px-5 py-4 text-sm font-medium capitalize text-start text-n-slate-12"
|
||||||
>
|
>
|
||||||
{{ $t('DATE_PICKER.DATE_RANGE_OPTIONS.TITLE') }}
|
{{ $t('DATE_PICKER.DATE_RANGE_OPTIONS.TITLE') }}
|
||||||
</h4>
|
</h4>
|
||||||
@@ -26,11 +26,11 @@ const setDateRange = range => {
|
|||||||
<button
|
<button
|
||||||
v-for="range in dateRanges"
|
v-for="range in dateRanges"
|
||||||
:key="range.label"
|
:key="range.label"
|
||||||
class="w-full px-5 py-3 text-sm font-medium truncate border-none rounded-none text-start hover:bg-slate-50 dark:hover:bg-slate-700"
|
class="w-full px-5 py-3 text-sm font-medium truncate border-none rounded-none text-start hover:bg-n-alpha-2 dark:hover:bg-n-solid-3"
|
||||||
:class="
|
:class="
|
||||||
range.value === selectedRange
|
range.value === selectedRange
|
||||||
? 'text-slate-800 dark:text-slate-50 bg-slate-50 dark:bg-slate-700'
|
? 'text-n-slate-12 bg-n-alpha-1 dark:bg-n-solid-active'
|
||||||
: 'text-slate-600 dark:text-slate-200'
|
: 'text-n-slate-12'
|
||||||
"
|
"
|
||||||
@click="setDateRange(range)"
|
@click="setDateRange(range)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
const emit = defineEmits(['clear', 'change']);
|
const emit = defineEmits(['clear', 'change']);
|
||||||
|
|
||||||
const onClickClear = () => {
|
const onClickClear = () => {
|
||||||
@@ -11,18 +13,19 @@ const onClickApply = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[56px] flex justify-between px-5 py-3 items-center">
|
<div class="h-[56px] flex justify-between gap-2 px-2 py-3 items-center">
|
||||||
<button
|
<NextButton
|
||||||
class="p-1.5 rounded-lg w-fit text-sm font-medium text-slate-600 dark:text-slate-200 hover:text-slate-800 dark:hover:text-slate-100"
|
slate
|
||||||
|
ghost
|
||||||
|
sm
|
||||||
|
:label="$t('DATE_PICKER.CLEAR_BUTTON')"
|
||||||
@click="onClickClear"
|
@click="onClickClear"
|
||||||
>
|
/>
|
||||||
{{ $t('DATE_PICKER.CLEAR_BUTTON') }}
|
<NextButton
|
||||||
</button>
|
sm
|
||||||
<button
|
ghost
|
||||||
class="p-1.5 rounded-lg w-fit text-sm font-medium text-woot-500 dark:text-woot-300 hover:text-woot-700 dark:hover:text-woot-500"
|
:label="$t('DATE_PICKER.APPLY_BUTTON')"
|
||||||
@click="onClickApply"
|
@click="onClickApply"
|
||||||
>
|
/>
|
||||||
{{ $t('DATE_PICKER.APPLY_BUTTON') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -71,10 +71,12 @@ const selectMonth = index => {
|
|||||||
<button
|
<button
|
||||||
v-for="(month, index) in months"
|
v-for="(month, index) in months"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="p-2 text-sm font-medium text-center text-slate-800 dark:text-slate-50 w-[92px] h-10 rounded-lg py-2.5 px-2 hover:bg-slate-75 dark:hover:bg-slate-700"
|
class="p-2 text-sm font-medium text-center text-n-slate-12 w-[92px] h-10 rounded-lg py-2.5 px-2"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white hover:bg-woot-500 dark:bg-woot-700':
|
'bg-n-brand text-white hover:bg-n-blue-10':
|
||||||
index === activeMonthIndex,
|
index === activeMonthIndex,
|
||||||
|
'hover:bg-n-alpha-2 dark:hover:bg-n-solid-3':
|
||||||
|
index !== activeMonthIndex,
|
||||||
}"
|
}"
|
||||||
@click="selectMonth(index)"
|
@click="selectMonth(index)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -107,17 +107,16 @@ const isNextDayInRange = day => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dayClasses = day => ({
|
const dayClasses = day => ({
|
||||||
'text-slate-500 dark:text-slate-400 pointer-events-none':
|
'text-n-slate-10 pointer-events-none': !isInCurrentMonth(day),
|
||||||
!isInCurrentMonth(day),
|
'text-n-slate-12 hover:text-n-slate-12 hover:bg-n-blue-6 dark:hover:bg-n-blue-7':
|
||||||
'text-slate-800 dark:text-slate-50 hover:text-slate-800 dark:hover:text-white hover:bg-woot-100 dark:hover:bg-woot-700':
|
|
||||||
isInCurrentMonth(day),
|
isInCurrentMonth(day),
|
||||||
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white':
|
'bg-n-brand text-white':
|
||||||
isSelectedStartOrEndDate(day) && isInCurrentMonth(day),
|
isSelectedStartOrEndDate(day) && isInCurrentMonth(day),
|
||||||
'bg-woot-50 dark:bg-woot-800':
|
'bg-n-blue-4 dark:bg-n-blue-5':
|
||||||
(isInRange(day) || isHoveringInRange(day)) &&
|
(isInRange(day) || isHoveringInRange(day)) &&
|
||||||
!isSelectedStartOrEndDate(day) &&
|
!isSelectedStartOrEndDate(day) &&
|
||||||
isInCurrentMonth(day),
|
isInCurrentMonth(day),
|
||||||
'outline outline-1 outline-woot-200 -outline-offset-1 dark:outline-woot-700 text-woot-600 dark:text-woot-400':
|
'outline outline-1 outline-n-blue-8 -outline-offset-1 !text-n-blue-text':
|
||||||
isToday(props.currentDate, day) && !isSelectedStartOrEndDate(day),
|
isToday(props.currentDate, day) && !isSelectedStartOrEndDate(day),
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -164,7 +163,7 @@ const dayClasses = day => ({
|
|||||||
!isLastDayOfMonth(day) &&
|
!isLastDayOfMonth(day) &&
|
||||||
isInCurrentMonth(day)
|
isInCurrentMonth(day)
|
||||||
"
|
"
|
||||||
class="absolute bottom-0 w-6 h-8 ltr:-right-4 rtl:-left-4 bg-woot-50 dark:bg-woot-800 -z-10"
|
class="absolute bottom-0 w-6 h-8 ltr:-right-4 rtl:-left-4 bg-n-blue-4 dark:bg-n-blue-5 -z-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ const selectYear = year => {
|
|||||||
<button
|
<button
|
||||||
v-for="year in years"
|
v-for="year in years"
|
||||||
:key="year"
|
:key="year"
|
||||||
class="p-2 text-sm font-medium text-center text-slate-800 dark:text-slate-50 w-[144px] h-10 rounded-lg py-2.5 px-2 hover:bg-slate-75 dark:hover:bg-slate-700"
|
class="p-2 text-sm font-medium text-center text-n-slate-12 w-[144px] h-10 rounded-lg py-2.5 px-2"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white hover:bg-woot-500 dark:hover:bg-woot-700':
|
'bg-n-brand text-white hover:bg-n-blue-10': year === activeYear,
|
||||||
year === activeYear,
|
'hover:bg-n-alpha-2 dark:hover:bg-n-solid-3': year !== activeYear,
|
||||||
}"
|
}"
|
||||||
@click="selectYear(year)"
|
@click="selectYear(year)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const openDatePicker = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="inline-flex relative items-center rounded-lg gap-2 py-1.5 px-3 h-8 bg-slate-50 dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"
|
class="inline-flex relative items-center rounded-lg gap-2 py-1.5 px-3 h-8 bg-n-alpha-2 hover:bg-n-alpha-1 active:bg-n-alpha-1"
|
||||||
@click="openDatePicker"
|
@click="openDatePicker"
|
||||||
>
|
>
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ const plugins = computed(() => {
|
|||||||
trigger: '@',
|
trigger: '@',
|
||||||
showMenu: showUserMentions,
|
showMenu: showUserMentions,
|
||||||
searchTerm: mentionSearchKey,
|
searchTerm: mentionSearchKey,
|
||||||
|
isAllowed: () => props.isPrivate,
|
||||||
}),
|
}),
|
||||||
createSuggestionPlugin({
|
createSuggestionPlugin({
|
||||||
trigger: '/',
|
trigger: '/',
|
||||||
|
|||||||
@@ -69,10 +69,6 @@ const onAgentSelect = index => {
|
|||||||
v-if="items.length"
|
v-if="items.length"
|
||||||
ref="tagAgentsRef"
|
ref="tagAgentsRef"
|
||||||
class="vertical dropdown menu mention--box bg-n-solid-1 p-1 rounded-xl text-sm overflow-auto absolute w-full z-20 shadow-md left-0 leading-[1.2] bottom-full max-h-[12.5rem] border border-solid border-n-strong"
|
class="vertical dropdown menu mention--box bg-n-solid-1 p-1 rounded-xl text-sm overflow-auto absolute w-full z-20 shadow-md left-0 leading-[1.2] bottom-full max-h-[12.5rem] border border-solid border-n-strong"
|
||||||
:class="{
|
|
||||||
'border-b-[0.5rem] border-solid border-white dark:!border-slate-700':
|
|
||||||
items.length <= 4,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
v-for="(agent, index) in items"
|
v-for="(agent, index) in items"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const useConversationSidebarItemsOrder = uiSettings => {
|
|||||||
const { conversation_sidebar_items_order: itemsOrder } = uiSettings.value;
|
const { conversation_sidebar_items_order: itemsOrder } = uiSettings.value;
|
||||||
// If the sidebar order is not set, use the default order.
|
// If the sidebar order is not set, use the default order.
|
||||||
if (!itemsOrder) {
|
if (!itemsOrder) {
|
||||||
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
|
return [...DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER];
|
||||||
}
|
}
|
||||||
// Create a copy of itemsOrder to avoid mutating the original store object.
|
// Create a copy of itemsOrder to avoid mutating the original store object.
|
||||||
const itemsOrderCopy = [...itemsOrder];
|
const itemsOrderCopy = [...itemsOrder];
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ export const getUserPermissions = (user, accountId) => {
|
|||||||
return currentAccount.permissions || [];
|
return currentAccount.permissions || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserRole = (user, accountId) => {
|
||||||
|
const currentAccount = getCurrentAccount(user, accountId) || {};
|
||||||
|
if (currentAccount.custom_role_id) {
|
||||||
|
return 'custom_role';
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentAccount.role || 'agent';
|
||||||
|
};
|
||||||
|
|
||||||
const isPermissionsPresentInRoute = route =>
|
const isPermissionsPresentInRoute = route =>
|
||||||
route.meta && route.meta.permissions;
|
route.meta && route.meta.permissions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||||
import { applyPageFilters, sortComparator } from './helpers';
|
import { applyPageFilters, applyRoleFilter, sortComparator } from './helpers';
|
||||||
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
|
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
|
||||||
import { matchesFilters } from './helpers/filterHelpers';
|
import { matchesFilters } from './helpers/filterHelpers';
|
||||||
|
import {
|
||||||
|
getUserPermissions,
|
||||||
|
getUserRole,
|
||||||
|
} from '../../../helper/permissionsHelper';
|
||||||
import camelcaseKeys from 'camelcase-keys';
|
import camelcaseKeys from 'camelcase-keys';
|
||||||
|
|
||||||
export const getSelectedChatConversation = ({
|
export const getSelectedChatConversation = ({
|
||||||
@@ -77,10 +81,24 @@ const getters = {
|
|||||||
return isUnAssigned && shouldFilter;
|
return isUnAssigned && shouldFilter;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getAllStatusChats: _state => activeFilters => {
|
getAllStatusChats: (_state, _, __, rootGetters) => activeFilters => {
|
||||||
|
const currentUser = rootGetters.getCurrentUser;
|
||||||
|
const currentUserId = rootGetters.getCurrentUser.id;
|
||||||
|
const currentAccountId = rootGetters.getCurrentAccountId;
|
||||||
|
|
||||||
|
const permissions = getUserPermissions(currentUser, currentAccountId);
|
||||||
|
const userRole = getUserRole(currentUser, currentAccountId);
|
||||||
|
|
||||||
return _state.allConversations.filter(conversation => {
|
return _state.allConversations.filter(conversation => {
|
||||||
const shouldFilter = applyPageFilters(conversation, activeFilters);
|
const shouldFilter = applyPageFilters(conversation, activeFilters);
|
||||||
return shouldFilter;
|
const allowedForRole = applyRoleFilter(
|
||||||
|
conversation,
|
||||||
|
userRole,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
);
|
||||||
|
|
||||||
|
return shouldFilter && allowedForRole;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getChatListLoadingStatus: ({ listLoadingStatus }) => listLoadingStatus,
|
getChatListLoadingStatus: ({ listLoadingStatus }) => listLoadingStatus,
|
||||||
|
|||||||
@@ -62,6 +62,51 @@ export const applyPageFilters = (conversation, filters) => {
|
|||||||
return shouldFilter;
|
return shouldFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters conversations based on user role and permissions
|
||||||
|
*
|
||||||
|
* @param {Object} conversation - The conversation object to check permissions for
|
||||||
|
* @param {string} role - The user's role (administrator, agent, etc.)
|
||||||
|
* @param {Array<string>} permissions - List of permission strings the user has
|
||||||
|
* @param {number|string} currentUserId - The ID of the current user
|
||||||
|
* @returns {boolean} - Whether the user has permissions to access this conversation
|
||||||
|
*/
|
||||||
|
export const applyRoleFilter = (
|
||||||
|
conversation,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
) => {
|
||||||
|
// the role === "agent" check is typically not correct on it's own
|
||||||
|
// the backend handles this by checking the custom_role_id at the user model
|
||||||
|
// here however, the `getUserRole` returns "custom_role" if the id is present,
|
||||||
|
// so we can check the role === "agent" directly
|
||||||
|
if (['administrator', 'agent'].includes(role)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for full conversation management permission
|
||||||
|
if (permissions.includes('conversation_manage')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversationAssignee = conversation.meta.assignee;
|
||||||
|
const isUnassigned = !conversationAssignee;
|
||||||
|
const isAssignedToUser = conversationAssignee?.id === currentUserId;
|
||||||
|
|
||||||
|
// Check unassigned management permission
|
||||||
|
if (permissions.includes('conversation_unassigned_manage')) {
|
||||||
|
return isUnassigned || isAssignedToUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check participating conversation management permission
|
||||||
|
if (permissions.includes('conversation_participating_manage')) {
|
||||||
|
return isAssignedToUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const SORT_OPTIONS = {
|
const SORT_OPTIONS = {
|
||||||
last_activity_at_asc: ['sortOnLastActivityAt', 'asc'],
|
last_activity_at_asc: ['sortOnLastActivityAt', 'asc'],
|
||||||
last_activity_at_desc: ['sortOnLastActivityAt', 'desc'],
|
last_activity_at_desc: ['sortOnLastActivityAt', 'desc'],
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { applyRoleFilter } from '../helpers';
|
||||||
|
|
||||||
|
describe('Conversation Helpers', () => {
|
||||||
|
describe('#applyRoleFilter', () => {
|
||||||
|
// Test data for conversations
|
||||||
|
const conversationWithAssignee = {
|
||||||
|
meta: {
|
||||||
|
assignee: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const conversationWithDifferentAssignee = {
|
||||||
|
meta: {
|
||||||
|
assignee: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const conversationWithoutAssignee = {
|
||||||
|
meta: {
|
||||||
|
assignee: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test for administrator role
|
||||||
|
it('always returns true for administrator role regardless of permissions', () => {
|
||||||
|
const role = 'administrator';
|
||||||
|
const permissions = [];
|
||||||
|
const currentUserId = 1;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithDifferentAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithoutAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test for agent role
|
||||||
|
it('always returns true for agent role regardless of permissions', () => {
|
||||||
|
const role = 'agent';
|
||||||
|
const permissions = [];
|
||||||
|
const currentUserId = 1;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithDifferentAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithoutAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test for custom role with 'conversation_manage' permission
|
||||||
|
it('returns true for any user with conversation_manage permission', () => {
|
||||||
|
const role = 'custom_role';
|
||||||
|
const permissions = ['conversation_manage'];
|
||||||
|
const currentUserId = 1;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithDifferentAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithoutAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test for custom role with 'conversation_unassigned_manage' permission
|
||||||
|
describe('with conversation_unassigned_manage permission', () => {
|
||||||
|
const role = 'custom_role';
|
||||||
|
const permissions = ['conversation_unassigned_manage'];
|
||||||
|
const currentUserId = 1;
|
||||||
|
|
||||||
|
it('returns true for conversations assigned to the user', () => {
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for unassigned conversations', () => {
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithoutAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for conversations assigned to other users', () => {
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithDifferentAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test for custom role with 'conversation_participating_manage' permission
|
||||||
|
describe('with conversation_participating_manage permission', () => {
|
||||||
|
const role = 'custom_role';
|
||||||
|
const permissions = ['conversation_participating_manage'];
|
||||||
|
const currentUserId = 1;
|
||||||
|
|
||||||
|
it('returns true for conversations assigned to the user', () => {
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for unassigned conversations', () => {
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithoutAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for conversations assigned to other users', () => {
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithDifferentAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test for user with no relevant permissions
|
||||||
|
it('returns false for custom role without any relevant permissions', () => {
|
||||||
|
const role = 'custom_role';
|
||||||
|
const permissions = ['some_other_permission'];
|
||||||
|
const currentUserId = 1;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithDifferentAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithoutAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test edge cases for meta.assignee
|
||||||
|
describe('handles edge cases with meta.assignee', () => {
|
||||||
|
const role = 'custom_role';
|
||||||
|
const permissions = ['conversation_unassigned_manage'];
|
||||||
|
const currentUserId = 1;
|
||||||
|
|
||||||
|
it('treats undefined assignee as unassigned', () => {
|
||||||
|
const conversationWithUndefinedAssignee = {
|
||||||
|
meta: {
|
||||||
|
assignee: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithUndefinedAssignee,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty meta object', () => {
|
||||||
|
const conversationWithEmptyMeta = {
|
||||||
|
meta: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
applyRoleFilter(
|
||||||
|
conversationWithEmptyMeta,
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -38,7 +38,7 @@ export const SDK_CSS = `
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woot-widget-bubble[class] {
|
.woot-widget-bubble {
|
||||||
background: #1f93ff;
|
background: #1f93ff;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
border-width: 0px;
|
border-width: 0px;
|
||||||
|
|||||||
@@ -44,15 +44,18 @@ RSpec.describe 'DeviseOverrides::OmniauthCallbacksController', type: :request do
|
|||||||
|
|
||||||
it 'blocks personal accounts signup' do
|
it 'blocks personal accounts signup' do
|
||||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||||
set_omniauth_config('personal@gmail.com')
|
# Test different case variations of Gmail
|
||||||
get '/omniauth/google_oauth2/callback'
|
['personal@gmail.com', 'personal@Gmail.com', 'personal@GMAIL.com', 'personal@Gmail.COM'].each do |email|
|
||||||
|
set_omniauth_config(email)
|
||||||
|
get '/omniauth/google_oauth2/callback'
|
||||||
|
|
||||||
# expect a 302 redirect to auth/google_oauth2/callback
|
# expect a 302 redirect to auth/google_oauth2/callback
|
||||||
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||||
follow_redirect!
|
follow_redirect!
|
||||||
|
|
||||||
# expect a 302 redirect to app/login with error disallowing personal accounts
|
# expect a 302 redirect to app/login with error disallowing personal accounts
|
||||||
expect(response).to redirect_to(%r{/app/login\?error=business-account-only$})
|
expect(response).to redirect_to(%r{/app/login\?error=business-account-only$})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -315,6 +315,21 @@ export const colors = {
|
|||||||
12: 'rgb(var(--iris-12) / <alpha-value>)',
|
12: 'rgb(var(--iris-12) / <alpha-value>)',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
blue: {
|
||||||
|
1: 'rgb(var(--blue-1) / <alpha-value>)',
|
||||||
|
2: 'rgb(var(--blue-2) / <alpha-value>)',
|
||||||
|
3: 'rgb(var(--blue-3) / <alpha-value>)',
|
||||||
|
4: 'rgb(var(--blue-4) / <alpha-value>)',
|
||||||
|
5: 'rgb(var(--blue-5) / <alpha-value>)',
|
||||||
|
6: 'rgb(var(--blue-6) / <alpha-value>)',
|
||||||
|
7: 'rgb(var(--blue-7) / <alpha-value>)',
|
||||||
|
8: 'rgb(var(--blue-8) / <alpha-value>)',
|
||||||
|
9: 'rgb(var(--blue-9) / <alpha-value>)',
|
||||||
|
10: 'rgb(var(--blue-10) / <alpha-value>)',
|
||||||
|
11: 'rgb(var(--blue-11) / <alpha-value>)',
|
||||||
|
12: 'rgb(var(--blue-12) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
|
||||||
ruby: {
|
ruby: {
|
||||||
1: 'rgb(var(--ruby-1) / <alpha-value>)',
|
1: 'rgb(var(--ruby-1) / <alpha-value>)',
|
||||||
2: 'rgb(var(--ruby-2) / <alpha-value>)',
|
2: 'rgb(var(--ruby-2) / <alpha-value>)',
|
||||||
|
|||||||
Reference in New Issue
Block a user