Merge branch 'develop' into fix/conv-order
This commit is contained in:
@@ -5,7 +5,7 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import HelperTextPopup from 'dashboard/components/ui/HelperTextPopup.vue';
|
||||
import { isValidURL } from '../helper/URLHelper';
|
||||
import customAttributeMixin from '../mixins/customAttributeMixin';
|
||||
import { getRegexp } from 'shared/helpers/Validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
@@ -15,7 +15,6 @@ export default {
|
||||
MultiselectDropdown,
|
||||
HelperTextPopup,
|
||||
},
|
||||
mixins: [customAttributeMixin],
|
||||
props: {
|
||||
label: { type: String, required: true },
|
||||
description: { type: String, default: '' },
|
||||
@@ -128,8 +127,7 @@ export default {
|
||||
required,
|
||||
regexValidation: value => {
|
||||
return !(
|
||||
this.attributeRegex &&
|
||||
!this.getRegexp(this.attributeRegex).test(value)
|
||||
this.attributeRegex && !getRegexp(this.attributeRegex).test(value)
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -101,6 +101,8 @@ const shouldShowEmptyState = computed(() => {
|
||||
:key="item.id"
|
||||
:is-active="isFilterActive(item.id)"
|
||||
:button-text="item.name"
|
||||
:icon="item.icon"
|
||||
:icon-color="item.iconColor"
|
||||
@click="$emit('click', item)"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
@@ -8,6 +8,14 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -20,6 +28,12 @@ defineProps({
|
||||
@focus="$emit('focus')"
|
||||
>
|
||||
<div class="inline-flex items-center gap-3 overflow-hidden">
|
||||
<fluent-icon
|
||||
v-if="icon"
|
||||
:icon="icon"
|
||||
size="18"
|
||||
:style="{ color: iconColor }"
|
||||
/>
|
||||
<span
|
||||
class="text-sm font-medium truncate text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
|
||||
@@ -62,6 +62,8 @@ const onSearch = async value => {
|
||||
issues.value = response.data.map(issue => ({
|
||||
id: issue.id,
|
||||
name: `${issue.identifier} ${issue.title}`,
|
||||
icon: 'status',
|
||||
iconColor: issue.state.color,
|
||||
}));
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useMacros } from '../useMacros';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/helper/automationHelper.js');
|
||||
|
||||
describe('useMacros', () => {
|
||||
const mockLabels = [
|
||||
{
|
||||
id: 6,
|
||||
title: 'sales',
|
||||
description: 'sales team',
|
||||
color: '#8EA20F',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'billing',
|
||||
description: 'billing',
|
||||
color: '#4077DA',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'snoozed',
|
||||
description: 'Items marked for later',
|
||||
color: '#D12F42',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'mobile-app',
|
||||
description: 'tech team',
|
||||
color: '#2DB1CC',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
title: 'human-resources-department-with-long-title',
|
||||
description: 'Test',
|
||||
color: '#FF6E09',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
title: 'priority',
|
||||
description: 'For important sales leads',
|
||||
color: '#7E7CED',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
];
|
||||
const mockTeams = [
|
||||
{
|
||||
id: 1,
|
||||
name: '⚙️ sales team',
|
||||
description: 'This is our internal sales team',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '🤷♂️ fayaz',
|
||||
description: 'Test',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '🇮🇳 apac sales',
|
||||
description: 'Sales team for France Territory',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
];
|
||||
const mockAgents = [
|
||||
{
|
||||
id: 1,
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'john@doe.com',
|
||||
available_name: 'John Doe',
|
||||
name: 'John Doe',
|
||||
role: 'agent',
|
||||
thumbnail: 'https://example.com/image.png',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'clark@kent.com',
|
||||
available_name: 'Clark Kent',
|
||||
name: 'Clark Kent',
|
||||
role: 'agent',
|
||||
thumbnail: '',
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
useStoreGetters.mockReturnValue({
|
||||
'labels/getLabels': { value: mockLabels },
|
||||
'teams/getTeams': { value: mockTeams },
|
||||
'agents/getAgents': { value: mockAgents },
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes computed properties correctly', () => {
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
expect(getMacroDropdownValues('add_label')).toHaveLength(mockLabels.length);
|
||||
expect(getMacroDropdownValues('assign_team')).toHaveLength(
|
||||
mockTeams.length
|
||||
);
|
||||
expect(getMacroDropdownValues('assign_agent')).toHaveLength(
|
||||
mockAgents.length + 1
|
||||
); // +1 for "Self"
|
||||
});
|
||||
|
||||
it('returns teams for assign_team and send_email_to_team types', () => {
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
expect(getMacroDropdownValues('assign_team')).toEqual(mockTeams);
|
||||
expect(getMacroDropdownValues('send_email_to_team')).toEqual(mockTeams);
|
||||
});
|
||||
|
||||
it('returns agents with "Self" option for assign_agent type', () => {
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
const result = getMacroDropdownValues('assign_agent');
|
||||
expect(result[0]).toEqual({ id: 'self', name: 'Self' });
|
||||
expect(result.slice(1)).toEqual(mockAgents);
|
||||
});
|
||||
|
||||
it('returns formatted labels for add_label and remove_label types', () => {
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
const expectedLabels = mockLabels.map(i => ({
|
||||
id: i.title,
|
||||
name: i.title,
|
||||
}));
|
||||
expect(getMacroDropdownValues('add_label')).toEqual(expectedLabels);
|
||||
expect(getMacroDropdownValues('remove_label')).toEqual(expectedLabels);
|
||||
});
|
||||
|
||||
it('returns PRIORITY_CONDITION_VALUES for change_priority type', () => {
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
expect(getMacroDropdownValues('change_priority')).toEqual(
|
||||
PRIORITY_CONDITION_VALUES
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty array for unknown types', () => {
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
expect(getMacroDropdownValues('unknown_type')).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles empty data correctly', () => {
|
||||
useStoreGetters.mockReturnValue({
|
||||
'labels/getLabels': { value: [] },
|
||||
'teams/getTeams': { value: [] },
|
||||
'agents/getAgents': { value: [] },
|
||||
});
|
||||
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
expect(getMacroDropdownValues('add_label')).toEqual([]);
|
||||
expect(getMacroDropdownValues('assign_team')).toEqual([]);
|
||||
expect(getMacroDropdownValues('assign_agent')).toEqual([
|
||||
{ id: 'self', name: 'Self' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { computed } from 'vue';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js';
|
||||
|
||||
/**
|
||||
* Composable for handling macro-related functionality
|
||||
* @returns {Object} An object containing the getMacroDropdownValues function
|
||||
*/
|
||||
export const useMacros = () => {
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const labels = computed(() => getters['labels/getLabels'].value);
|
||||
const teams = computed(() => getters['teams/getTeams'].value);
|
||||
const agents = computed(() => getters['agents/getAgents'].value);
|
||||
|
||||
/**
|
||||
* Get dropdown values based on the specified type
|
||||
* @param {string} type - The type of dropdown values to retrieve
|
||||
* @returns {Array} An array of dropdown values
|
||||
*/
|
||||
const getMacroDropdownValues = type => {
|
||||
switch (type) {
|
||||
case 'assign_team':
|
||||
case 'send_email_to_team':
|
||||
return teams.value;
|
||||
case 'assign_agent':
|
||||
return [{ id: 'self', name: 'Self' }, ...agents.value];
|
||||
case 'add_label':
|
||||
case 'remove_label':
|
||||
return labels.value.map(i => ({
|
||||
id: i.title,
|
||||
name: i.title,
|
||||
}));
|
||||
case 'change_priority':
|
||||
return PRIORITY_CONDITION_VALUES;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getMacroDropdownValues,
|
||||
};
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
export default {
|
||||
methods: {
|
||||
getRegexp(regexPatternValue) {
|
||||
let lastSlash = regexPatternValue.lastIndexOf('/');
|
||||
return new RegExp(
|
||||
regexPatternValue.slice(1, lastSlash),
|
||||
regexPatternValue.slice(lastSlash + 1)
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js';
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
labels: 'labels/getLabels',
|
||||
teams: 'teams/getTeams',
|
||||
agents: 'agents/getAgents',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
getDropdownValues(type) {
|
||||
switch (type) {
|
||||
case 'assign_team':
|
||||
case 'send_email_to_team':
|
||||
return this.teams;
|
||||
case 'assign_agent':
|
||||
return [{ id: 'self', name: 'Self' }, ...this.agents];
|
||||
case 'add_label':
|
||||
case 'remove_label':
|
||||
return this.labels.map(i => {
|
||||
return {
|
||||
id: i.title,
|
||||
name: i.title,
|
||||
};
|
||||
});
|
||||
case 'change_priority':
|
||||
return PRIORITY_CONDITION_VALUES;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import { createWrapper } from '@vue/test-utils';
|
||||
import macrosMixin from '../macrosMixin';
|
||||
import Vue from 'vue';
|
||||
import { teams, labels, agents } from '../../helper/specs/macrosFixtures';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js';
|
||||
describe('webhookMixin', () => {
|
||||
describe('#getEventLabel', () => {
|
||||
it('returns correct i18n translation:', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'MyComponent',
|
||||
mixins: [macrosMixin],
|
||||
data: () => {
|
||||
return {
|
||||
teams,
|
||||
labels,
|
||||
agents,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
$t(text) {
|
||||
return text;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resolvedLabels = labels.map(i => {
|
||||
return {
|
||||
id: i.title,
|
||||
name: i.title,
|
||||
};
|
||||
});
|
||||
|
||||
const Constructor = Vue.extend(Component);
|
||||
const vm = new Constructor().$mount();
|
||||
const wrapper = createWrapper(vm);
|
||||
expect(wrapper.vm.getDropdownValues('assign_team')).toEqual(teams);
|
||||
expect(wrapper.vm.getDropdownValues('send_email_to_team')).toEqual(teams);
|
||||
expect(wrapper.vm.getDropdownValues('add_label')).toEqual(resolvedLabels);
|
||||
expect(wrapper.vm.getDropdownValues('assign_agent')).toEqual([
|
||||
{ id: 'self', name: 'Self' },
|
||||
...agents,
|
||||
]);
|
||||
expect(wrapper.vm.getDropdownValues('change_priority')).toEqual(
|
||||
PRIORITY_CONDITION_VALUES
|
||||
);
|
||||
expect(wrapper.vm.getDropdownValues()).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,43 +1,34 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
export default {
|
||||
components: {
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
data() {
|
||||
return {
|
||||
noteContent: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
buttonDisabled() {
|
||||
return this.noteContent === '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+Enter': {
|
||||
action: () => this.onAdd(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
onAdd() {
|
||||
if (this.noteContent !== '') {
|
||||
this.$emit('add', this.noteContent);
|
||||
}
|
||||
this.noteContent = '';
|
||||
},
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const addNoteRef = ref(null);
|
||||
const noteContent = ref('');
|
||||
|
||||
const buttonDisabled = computed(() => noteContent.value === '');
|
||||
|
||||
const onAdd = () => {
|
||||
if (noteContent.value !== '') {
|
||||
emit('add', noteContent.value);
|
||||
}
|
||||
noteContent.value = '';
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
'$mod+Enter': {
|
||||
action: () => onAdd(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, addNoteRef);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="addNoteRef"
|
||||
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
<WootMessageEditor
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||
|
||||
export default {
|
||||
@@ -14,7 +15,7 @@ export default {
|
||||
AddLabel,
|
||||
},
|
||||
|
||||
mixins: [conversationLabelMixin, keyboardEventListenerMixins],
|
||||
mixins: [conversationLabelMixin],
|
||||
props: {
|
||||
// conversationId prop is used in /conversation/labelMixin,
|
||||
// remove this props when refactoring to composable if not needed
|
||||
@@ -26,14 +27,46 @@ export default {
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const conversationLabelBoxRef = ref(null);
|
||||
const showSearchDropdownLabel = ref(false);
|
||||
|
||||
const toggleLabels = () => {
|
||||
showSearchDropdownLabel.value = !showSearchDropdownLabel.value;
|
||||
};
|
||||
|
||||
const closeDropdownLabel = () => {
|
||||
showSearchDropdownLabel.value = false;
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
KeyL: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
toggleLabels();
|
||||
},
|
||||
},
|
||||
Escape: {
|
||||
action: () => {
|
||||
if (showSearchDropdownLabel.value) {
|
||||
toggleLabels();
|
||||
}
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, conversationLabelBoxRef);
|
||||
return {
|
||||
isAdmin,
|
||||
conversationLabelBoxRef,
|
||||
showSearchDropdownLabel,
|
||||
closeDropdownLabel,
|
||||
toggleLabels,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedLabels: [],
|
||||
showSearchDropdownLabel: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -42,37 +75,11 @@ export default {
|
||||
conversationUiFlags: 'conversationLabels/getUIFlags',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
toggleLabels() {
|
||||
this.showSearchDropdownLabel = !this.showSearchDropdownLabel;
|
||||
},
|
||||
closeDropdownLabel() {
|
||||
this.showSearchDropdownLabel = false;
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
KeyL: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.toggleLabels();
|
||||
},
|
||||
},
|
||||
Escape: {
|
||||
action: () => {
|
||||
if (this.showSearchDropdownLabel) {
|
||||
this.toggleLabels();
|
||||
}
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-labels-wrap">
|
||||
<div ref="conversationLabelBoxRef" class="sidebar-labels-wrap">
|
||||
<div
|
||||
v-if="!conversationUiFlags.isFetching"
|
||||
class="contact-conversation--list"
|
||||
|
||||
+40
-44
@@ -1,53 +1,51 @@
|
||||
<script>
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
|
||||
export default {
|
||||
name: 'ChatwootSearch',
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Chatwoot',
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Chatwoot',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['search', 'close']);
|
||||
|
||||
const articleSearchHeaderRef = ref(null);
|
||||
const searchInputRef = ref(null);
|
||||
const searchQuery = ref('');
|
||||
|
||||
onMounted(() => {
|
||||
searchInputRef.value.focus();
|
||||
});
|
||||
|
||||
const onInput = e => {
|
||||
emit('search', e.target.value);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
Slash: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
searchInputRef.value.focus();
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
isInputFocused: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.searchInput.focus();
|
||||
},
|
||||
methods: {
|
||||
onInput(e) {
|
||||
this.$emit('search', e.target.value);
|
||||
},
|
||||
onClose() {
|
||||
this.$emit('close');
|
||||
},
|
||||
onFocus() {
|
||||
this.isInputFocused = true;
|
||||
},
|
||||
onBlur() {
|
||||
this.isInputFocused = false;
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Slash: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.$refs.searchInput.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
Escape: {
|
||||
action: () => {
|
||||
onClose();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, articleSearchHeaderRef);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col py-1">
|
||||
<div ref="articleSearchHeaderRef" class="flex flex-col py-1">
|
||||
<div class="flex items-center justify-between py-2 mb-1">
|
||||
<h3 class="text-base text-slate-900 dark:text-slate-25">
|
||||
{{ title }}
|
||||
@@ -68,13 +66,11 @@ export default {
|
||||
<fluent-icon icon="search" class="" size="18" />
|
||||
</div>
|
||||
<input
|
||||
ref="searchInput"
|
||||
ref="searchInputRef"
|
||||
type="text"
|
||||
:placeholder="$t('HELP_CENTER.ARTICLE_SEARCH.PLACEHOLDER')"
|
||||
class="block w-full !h-9 ltr:!pl-8 rtl:!pr-8 dark:!bg-slate-700 !bg-slate-25 text-sm rounded-md leading-8 text-slate-700 shadow-sm ring-2 ring-transparent ring-slate-300 border border-solid border-slate-300 placeholder:text-slate-400 focus:border-woot-600 focus:ring-woot-200 !mb-0 focus:bg-slate-25 dark:focus:bg-slate-700 dark:focus:ring-woot-700"
|
||||
:value="searchQuery"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onInput"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
-12
@@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
import SearchHeader from './Header.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
@@ -17,7 +16,7 @@ export default {
|
||||
SearchResults,
|
||||
ArticleView,
|
||||
},
|
||||
mixins: [portalMixin, keyboardEventListenerMixins],
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
selectedPortalSlug: {
|
||||
type: String,
|
||||
@@ -114,16 +113,6 @@ export default {
|
||||
useAlert(this.$t('HELP_CENTER.ARTICLE_SEARCH.SUCCESS_ARTICLE_INSERTED'));
|
||||
this.onClose();
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Escape: {
|
||||
action: () => {
|
||||
this.onClose();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { getRegexp } from 'shared/helpers/Validators';
|
||||
import { ATTRIBUTE_TYPES } from './constants';
|
||||
import customAttributeMixin from '../../../../mixins/customAttributeMixin';
|
||||
export default {
|
||||
components: {},
|
||||
mixins: [customAttributeMixin],
|
||||
props: {
|
||||
selectedAttribute: {
|
||||
type: Object,
|
||||
@@ -116,7 +115,7 @@ export default {
|
||||
},
|
||||
setFormValues() {
|
||||
const regexPattern = this.selectedAttribute.regex_pattern
|
||||
? this.getRegexp(this.selectedAttribute.regex_pattern).source
|
||||
? getRegexp(this.selectedAttribute.regex_pattern).source
|
||||
: null;
|
||||
this.displayName = this.selectedAttribute.attribute_display_name;
|
||||
this.description = this.selectedAttribute.attribute_description;
|
||||
|
||||
@@ -1,126 +1,123 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, watch, provide } from 'vue';
|
||||
import { useRoute, useRouter } from 'dashboard/composables/route';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
||||
import MacroForm from './MacroForm.vue';
|
||||
import { MACRO_ACTION_TYPES } from './constants';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import actionQueryGenerator from 'dashboard/helper/actionQueryGenerator.js';
|
||||
import macrosMixin from 'dashboard/mixins/macrosMixin';
|
||||
import { useMacros } from 'dashboard/composables/useMacros';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MacroForm,
|
||||
},
|
||||
mixins: [macrosMixin],
|
||||
provide() {
|
||||
return {
|
||||
macroActionTypes: this.macroActionTypes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
macro: null,
|
||||
mode: 'CREATE',
|
||||
macroActionTypes: MACRO_ACTION_TYPES,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'macros/getUIFlags',
|
||||
}),
|
||||
macroId() {
|
||||
return this.$route.params.macroId;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler() {
|
||||
this.fetchDropdownData();
|
||||
if (this.$route.params.macroId) {
|
||||
this.fetchMacro();
|
||||
} else {
|
||||
this.initNewMacro();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchDropdownData() {
|
||||
this.$store.dispatch('agents/get');
|
||||
this.$store.dispatch('teams/get');
|
||||
this.$store.dispatch('labels/get');
|
||||
},
|
||||
fetchMacro() {
|
||||
this.mode = 'EDIT';
|
||||
this.manifestMacro();
|
||||
},
|
||||
async manifestMacro() {
|
||||
await this.$store.dispatch('macros/getSingleMacro', this.macroId);
|
||||
const singleMacro = this.$store.getters['macros/getMacro'](this.macroId);
|
||||
this.macro = this.formatMacro(singleMacro);
|
||||
},
|
||||
formatMacro(macro) {
|
||||
const formattedActions = macro.actions.map(action => {
|
||||
let actionParams = [];
|
||||
if (action.action_params.length) {
|
||||
const inputType = this.macroActionTypes.find(
|
||||
item => item.key === action.action_name
|
||||
).inputType;
|
||||
if (inputType === 'multi_select' || inputType === 'search_select') {
|
||||
actionParams = [
|
||||
...this.getDropdownValues(action.action_name, this.$store),
|
||||
].filter(item => [...action.action_params].includes(item.id));
|
||||
} else if (inputType === 'team_message') {
|
||||
actionParams = {
|
||||
team_ids: [
|
||||
...this.getDropdownValues(action.action_name, this.$store),
|
||||
].filter(item =>
|
||||
[...action.action_params[0].team_ids].includes(item.id)
|
||||
),
|
||||
message: action.action_params[0].message,
|
||||
};
|
||||
} else actionParams = [...action.action_params];
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
action_params: actionParams,
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
|
||||
const macro = ref(null);
|
||||
const mode = ref('CREATE');
|
||||
const macroActionTypes = MACRO_ACTION_TYPES;
|
||||
|
||||
provide('macroActionTypes', macroActionTypes);
|
||||
|
||||
const uiFlags = computed(() => getters['macros/getUIFlags'].value);
|
||||
const macroId = computed(() => route.params.macroId);
|
||||
|
||||
const fetchDropdownData = () => {
|
||||
store.dispatch('agents/get');
|
||||
store.dispatch('teams/get');
|
||||
store.dispatch('labels/get');
|
||||
};
|
||||
|
||||
const formatMacro = macroData => {
|
||||
const formattedActions = macroData.actions.map(action => {
|
||||
let actionParams = [];
|
||||
if (action.action_params.length) {
|
||||
const inputType = macroActionTypes.find(
|
||||
item => item.key === action.action_name
|
||||
).inputType;
|
||||
if (inputType === 'multi_select' || inputType === 'search_select') {
|
||||
actionParams = getMacroDropdownValues(action.action_name).filter(item =>
|
||||
[...action.action_params].includes(item.id)
|
||||
);
|
||||
} else if (inputType === 'team_message') {
|
||||
actionParams = {
|
||||
team_ids: getMacroDropdownValues(action.action_name).filter(item =>
|
||||
[...action.action_params[0].team_ids].includes(item.id)
|
||||
),
|
||||
message: action.action_params[0].message,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...macro,
|
||||
actions: formattedActions,
|
||||
};
|
||||
},
|
||||
initNewMacro() {
|
||||
this.mode = 'CREATE';
|
||||
this.macro = {
|
||||
name: '',
|
||||
actions: [
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [],
|
||||
},
|
||||
],
|
||||
visibility: 'global',
|
||||
};
|
||||
},
|
||||
async saveMacro(macro) {
|
||||
try {
|
||||
const action = this.mode === 'EDIT' ? 'macros/update' : 'macros/create';
|
||||
let successMessage =
|
||||
this.mode === 'EDIT'
|
||||
? this.$t('MACROS.EDIT.API.SUCCESS_MESSAGE')
|
||||
: this.$t('MACROS.ADD.API.SUCCESS_MESSAGE');
|
||||
let serializedMacro = JSON.parse(JSON.stringify(macro));
|
||||
serializedMacro.actions = actionQueryGenerator(serializedMacro.actions);
|
||||
await this.$store.dispatch(action, serializedMacro);
|
||||
useAlert(successMessage);
|
||||
this.$router.push({ name: 'macros_wrapper' });
|
||||
} catch (error) {
|
||||
useAlert(this.$t('MACROS.ERROR'));
|
||||
}
|
||||
},
|
||||
} else actionParams = [...action.action_params];
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
action_params: actionParams,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...macroData,
|
||||
actions: formattedActions,
|
||||
};
|
||||
};
|
||||
|
||||
const manifestMacro = async () => {
|
||||
await store.dispatch('macros/getSingleMacro', macroId.value);
|
||||
const singleMacro = store.getters['macros/getMacro'](macroId.value);
|
||||
macro.value = formatMacro(singleMacro);
|
||||
};
|
||||
|
||||
const fetchMacro = () => {
|
||||
mode.value = 'EDIT';
|
||||
manifestMacro();
|
||||
};
|
||||
|
||||
const initNewMacro = () => {
|
||||
mode.value = 'CREATE';
|
||||
macro.value = {
|
||||
name: '',
|
||||
actions: [
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [],
|
||||
},
|
||||
],
|
||||
visibility: 'global',
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route,
|
||||
() => {
|
||||
fetchDropdownData();
|
||||
if (route.params.macroId) {
|
||||
fetchMacro();
|
||||
} else {
|
||||
initNewMacro();
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const saveMacro = async macroData => {
|
||||
try {
|
||||
const action = mode.value === 'EDIT' ? 'macros/update' : 'macros/create';
|
||||
const successMessage =
|
||||
mode.value === 'EDIT'
|
||||
? t('MACROS.EDIT.API.SUCCESS_MESSAGE')
|
||||
: t('MACROS.ADD.API.SUCCESS_MESSAGE');
|
||||
let serializedMacro = JSON.parse(JSON.stringify(macroData));
|
||||
serializedMacro.actions = actionQueryGenerator(serializedMacro.actions);
|
||||
await store.dispatch(action, serializedMacro);
|
||||
useAlert(successMessage);
|
||||
router.push({ name: 'macros_wrapper' });
|
||||
} catch (error) {
|
||||
useAlert(t('MACROS.ERROR'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -128,11 +125,12 @@ export default {
|
||||
<div class="flex flex-col flex-1 h-full overflow-auto">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetchingItem"
|
||||
:message="$t('MACROS.EDITOR.LOADING')"
|
||||
:message="t('MACROS.EDITOR.LOADING')"
|
||||
/>
|
||||
<MacroForm
|
||||
v-if="macro && !uiFlags.isFetchingItem"
|
||||
:macro-data.sync="macro"
|
||||
:macro-data="macro"
|
||||
@update:macro-data="macro = $event"
|
||||
@submit="saveMacro"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,84 +1,80 @@
|
||||
<script>
|
||||
import { inject } from 'vue';
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { useMacros } from 'dashboard/composables/useMacros';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import ActionInput from 'dashboard/components/widgets/AutomationActionInput.vue';
|
||||
import macrosMixin from 'dashboard/mixins/macrosMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActionInput,
|
||||
const props = defineProps({
|
||||
singleNode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
mixins: [macrosMixin],
|
||||
props: {
|
||||
singleNode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errorKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
setup() {
|
||||
const macroActionTypes = inject('macroActionTypes');
|
||||
return { macroActionTypes };
|
||||
errorKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
computed: {
|
||||
actionData: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value);
|
||||
},
|
||||
},
|
||||
errorMessage() {
|
||||
if (!this.errorKey) return '';
|
||||
fileName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
return this.$t(`MACROS.ERRORS.${this.errorKey}`);
|
||||
},
|
||||
showActionInput() {
|
||||
if (
|
||||
this.actionData.action_name === 'send_email_to_team' ||
|
||||
this.actionData.action_name === 'send_message'
|
||||
)
|
||||
return false;
|
||||
const type = this.macroActionTypes.find(
|
||||
action => action.key === this.actionData.action_name
|
||||
).inputType;
|
||||
return !!type;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dropdownValues() {
|
||||
return this.getDropdownValues(this.value.action_name, this.$store);
|
||||
},
|
||||
},
|
||||
const emit = defineEmits(['input', 'resetAction', 'deleteNode']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const macroActionTypes = inject('macroActionTypes');
|
||||
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
|
||||
const actionData = computed({
|
||||
get: () => props.value,
|
||||
set: value => emit('input', value),
|
||||
});
|
||||
|
||||
const errorMessage = computed(() => {
|
||||
if (!props.errorKey) return '';
|
||||
return t(`MACROS.ERRORS.${props.errorKey}`);
|
||||
});
|
||||
|
||||
const showActionInput = computed(() => {
|
||||
if (
|
||||
actionData.value.action_name === 'send_email_to_team' ||
|
||||
actionData.value.action_name === 'send_message'
|
||||
)
|
||||
return false;
|
||||
const type = macroActionTypes.find(
|
||||
action => action.key === actionData.value.action_name
|
||||
).inputType;
|
||||
return !!type;
|
||||
});
|
||||
|
||||
const dropdownValues = () => {
|
||||
return getMacroDropdownValues(props.value.action_name);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="macro__node-action-container">
|
||||
<div class="relative flex items-center w-full min-w-0 basis-full">
|
||||
<woot-button
|
||||
v-if="!singleNode"
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="navigation"
|
||||
class="macros__node-drag-handle"
|
||||
class="absolute cursor-move -left-8 macros__node-drag-handle"
|
||||
/>
|
||||
<div
|
||||
class="macro__node-action-item"
|
||||
:class="{
|
||||
'has-error': errorKey,
|
||||
}"
|
||||
class="flex-grow p-2 mr-2 rounded-md shadow-sm"
|
||||
:class="
|
||||
errorKey
|
||||
? 'bg-red-50 animate-shake dark:bg-red-800'
|
||||
: 'bg-white dark:bg-slate-700'
|
||||
"
|
||||
>
|
||||
<ActionInput
|
||||
v-model="actionData"
|
||||
@@ -103,39 +99,3 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.macros__node-drag-handle {
|
||||
@apply cursor-move -left-8 absolute;
|
||||
}
|
||||
.macro__node-action-container {
|
||||
@apply w-full min-w-0 basis-full items-center flex relative;
|
||||
|
||||
.macro__node-action-item {
|
||||
@apply flex-grow bg-white dark:bg-slate-700 p-2 mr-2 rounded-md shadow-sm;
|
||||
|
||||
&.has-error {
|
||||
animation: shake 0.3s ease-in-out 0s 2;
|
||||
@apply bg-red-50 dark:bg-red-800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(0.234375rem);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-0.234375rem);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(0.234375rem);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,59 +1,63 @@
|
||||
<script>
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
export default {
|
||||
name: 'WootDropdownMenu',
|
||||
componentName: 'WootDropdownMenu',
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'top',
|
||||
},
|
||||
defineProps({
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'top',
|
||||
},
|
||||
methods: {
|
||||
dropdownMenuButtons() {
|
||||
return this.$refs.dropdownMenu.querySelectorAll(
|
||||
'ul.dropdown li.dropdown-menu__item .button'
|
||||
);
|
||||
},
|
||||
getActiveButtonIndex(menuButtons) {
|
||||
const focusedButton = this.$refs.dropdownMenu.querySelector(
|
||||
'ul.dropdown li.dropdown-menu__item .button:focus'
|
||||
);
|
||||
return Array.from(menuButtons).indexOf(focusedButton);
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
const menuButtons = this.dropdownMenuButtons();
|
||||
return {
|
||||
ArrowUp: () => this.focusPreviousButton(menuButtons),
|
||||
ArrowDown: () => this.focusNextButton(menuButtons),
|
||||
};
|
||||
},
|
||||
focusPreviousButton(menuButtons) {
|
||||
const activeIndex = this.getActiveButtonIndex(menuButtons);
|
||||
const newIndex =
|
||||
activeIndex >= 1 ? activeIndex - 1 : menuButtons.length - 1;
|
||||
this.focusButton(menuButtons, newIndex);
|
||||
},
|
||||
focusNextButton(menuButtons) {
|
||||
const activeIndex = this.getActiveButtonIndex(menuButtons);
|
||||
const newIndex =
|
||||
activeIndex === menuButtons.length - 1 ? 0 : activeIndex + 1;
|
||||
this.focusButton(menuButtons, newIndex);
|
||||
},
|
||||
focusButton(menuButtons, newIndex) {
|
||||
if (menuButtons.length === 0) return;
|
||||
menuButtons[newIndex].focus();
|
||||
},
|
||||
});
|
||||
|
||||
const dropdownMenuRef = ref(null);
|
||||
|
||||
const dropdownMenuButtons = () => {
|
||||
return dropdownMenuRef.value.querySelectorAll(
|
||||
'ul.dropdown li.dropdown-menu__item .button'
|
||||
);
|
||||
};
|
||||
|
||||
const getActiveButtonIndex = menuButtons => {
|
||||
const focusedButton = dropdownMenuRef.value.querySelector(
|
||||
'ul.dropdown li.dropdown-menu__item .button:focus'
|
||||
);
|
||||
return Array.from(menuButtons).indexOf(focusedButton);
|
||||
};
|
||||
|
||||
const focusButton = (menuButtons, newIndex) => {
|
||||
if (menuButtons.length === 0) return;
|
||||
menuButtons[newIndex].focus();
|
||||
};
|
||||
|
||||
const focusPreviousButton = menuButtons => {
|
||||
const activeIndex = getActiveButtonIndex(menuButtons);
|
||||
const newIndex = activeIndex >= 1 ? activeIndex - 1 : menuButtons.length - 1;
|
||||
focusButton(menuButtons, newIndex);
|
||||
};
|
||||
|
||||
const focusNextButton = menuButtons => {
|
||||
const activeIndex = getActiveButtonIndex(menuButtons);
|
||||
const newIndex = activeIndex === menuButtons.length - 1 ? 0 : activeIndex + 1;
|
||||
focusButton(menuButtons, newIndex);
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
ArrowUp: {
|
||||
action: () => focusPreviousButton(dropdownMenuButtons()),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
ArrowDown: {
|
||||
action: () => focusNextButton(dropdownMenuButtons()),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
|
||||
useKeyboardEvents(keyboardEvents, dropdownMenuRef);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
ref="dropdownMenu"
|
||||
ref="dropdownMenuRef"
|
||||
class="dropdown menu vertical"
|
||||
:class="[placement && `dropdown--${placement}`]"
|
||||
>
|
||||
|
||||
@@ -1,22 +1,58 @@
|
||||
/**
|
||||
* Checks if a string is a valid E.164 phone number format.
|
||||
* @param {string} value - The phone number to validate.
|
||||
* @returns {boolean} True if the number is in E.164 format, false otherwise.
|
||||
*/
|
||||
export const isPhoneE164 = value => !!value.match(/^\+[1-9]\d{1,14}$/);
|
||||
|
||||
/**
|
||||
* Validates a phone number after removing the dial code.
|
||||
* @param {string} value - The full phone number including dial code.
|
||||
* @param {string} dialCode - The dial code to remove before validation.
|
||||
* @returns {boolean} True if the number (without dial code) is valid, false otherwise.
|
||||
*/
|
||||
export const isPhoneNumberValid = (value, dialCode) => {
|
||||
const number = value.replace(dialCode, '');
|
||||
return !!number.match(/^[0-9]{1,14}$/);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a string is either a valid E.164 phone number or empty.
|
||||
* @param {string} value - The phone number to validate.
|
||||
* @returns {boolean} True if the number is in E.164 format or empty, false otherwise.
|
||||
*/
|
||||
export const isPhoneE164OrEmpty = value => isPhoneE164(value) || value === '';
|
||||
|
||||
/**
|
||||
* Validates a phone number with dial code, requiring at least 5 digits.
|
||||
* @param {string} value - The full phone number including dial code.
|
||||
* @returns {boolean} True if the number is valid, false otherwise.
|
||||
*/
|
||||
export const isPhoneNumberValidWithDialCode = value => {
|
||||
const number = value.replace(/^\+/, ''); // Remove the '+' sign
|
||||
return !!number.match(/^[1-9]\d{4,}$/); // Validate the phone number with minimum 5 digits
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a string starts with a plus sign.
|
||||
* @param {string} value - The string to check.
|
||||
* @returns {boolean} True if the string starts with '+', false otherwise.
|
||||
*/
|
||||
export const startsWithPlus = value => value.startsWith('+');
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid URL (starts with 'http') or is empty.
|
||||
* @param {string} [value=''] - The string to check.
|
||||
* @returns {boolean} True if the string is a valid URL or empty, false otherwise.
|
||||
*/
|
||||
export const shouldBeUrl = (value = '') =>
|
||||
value ? value.startsWith('http') : true;
|
||||
|
||||
/**
|
||||
* Validates a password for complexity requirements.
|
||||
* @param {string} value - The password to validate.
|
||||
* @returns {boolean} True if the password meets all requirements, false otherwise.
|
||||
*/
|
||||
export const isValidPassword = value => {
|
||||
const containsUppercase = /[A-Z]/.test(value);
|
||||
const containsLowercase = /[a-z]/.test(value);
|
||||
@@ -32,8 +68,18 @@ export const isValidPassword = value => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a string consists only of digits.
|
||||
* @param {string} value - The string to check.
|
||||
* @returns {boolean} True if the string contains only digits, false otherwise.
|
||||
*/
|
||||
export const isNumber = value => /^\d+$/.test(value);
|
||||
|
||||
/**
|
||||
* Validates a domain name.
|
||||
* @param {string} value - The domain name to validate.
|
||||
* @returns {boolean} True if the domain is valid or empty, false otherwise.
|
||||
*/
|
||||
export const isDomain = value => {
|
||||
if (value !== '') {
|
||||
const domainRegex = /^([\p{L}0-9]+(-[\p{L}0-9]+)*\.)+[a-z]{2,}$/gmu;
|
||||
@@ -41,3 +87,16 @@ export const isDomain = value => {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a RegExp object from a string representation of a regular expression.
|
||||
* @param {string} regexPatternValue - The string representation of the regex (e.g., '/pattern/flags').
|
||||
* @returns {RegExp} A RegExp object created from the input string.
|
||||
*/
|
||||
export const getRegexp = regexPatternValue => {
|
||||
let lastSlash = regexPatternValue.lastIndexOf('/');
|
||||
return new RegExp(
|
||||
regexPatternValue.slice(1, lastSlash),
|
||||
regexPatternValue.slice(lastSlash + 1)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isPhoneNumberValid,
|
||||
isNumber,
|
||||
isDomain,
|
||||
getRegexp,
|
||||
} from '../Validators';
|
||||
|
||||
describe('#shouldBeUrl', () => {
|
||||
@@ -115,3 +116,40 @@ describe('#startsWithPlus', () => {
|
||||
expect(startsWithPlus('123456789')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getRegexp', () => {
|
||||
it('should create a correct RegExp object', () => {
|
||||
const regexPattern = '/^[a-z]+$/i';
|
||||
const regex = getRegexp(regexPattern);
|
||||
|
||||
expect(regex).toBeInstanceOf(RegExp);
|
||||
expect(regex.toString()).toBe(regexPattern);
|
||||
|
||||
expect(regex.test('abc')).toBe(true);
|
||||
expect(regex.test('ABC')).toBe(true);
|
||||
expect(regex.test('123')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle regex with flags', () => {
|
||||
const regexPattern = '/hello/gi';
|
||||
const regex = getRegexp(regexPattern);
|
||||
|
||||
expect(regex).toBeInstanceOf(RegExp);
|
||||
expect(regex.toString()).toBe(regexPattern);
|
||||
|
||||
expect(regex.test('hello')).toBe(true);
|
||||
expect(regex.test('HELLO')).toBe(false);
|
||||
expect(regex.test('Hello World')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle regex with special characters', () => {
|
||||
const regexPattern = '/\\d{3}-\\d{2}-\\d{4}/';
|
||||
const regex = getRegexp(regexPattern);
|
||||
|
||||
expect(regex).toBeInstanceOf(RegExp);
|
||||
expect(regex.toString()).toBe(regexPattern);
|
||||
|
||||
expect(regex.test('123-45-6789')).toBe(true);
|
||||
expect(regex.test('12-34-5678')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import PlaygroundHeader from '../../components/playground/Header.vue';
|
||||
import UserMessage from '../../components/playground/UserMessage.vue';
|
||||
import BotMessage from '../../components/playground/BotMessage.vue';
|
||||
@@ -13,7 +12,7 @@ export default {
|
||||
BotMessage,
|
||||
TypingIndicator,
|
||||
},
|
||||
mixins: [messageFormatterMixin, keyboardEventListenerMixins],
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
componentData: {
|
||||
type: Object,
|
||||
@@ -35,16 +34,6 @@ export default {
|
||||
this.focusInput();
|
||||
},
|
||||
methods: {
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+Enter': {
|
||||
action: () => {
|
||||
this.onMessageSend();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
focusInput() {
|
||||
this.$refs.messageInput.focus();
|
||||
},
|
||||
@@ -133,6 +122,8 @@ export default {
|
||||
placeholder="Type a message... [CMD/CTRL + Enter to send]"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
@keydown.meta.enter="onMessageSend"
|
||||
@keydown.ctrl.enter="onMessageSend"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3,25 +3,19 @@ import CustomButton from 'shared/components/Button.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import { getRegexp } from 'shared/helpers/Validators';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import customAttributeMixin from '../../../dashboard/mixins/customAttributeMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CustomButton,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [
|
||||
routerMixin,
|
||||
darkModeMixin,
|
||||
messageFormatterMixin,
|
||||
configMixin,
|
||||
customAttributeMixin,
|
||||
],
|
||||
mixins: [routerMixin, darkModeMixin, messageFormatterMixin, configMixin],
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
@@ -184,7 +178,7 @@ export default {
|
||||
return this.formValues[name] || null;
|
||||
},
|
||||
getValidation({ type, name, field_type, regex_pattern }) {
|
||||
let regex = regex_pattern ? this.getRegexp(regex_pattern) : null;
|
||||
let regex = regex_pattern ? getRegexp(regex_pattern) : null;
|
||||
const validations = {
|
||||
emailAddress: 'email',
|
||||
phoneNumber: ['startsWithPlus', 'isValidPhoneNumber'],
|
||||
|
||||
@@ -54,6 +54,10 @@ module Linear::Queries
|
||||
title
|
||||
description
|
||||
identifier
|
||||
state {
|
||||
name
|
||||
color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,12 +66,19 @@ module.exports = {
|
||||
transform: 'translateX(1px)',
|
||||
},
|
||||
},
|
||||
shake: {
|
||||
'0%, 100%': { transform: 'translateX(0)' },
|
||||
'25%': { transform: 'translateX(0.234375rem)' },
|
||||
'50%': { transform: 'translateX(-0.234375rem)' },
|
||||
'75%': { transform: 'translateX(0.234375rem)' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
...defaultTheme.animation,
|
||||
wiggle: 'wiggle 0.5s ease-in-out',
|
||||
'loader-pulse': 'loader-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'card-select': 'card-select 0.25s ease-in-out',
|
||||
shake: 'shake 0.3s ease-in-out 0s 2',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
Reference in New Issue
Block a user