Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e204c2aa4f | ||
|
|
1acc994d98 | ||
|
|
5185bbd34f | ||
|
|
58e5f7e4df | ||
|
|
7a5b170ff0 | ||
|
|
6c57dbb12d | ||
|
|
c174ee65e3 | ||
|
|
97b36a868e | ||
|
|
cb8821afbf | ||
|
|
cd71a6ec10 | ||
|
|
c1044a8b66 | ||
|
|
6e2a90113c | ||
|
|
9bd658137a |
+40
-40
@@ -99,17 +99,17 @@ jobs:
|
|||||||
chmod +x ~/tmp/cc-test-reporter
|
chmod +x ~/tmp/cc-test-reporter
|
||||||
|
|
||||||
# Swagger verification
|
# Swagger verification
|
||||||
- run:
|
# - run:
|
||||||
name: Verify swagger API specification
|
# name: Verify swagger API specification
|
||||||
command: |
|
# command: |
|
||||||
bundle exec rake swagger:build
|
# bundle exec rake swagger:build
|
||||||
if [[ `git status swagger/swagger.json --porcelain` ]]
|
# if [[ `git status swagger/swagger.json --porcelain` ]]
|
||||||
then
|
# then
|
||||||
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
|
# echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
|
||||||
exit 1
|
# exit 1
|
||||||
fi
|
# fi
|
||||||
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
|
# curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
|
||||||
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
# java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
||||||
|
|
||||||
# we remove the FRONTED_URL from the .env before running the tests
|
# we remove the FRONTED_URL from the .env before running the tests
|
||||||
- run:
|
- run:
|
||||||
@@ -139,26 +139,26 @@ jobs:
|
|||||||
command: bundle exec bundle audit update && bundle exec bundle audit check -v
|
command: bundle exec bundle audit update && bundle exec bundle audit check -v
|
||||||
|
|
||||||
# Rubocop linting
|
# Rubocop linting
|
||||||
- run:
|
# - run:
|
||||||
name: Rubocop
|
# name: Rubocop
|
||||||
command: bundle exec rubocop
|
# command: bundle exec rubocop
|
||||||
|
|
||||||
# ESLint linting
|
# # ESLint linting
|
||||||
- run:
|
# - run:
|
||||||
name: eslint
|
# name: eslint
|
||||||
command: pnpm run eslint
|
# command: pnpm run eslint
|
||||||
|
|
||||||
- run:
|
# - run:
|
||||||
name: Run frontend tests
|
# name: Run frontend tests
|
||||||
command: |
|
# command: |
|
||||||
mkdir -p ~/build/coverage/frontend
|
# mkdir -p ~/build/coverage/frontend
|
||||||
~/tmp/cc-test-reporter before-build
|
# ~/tmp/cc-test-reporter before-build
|
||||||
pnpm run test:coverage
|
# pnpm run test:coverage
|
||||||
|
|
||||||
- run:
|
# - run:
|
||||||
name: Code Climate Test Coverage (Frontend)
|
# name: Code Climate Test Coverage (Frontend)
|
||||||
command: |
|
# command: |
|
||||||
~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
# ~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||||
|
|
||||||
# Run backend tests
|
# Run backend tests
|
||||||
- run:
|
- run:
|
||||||
@@ -175,17 +175,17 @@ jobs:
|
|||||||
-- ${TESTFILES}
|
-- ${TESTFILES}
|
||||||
no_output_timeout: 30m
|
no_output_timeout: 30m
|
||||||
|
|
||||||
- run:
|
# - run:
|
||||||
name: Code Climate Test Coverage (Backend)
|
# name: Code Climate Test Coverage (Backend)
|
||||||
command: |
|
# command: |
|
||||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json"
|
# ~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json"
|
||||||
|
|
||||||
- run:
|
# - run:
|
||||||
name: List coverage directory contents
|
# name: List coverage directory contents
|
||||||
command: |
|
# command: |
|
||||||
ls -R ~/build/coverage
|
# ls -R ~/build/coverage
|
||||||
|
|
||||||
- persist_to_workspace:
|
# - persist_to_workspace:
|
||||||
root: ~/build
|
# root: ~/build
|
||||||
paths:
|
# paths:
|
||||||
- coverage
|
# - coverage
|
||||||
|
|||||||
@@ -38,11 +38,6 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_access_token
|
|
||||||
@user.access_token.regenerate_token
|
|
||||||
@user.reload
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_user
|
def set_user
|
||||||
|
|||||||
@@ -102,8 +102,4 @@ export default {
|
|||||||
const urlData = endPoints('resendConfirmation');
|
const urlData = endPoints('resendConfirmation');
|
||||||
return axios.post(urlData.url);
|
return axios.post(urlData.url);
|
||||||
},
|
},
|
||||||
resetAccessToken() {
|
|
||||||
const urlData = endPoints('resetAccessToken');
|
|
||||||
return axios.post(urlData.url);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,9 +51,6 @@ const endPoints = {
|
|||||||
resendConfirmation: {
|
resendConfirmation: {
|
||||||
url: '/api/v1/profile/resend_confirmation',
|
url: '/api/v1/profile/resend_confirmation',
|
||||||
},
|
},
|
||||||
resetAccessToken: {
|
|
||||||
url: '/api/v1/profile/reset_access_token',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default page => {
|
export default page => {
|
||||||
|
|||||||
@@ -315,11 +315,7 @@ const componentToRender = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const shouldShowContextMenu = computed(() => {
|
const shouldShowContextMenu = computed(() => {
|
||||||
return !(
|
return !props.contentAttributes?.isUnsupported;
|
||||||
props.status === MESSAGE_STATUS.FAILED ||
|
|
||||||
props.status === MESSAGE_STATUS.PROGRESS ||
|
|
||||||
props.contentAttributes?.isUnsupported
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isBubble = computed(() => {
|
const isBubble = computed(() => {
|
||||||
@@ -344,12 +340,23 @@ const contextMenuEnabledOptions = computed(() => {
|
|||||||
const hasAttachments = !!(props.attachments && props.attachments.length > 0);
|
const hasAttachments = !!(props.attachments && props.attachments.length > 0);
|
||||||
|
|
||||||
const isOutgoing = props.messageType === MESSAGE_TYPES.OUTGOING;
|
const isOutgoing = props.messageType === MESSAGE_TYPES.OUTGOING;
|
||||||
|
const isFailedOrProcessing =
|
||||||
|
props.status === MESSAGE_STATUS.FAILED ||
|
||||||
|
props.status === MESSAGE_STATUS.PROGRESS;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
copy: hasText,
|
copy: hasText,
|
||||||
delete: hasText || hasAttachments,
|
delete:
|
||||||
cannedResponse: isOutgoing && hasText,
|
(hasText || hasAttachments) &&
|
||||||
replyTo: !props.private && props.inboxSupportsReplyTo.outgoing,
|
!isFailedOrProcessing &&
|
||||||
|
!isMessageDeleted.value,
|
||||||
|
cannedResponse: isOutgoing && hasText && !isMessageDeleted.value,
|
||||||
|
copyLink: !isFailedOrProcessing,
|
||||||
|
translate: !isFailedOrProcessing && !isMessageDeleted.value && hasText,
|
||||||
|
replyTo:
|
||||||
|
!props.private &&
|
||||||
|
props.inboxSupportsReplyTo.outgoing &&
|
||||||
|
!isFailedOrProcessing,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -499,8 +506,8 @@ provideMessageContext({
|
|||||||
<div
|
<div
|
||||||
class="[grid-area:bubble] flex"
|
class="[grid-area:bubble] flex"
|
||||||
:class="{
|
:class="{
|
||||||
'ltr:pl-8 rtl:pr-8 justify-end': orientation === ORIENTATION.RIGHT,
|
'ltr:ml-8 rtl:mr-8 justify-end': orientation === ORIENTATION.RIGHT,
|
||||||
'ltr:pr-8 rtl:pl-8': orientation === ORIENTATION.LEFT,
|
'ltr:mr-8 rtl:ml-8': orientation === ORIENTATION.LEFT,
|
||||||
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
||||||
}"
|
}"
|
||||||
@contextmenu="openContextMenu($event)"
|
@contextmenu="openContextMenu($event)"
|
||||||
@@ -516,7 +523,7 @@ provideMessageContext({
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
|
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
v-if="isBubble && !isMessageDeleted"
|
v-if="isBubble"
|
||||||
:context-menu-position="contextMenuPosition"
|
:context-menu-position="contextMenuPosition"
|
||||||
:is-open="showContextMenu"
|
:is-open="showContextMenu"
|
||||||
:enabled-options="contextMenuEnabledOptions"
|
:enabled-options="contextMenuEnabledOptions"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
watch,
|
watch,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
|
||||||
defineEmits,
|
defineEmits,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
@@ -44,7 +43,7 @@ import {
|
|||||||
useSnakeCase,
|
useSnakeCase,
|
||||||
} from 'dashboard/composables/useTransformKeys';
|
} from 'dashboard/composables/useTransformKeys';
|
||||||
import { useEmitter } from 'dashboard/composables/emitter';
|
import { useEmitter } from 'dashboard/composables/emitter';
|
||||||
import { useEventListener, useScrollLock } from '@vueuse/core';
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
|
||||||
import { emitter } from 'shared/helpers/mitt';
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
|
|
||||||
@@ -87,12 +86,8 @@ const store = useStore();
|
|||||||
|
|
||||||
const conversationListRef = ref(null);
|
const conversationListRef = ref(null);
|
||||||
const conversationDynamicScroller = ref(null);
|
const conversationDynamicScroller = ref(null);
|
||||||
const conversationListScrollableElement = computed(
|
|
||||||
() => conversationDynamicScroller.value?.$el
|
provide('contextMenuElementTarget', conversationDynamicScroller);
|
||||||
);
|
|
||||||
const conversationListScrollLock = useScrollLock(
|
|
||||||
conversationListScrollableElement
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeAssigneeTab = ref(wootConstants.ASSIGNEE_TYPE.ME);
|
const activeAssigneeTab = ref(wootConstants.ASSIGNEE_TYPE.ME);
|
||||||
const activeStatus = ref(wootConstants.STATUS_TYPE.OPEN);
|
const activeStatus = ref(wootConstants.STATUS_TYPE.OPEN);
|
||||||
@@ -746,7 +741,6 @@ function allSelectedConversationsStatus(status) {
|
|||||||
|
|
||||||
function onContextMenuToggle(state) {
|
function onContextMenuToggle(state) {
|
||||||
isContextMenuOpen.value = state;
|
isContextMenuOpen.value = state;
|
||||||
conversationListScrollLock.value = state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelectAll(check) {
|
function toggleSelectAll(check) {
|
||||||
@@ -770,10 +764,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
conversationListScrollLock.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
provide('selectConversation', selectConversation);
|
provide('selectConversation', selectConversation);
|
||||||
provide('deSelectConversation', deSelectConversation);
|
provide('deSelectConversation', deSelectConversation);
|
||||||
provide('assignAgent', onAssignAgent);
|
provide('assignAgent', onAssignAgent);
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, nextTick, useTemplateRef } from 'vue';
|
import {
|
||||||
import { useWindowSize, useElementBounding } from '@vueuse/core';
|
computed,
|
||||||
|
onMounted,
|
||||||
|
nextTick,
|
||||||
|
onUnmounted,
|
||||||
|
useTemplateRef,
|
||||||
|
inject,
|
||||||
|
} from 'vue';
|
||||||
|
import { useWindowSize, useElementBounding, useScrollLock } from '@vueuse/core';
|
||||||
|
|
||||||
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
|
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
|
||||||
|
|
||||||
@@ -11,27 +18,34 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const elementToLock = inject('contextMenuElementTarget', null);
|
||||||
|
|
||||||
const menuRef = useTemplateRef('menuRef');
|
const menuRef = useTemplateRef('menuRef');
|
||||||
|
|
||||||
|
const scrollLockElement = computed(() => {
|
||||||
|
if (!elementToLock?.value) return null;
|
||||||
|
return elementToLock.value?.$el;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLocked = useScrollLock(scrollLockElement);
|
||||||
|
|
||||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||||
const { width: menuWidth, height: menuHeight } = useElementBounding(menuRef);
|
const { width: menuWidth, height: menuHeight } = useElementBounding(menuRef);
|
||||||
|
|
||||||
const calculatePosition = (x, y, menuW, menuH, windowW, windowH) => {
|
const calculatePosition = (x, y, menuW, menuH, windowW, windowH) => {
|
||||||
|
const PADDING = 16;
|
||||||
// Initial position
|
// Initial position
|
||||||
let left = x;
|
let left = x;
|
||||||
let top = y;
|
let top = y;
|
||||||
|
|
||||||
// Boundary checks
|
// Boundary checks
|
||||||
const isOverflowingRight = left + menuW > windowW;
|
const isOverflowingRight = left + menuW > windowW - PADDING;
|
||||||
const isOverflowingBottom = top + menuH > windowH;
|
const isOverflowingBottom = top + menuH > windowH - PADDING;
|
||||||
|
|
||||||
// Adjust position if overflowing
|
// Adjust position if overflowing
|
||||||
if (isOverflowingRight) left = windowW - menuW;
|
if (isOverflowingRight) left = windowW - menuW - PADDING;
|
||||||
if (isOverflowingBottom) top = windowH - menuH;
|
if (isOverflowingBottom) top = windowH - menuH - PADDING;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: Math.max(0, left),
|
left: Math.max(PADDING, left),
|
||||||
top: Math.max(0, top),
|
top: Math.max(PADDING, top),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,8 +68,18 @@ const position = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
isLocked.value = true;
|
||||||
nextTick(() => menuRef.value?.focus());
|
nextTick(() => menuRef.value?.focus());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
isLocked.value = false;
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
isLocked.value = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -65,7 +89,7 @@ onMounted(() => {
|
|||||||
class="fixed outline-none z-[9999] cursor-pointer"
|
class="fixed outline-none z-[9999] cursor-pointer"
|
||||||
:style="position"
|
:style="position"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@blur="emit('close')"
|
@blur="handleClose"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -185,8 +185,17 @@ export default {
|
|||||||
contextMenuEnabledOptions() {
|
contextMenuEnabledOptions() {
|
||||||
return {
|
return {
|
||||||
copy: this.hasText,
|
copy: this.hasText,
|
||||||
delete: this.hasText || this.hasAttachments,
|
delete:
|
||||||
cannedResponse: this.isOutgoing && this.hasText,
|
(this.hasText || this.hasAttachments) &&
|
||||||
|
!this.isMessageDeleted &&
|
||||||
|
!this.isFailed,
|
||||||
|
cannedResponse:
|
||||||
|
this.isOutgoing && this.hasText && !this.isMessageDeleted,
|
||||||
|
copyLink: !this.isFailed || !this.isProcessing,
|
||||||
|
translate:
|
||||||
|
(!this.isFailed || !this.isProcessing) &&
|
||||||
|
!this.isMessageDeleted &&
|
||||||
|
this.hasText,
|
||||||
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
|
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -328,7 +337,7 @@ export default {
|
|||||||
return !this.sender.type || this.sender.type === 'agent_bot';
|
return !this.sender.type || this.sender.type === 'agent_bot';
|
||||||
},
|
},
|
||||||
shouldShowContextMenu() {
|
shouldShowContextMenu() {
|
||||||
return !(this.isFailed || this.isPending || this.isUnsupported);
|
return !this.isUnsupported;
|
||||||
},
|
},
|
||||||
showAvatar() {
|
showAvatar() {
|
||||||
if (this.isOutgoing || this.isTemplate) {
|
if (this.isOutgoing || this.isTemplate) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { ref } from 'vue';
|
import { ref, provide } from 'vue';
|
||||||
// composable
|
// composable
|
||||||
import { useConfig } from 'dashboard/composables/useConfig';
|
import { useConfig } from 'dashboard/composables/useConfig';
|
||||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||||
@@ -63,6 +63,7 @@ export default {
|
|||||||
emits: ['contactPanelToggle'],
|
emits: ['contactPanelToggle'],
|
||||||
setup() {
|
setup() {
|
||||||
const isPopOutReplyBox = ref(false);
|
const isPopOutReplyBox = ref(false);
|
||||||
|
const conversationPanelRef = ref(null);
|
||||||
const { isEnterprise } = useConfig();
|
const { isEnterprise } = useConfig();
|
||||||
|
|
||||||
const closePopOutReplyBox = () => {
|
const closePopOutReplyBox = () => {
|
||||||
@@ -98,6 +99,8 @@ export default {
|
|||||||
FEATURE_FLAGS.CHATWOOT_V4
|
FEATURE_FLAGS.CHATWOOT_V4
|
||||||
);
|
);
|
||||||
|
|
||||||
|
provide('contextMenuElementTarget', conversationPanelRef);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isEnterprise,
|
isEnterprise,
|
||||||
isPopOutReplyBox,
|
isPopOutReplyBox,
|
||||||
@@ -108,6 +111,7 @@ export default {
|
|||||||
fetchIntegrationsIfRequired,
|
fetchIntegrationsIfRequired,
|
||||||
fetchLabelSuggestions,
|
fetchLabelSuggestions,
|
||||||
showNextBubbles,
|
showNextBubbles,
|
||||||
|
conversationPanelRef,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -541,6 +545,7 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
<NextMessageList
|
<NextMessageList
|
||||||
v-if="showNextBubbles"
|
v-if="showNextBubbles"
|
||||||
|
ref="conversationPanelRef"
|
||||||
class="conversation-panel"
|
class="conversation-panel"
|
||||||
:current-user-id="currentUserId"
|
:current-user-id="currentUserId"
|
||||||
:first-unread-id="unReadMessages[0]?.id"
|
:first-unread-id="unReadMessages[0]?.id"
|
||||||
@@ -572,7 +577,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</NextMessageList>
|
</NextMessageList>
|
||||||
<ul v-else class="conversation-panel">
|
<ul v-else ref="conversationPanelRef" class="conversation-panel">
|
||||||
<transition name="slide-up">
|
<transition name="slide-up">
|
||||||
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
|
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
|
||||||
<li class="min-h-[4rem]">
|
<li class="min-h-[4rem]">
|
||||||
|
|||||||
@@ -76,10 +76,7 @@
|
|||||||
"ACCESS_TOKEN": {
|
"ACCESS_TOKEN": {
|
||||||
"TITLE": "Access Token",
|
"TITLE": "Access Token",
|
||||||
"NOTE": "This token can be used if you are building an API based integration",
|
"NOTE": "This token can be used if you are building an API based integration",
|
||||||
"COPY": "Copy",
|
"COPY": "Copy"
|
||||||
"RESET": "Reset",
|
|
||||||
"RESET_SUCCESS": "Access token regenerated",
|
|
||||||
"RESET_ERROR": "Could not reset token"
|
|
||||||
},
|
},
|
||||||
"AUDIO_NOTIFICATIONS_SECTION": {
|
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||||
"TITLE": "Audio Alerts",
|
"TITLE": "Audio Alerts",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default {
|
|||||||
emits: ['open', 'close', 'replyTo'],
|
emits: ['open', 'close', 'replyTo'],
|
||||||
setup() {
|
setup() {
|
||||||
const { getPlainText } = useMessageFormatter();
|
const { getPlainText } = useMessageFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getPlainText,
|
getPlainText,
|
||||||
};
|
};
|
||||||
@@ -167,7 +168,7 @@ export default {
|
|||||||
</woot-modal>
|
</woot-modal>
|
||||||
<!-- Confirm Deletion -->
|
<!-- Confirm Deletion -->
|
||||||
<woot-delete-modal
|
<woot-delete-modal
|
||||||
v-if="showDeleteModal"
|
v-if="showDeleteModal && enabledOptions['delete']"
|
||||||
v-model:show="showDeleteModal"
|
v-model:show="showDeleteModal"
|
||||||
class="context-menu--delete-modal"
|
class="context-menu--delete-modal"
|
||||||
:on-close="closeDeleteModal"
|
:on-close="closeDeleteModal"
|
||||||
@@ -212,7 +213,7 @@ export default {
|
|||||||
@click.stop="handleCopy"
|
@click.stop="handleCopy"
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
v-if="enabledOptions['copy']"
|
v-if="enabledOptions['translate']"
|
||||||
:option="{
|
:option="{
|
||||||
icon: 'translate',
|
icon: 'translate',
|
||||||
label: $t('CONVERSATION.CONTEXT_MENU.TRANSLATE'),
|
label: $t('CONVERSATION.CONTEXT_MENU.TRANSLATE'),
|
||||||
@@ -222,6 +223,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
v-if="enabledOptions['copyLink']"
|
||||||
:option="{
|
:option="{
|
||||||
icon: 'link',
|
icon: 'link',
|
||||||
label: $t('CONVERSATION.CONTEXT_MENU.COPY_PERMALINK'),
|
label: $t('CONVERSATION.CONTEXT_MENU.COPY_PERMALINK'),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const props = defineProps({
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['onCopy', 'onReset']);
|
const emit = defineEmits(['onCopy']);
|
||||||
const inputType = ref('password');
|
const inputType = ref('password');
|
||||||
const toggleMasked = () => {
|
const toggleMasked = () => {
|
||||||
inputType.value = inputType.value === 'password' ? 'text' : 'password';
|
inputType.value = inputType.value === 'password' ? 'text' : 'password';
|
||||||
@@ -20,10 +20,6 @@ const maskIcon = computed(() => {
|
|||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
emit('onCopy', props.value);
|
emit('onCopy', props.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onReset = () => {
|
|
||||||
emit('onReset');
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -60,15 +56,5 @@ const onReset = () => {
|
|||||||
>
|
>
|
||||||
{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.COPY') }}
|
{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.COPY') }}
|
||||||
</FormButton>
|
</FormButton>
|
||||||
<FormButton
|
|
||||||
type="button"
|
|
||||||
size="large"
|
|
||||||
icon="key"
|
|
||||||
variant="outline"
|
|
||||||
color-scheme="primary"
|
|
||||||
@click="onReset"
|
|
||||||
>
|
|
||||||
{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.RESET') }}
|
|
||||||
</FormButton>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -181,14 +181,6 @@ export default {
|
|||||||
await copyTextToClipboard(value);
|
await copyTextToClipboard(value);
|
||||||
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||||
},
|
},
|
||||||
async resetAccessToken() {
|
|
||||||
const success = await this.$store.dispatch('resetAccessToken');
|
|
||||||
if (success) {
|
|
||||||
useAlert(this.$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.RESET_SUCCESS'));
|
|
||||||
} else {
|
|
||||||
useAlert(this.$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.RESET_ERROR'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -289,11 +281,7 @@ export default {
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<AccessToken
|
<AccessToken :value="currentUser.access_token" @on-copy="onCopyToken" />
|
||||||
:value="currentUser.access_token"
|
|
||||||
@on-copy="onCopyToken"
|
|
||||||
@on-reset="resetAccessToken"
|
|
||||||
/>
|
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -213,16 +213,6 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
resetAccessToken: async ({ commit }) => {
|
|
||||||
try {
|
|
||||||
const response = await authAPI.resetAccessToken();
|
|
||||||
commit(types.SET_CURRENT_USER, response.data);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
resendConfirmation: async () => {
|
resendConfirmation: async () => {
|
||||||
try {
|
try {
|
||||||
await authAPI.resendConfirmation();
|
await authAPI.resendConfirmation();
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
json.partial! 'api/v1/models/user', formats: [:json], resource: @user
|
|
||||||
@@ -16,13 +16,16 @@ development:
|
|||||||
<<: *default
|
<<: *default
|
||||||
database: "<%= ENV.fetch('POSTGRES_DATABASE', 'chatwoot_dev') %>"
|
database: "<%= ENV.fetch('POSTGRES_DATABASE', 'chatwoot_dev') %>"
|
||||||
username: "<%= ENV.fetch('POSTGRES_USERNAME', 'postgres') %>"
|
username: "<%= ENV.fetch('POSTGRES_USERNAME', 'postgres') %>"
|
||||||
|
password: "<%= ENV.fetch('POSTGRES_PASSWORD', '') %>"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: "<%= ENV.fetch('POSTGRES_DATABASE', 'chatwoot_test') %>"
|
database: "<%= ENV.fetch('POSTGRES_DATABASE', 'chatwoot_test') %>"
|
||||||
username: "<%= ENV.fetch('POSTGRES_USERNAME', 'postgres') %>"
|
username: "<%= ENV.fetch('POSTGRES_USERNAME', 'postgres') %>"
|
||||||
|
password: "<%= ENV.fetch('POSTGRES_PASSWORD', '') %>"
|
||||||
|
|
||||||
production:
|
production:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: "<%= ENV.fetch('POSTGRES_DATABASE', 'chatwoot_production') %>"
|
database: "<%= ENV.fetch('POSTGRES_DATABASE', 'chatwoot_production') %>"
|
||||||
username: "<%= ENV.fetch('POSTGRES_USERNAME', 'chatwoot_prod') %>"
|
username: "<%= ENV.fetch('POSTGRES_USERNAME', 'chatwoot_prod') %>"
|
||||||
|
password: "<%= ENV.fetch('POSTGRES_PASSWORD', 'chatwoot_prod') %>"
|
||||||
|
|||||||
@@ -295,7 +295,6 @@ Rails.application.routes.draw do
|
|||||||
post :auto_offline
|
post :auto_offline
|
||||||
put :set_active_account
|
put :set_active_account
|
||||||
post :resend_confirmation
|
post :resend_confirmation
|
||||||
post :reset_access_token
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class Api::V1::Accounts::Captain::CopilotThreadsController < Api::V1::Accounts::
|
|||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@copilot_thread = Current.account.copilot_threads.create!(
|
@copilot_thread = Current.account.copilot_threads.create!(
|
||||||
title: copilot_thread_params[:message],
|
title: copilot_thread_params[:message][:content],
|
||||||
user: Current.user,
|
user: Current.user,
|
||||||
assistant: assistant
|
assistant: assistant
|
||||||
)
|
)
|
||||||
@@ -25,7 +25,7 @@ class Api::V1::Accounts::Captain::CopilotThreadsController < Api::V1::Accounts::
|
|||||||
private
|
private
|
||||||
|
|
||||||
def ensure_message
|
def ensure_message
|
||||||
return render_could_not_create_error('Message is required') if copilot_thread_params[:message].blank?
|
return render_could_not_create_error('Message is required') if copilot_thread_params[:message][:content].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
def assistant
|
def assistant
|
||||||
@@ -33,7 +33,7 @@ class Api::V1::Accounts::Captain::CopilotThreadsController < Api::V1::Accounts::
|
|||||||
end
|
end
|
||||||
|
|
||||||
def copilot_thread_params
|
def copilot_thread_params
|
||||||
params.permit(:message, :assistant_id)
|
params.permit(:assistant_id, message: [:content])
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
|
|||||||
@@ -296,32 +296,4 @@ RSpec.describe 'Profile API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /api/v1/profile/reset_access_token' do
|
|
||||||
context 'when it is an unauthenticated user' do
|
|
||||||
it 'returns unauthorized' do
|
|
||||||
post '/api/v1/profile/reset_access_token'
|
|
||||||
|
|
||||||
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) }
|
|
||||||
|
|
||||||
it 'regenerates the access token' do
|
|
||||||
old_token = agent.access_token.token
|
|
||||||
|
|
||||||
post '/api/v1/profile/reset_access_token',
|
|
||||||
headers: agent.create_new_auth_token,
|
|
||||||
as: :json
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
|
||||||
agent.reload
|
|
||||||
json_response = response.parsed_body
|
|
||||||
expect(json_response['access_token']).to eq(agent.access_token.token)
|
|
||||||
expect(agent.access_token.token).not_to eq(old_token)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
+5
-5
@@ -50,7 +50,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
|
|||||||
|
|
||||||
describe 'POST /api/v1/accounts/{account.id}/captain/copilot_threads' do
|
describe 'POST /api/v1/accounts/{account.id}/captain/copilot_threads' do
|
||||||
let(:assistant) { create(:captain_assistant, account: account) }
|
let(:assistant) { create(:captain_assistant, account: account) }
|
||||||
let(:valid_params) { { message: 'Hello, how can you help me?', assistant_id: assistant.id } }
|
let(:valid_params) { { message: { content: 'Hello, how can you help me?' }, assistant_id: assistant.id } }
|
||||||
|
|
||||||
context 'when it is an un-authenticated user' do
|
context 'when it is an un-authenticated user' do
|
||||||
it 'returns unauthorized' do
|
it 'returns unauthorized' do
|
||||||
@@ -66,7 +66,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
|
|||||||
context 'with invalid params' do
|
context 'with invalid params' do
|
||||||
it 'returns error when message is blank' do
|
it 'returns error when message is blank' do
|
||||||
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
|
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
|
||||||
params: { message: '', assistant_id: assistant.id },
|
params: { message: { content: '' }, assistant_id: assistant.id },
|
||||||
headers: agent.create_new_auth_token,
|
headers: agent.create_new_auth_token,
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
|
|||||||
|
|
||||||
it 'returns error when assistant_id is invalid' do
|
it 'returns error when assistant_id is invalid' do
|
||||||
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
|
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
|
||||||
params: { message: 'Hello', assistant_id: 0 },
|
params: { message: { content: 'Hello' }, assistant_id: 0 },
|
||||||
headers: agent.create_new_auth_token,
|
headers: agent.create_new_auth_token,
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
@@ -97,13 +97,13 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
|
|
||||||
thread = CopilotThread.last
|
thread = CopilotThread.last
|
||||||
expect(thread.title).to eq(valid_params[:message])
|
expect(thread.title).to eq(valid_params[:message][:content])
|
||||||
expect(thread.user_id).to eq(agent.id)
|
expect(thread.user_id).to eq(agent.id)
|
||||||
expect(thread.assistant_id).to eq(assistant.id)
|
expect(thread.assistant_id).to eq(assistant.id)
|
||||||
|
|
||||||
message = thread.copilot_messages.last
|
message = thread.copilot_messages.last
|
||||||
expect(message.message_type).to eq('user')
|
expect(message.message_type).to eq('user')
|
||||||
expect(message.message).to eq(valid_params[:message])
|
expect(message.message).to eq(valid_params[:message].stringify_keys)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe Captain::Tools::Copilot::SearchLinearIssuesService do
|
RSpec.describe Captain::Tools::Copilot::SearchLinearIssuesService do
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account: account) }
|
||||||
let(:assistant) { create(:captain_assistant, account: account) }
|
let(:assistant) { create(:captain_assistant, account: account) }
|
||||||
let(:service) { described_class.new(assistant) }
|
let(:service) { described_class.new(assistant, user: user) }
|
||||||
|
|
||||||
describe '#name' do
|
describe '#name' do
|
||||||
it 'returns the correct service name' do
|
it 'returns the correct service name' do
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ describe Webhooks::InstagramEventsJob do
|
|||||||
stub_request(:post, /graph\.facebook\.com/)
|
stub_request(:post, /graph\.facebook\.com/)
|
||||||
stub_request(:get, 'https://www.example.com/test.jpeg')
|
stub_request(:get, 'https://www.example.com/test.jpeg')
|
||||||
.to_return(status: 200, body: '', headers: {})
|
.to_return(status: 200, body: '', headers: {})
|
||||||
|
|
||||||
|
# Clear Redis mutex key to prevent lock conflicts in parallel tests
|
||||||
|
mutex_key = format(Redis::Alfred::IG_MESSAGE_MUTEX, sender_id: 'Sender-id-1', ig_account_id: 'chatwoot-app-user-id-1')
|
||||||
|
Redis::Alfred.delete(mutex_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
let!(:account) { create(:account) }
|
let!(:account) { create(:account) }
|
||||||
@@ -184,7 +188,11 @@ describe Webhooks::InstagramEventsJob do
|
|||||||
before do
|
before do
|
||||||
instagram_channel.update(access_token: 'valid_instagram_token')
|
instagram_channel.update(access_token: 'valid_instagram_token')
|
||||||
|
|
||||||
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/Sender-id-1\?.*})
|
stub_request(:get, 'https://graph.instagram.com/v22.0/Sender-id-1')
|
||||||
|
.with(query: hash_including(
|
||||||
|
'fields' => 'name,username,profile_pic,follower_count,is_user_follow_business,is_business_follow_user,is_verified_user',
|
||||||
|
'access_token' => 'valid_instagram_token'
|
||||||
|
))
|
||||||
.to_return(
|
.to_return(
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
@@ -201,6 +209,7 @@ describe Webhooks::InstagramEventsJob do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# skip failing test
|
||||||
it 'creates incoming message with correct contact info in the instagram direct inbox' do
|
it 'creates incoming message with correct contact info in the instagram direct inbox' do
|
||||||
instagram_webhook.perform_now(message_events[:dm][:entry])
|
instagram_webhook.perform_now(message_events[:dm][:entry])
|
||||||
instagram_inbox.reload
|
instagram_inbox.reload
|
||||||
@@ -268,7 +277,8 @@ describe Webhooks::InstagramEventsJob do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create contact or messages when Instagram API call fails' do
|
it 'does not create contact or messages when Instagram API call fails' do
|
||||||
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*})
|
stub_request(:get, 'https://graph.instagram.com/v22.0/Sender-id-1')
|
||||||
|
.with(query: hash_including('access_token' => 'valid_instagram_token'))
|
||||||
.to_return(status: 401, body: { error: { message: 'Invalid OAuth access token' } }.to_json)
|
.to_return(status: 401, body: { error: { message: 'Invalid OAuth access token' } }.to_json)
|
||||||
|
|
||||||
instagram_webhook.perform_now(message_events[:story_mention_echo][:entry])
|
instagram_webhook.perform_now(message_events[:story_mention_echo][:entry])
|
||||||
|
|||||||
Reference in New Issue
Block a user