mirror of
https://github.com/lingble/chatwoot.git
synced 2026-03-19 19:52:42 +00:00
feat(v4): Compose a new conversation from a phone number. (#10568)
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { INPUT_TYPES } from 'dashboard/components-next/taginput/helper/tagInputHelper.js';
|
||||
|
||||
import TagInput from 'dashboard/components-next/taginput/TagInput.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
contacts: {
|
||||
type: Array,
|
||||
@@ -42,15 +44,19 @@ const props = defineProps({
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'searchContacts',
|
||||
'setSelectedContact',
|
||||
'clearSelectedContact',
|
||||
'updateDropdown',
|
||||
]);
|
||||
|
||||
const i18nPrefix = 'COMPOSE_NEW_CONVERSATION.FORM.CONTACT_SELECTOR';
|
||||
const { t } = useI18n();
|
||||
|
||||
const inputType = ref(INPUT_TYPES.EMAIL);
|
||||
|
||||
const contactsList = computed(() => {
|
||||
return props.contacts?.map(({ name, id, thumbnail, email, ...rest }) => ({
|
||||
id,
|
||||
@@ -80,6 +86,14 @@ const errorClass = computed(() => {
|
||||
? '[&_input]:placeholder:!text-n-ruby-9 [&_input]:dark:placeholder:!text-n-ruby-9'
|
||||
: '';
|
||||
});
|
||||
|
||||
const handleInput = value => {
|
||||
// Update input type based on whether input starts with '+'
|
||||
// If it does, set input type to 'tel'
|
||||
// Otherwise, set input type to 'email'
|
||||
inputType.value = value.startsWith('+') ? INPUT_TYPES.TEL : INPUT_TYPES.EMAIL;
|
||||
emit('searchContacts', value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -126,11 +140,11 @@ const errorClass = computed(() => {
|
||||
:is-loading="isLoading"
|
||||
:disabled="contactableInboxesList?.length > 0 && showInboxesDropdown"
|
||||
allow-create
|
||||
type="email"
|
||||
:type="inputType"
|
||||
class="flex-1 min-h-7"
|
||||
:class="errorClass"
|
||||
focus-on-mount
|
||||
@input="emit('searchContacts', $event)"
|
||||
@input="handleInput"
|
||||
@on-click-outside="emit('updateDropdown', 'contacts', false)"
|
||||
@add="emit('setSelectedContact', $event)"
|
||||
@remove="emit('clearSelectedContact')"
|
||||
|
||||
@@ -193,10 +193,12 @@ export const searchContacts = async ({ keys, query }) => {
|
||||
return filteredPayload || [];
|
||||
};
|
||||
|
||||
export const createNewContact = async email => {
|
||||
export const createNewContact = async input => {
|
||||
const payload = {
|
||||
name: getCapitalizedNameFromEmail(email),
|
||||
email,
|
||||
name: input.startsWith('+')
|
||||
? input.slice(1) // Remove the '+' prefix if it exists
|
||||
: getCapitalizedNameFromEmail(input),
|
||||
...(input.startsWith('+') ? { phone_number: input } : { email: input }),
|
||||
};
|
||||
|
||||
const {
|
||||
|
||||
@@ -429,6 +429,28 @@ describe('composeConversationHelper', () => {
|
||||
email: 'john@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates new contact with phone number', async () => {
|
||||
const mockContact = {
|
||||
id: 1,
|
||||
name: '919999999999',
|
||||
phone_number: '+919999999999',
|
||||
};
|
||||
ContactAPI.create.mockResolvedValue({
|
||||
data: { payload: { contact: mockContact } },
|
||||
});
|
||||
|
||||
const result = await helpers.createNewContact('+919999999999');
|
||||
expect(result).toEqual({
|
||||
id: 1,
|
||||
name: '919999999999',
|
||||
phoneNumber: '+919999999999',
|
||||
});
|
||||
expect(ContactAPI.create).toHaveBeenCalledWith({
|
||||
name: '919999999999',
|
||||
phone_number: '+919999999999',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchContactableInboxes', () => {
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { email } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import {
|
||||
MODE,
|
||||
INPUT_TYPES,
|
||||
getValidationRules,
|
||||
checkTagTypeValidity,
|
||||
buildTagMenuItems,
|
||||
canAddTag,
|
||||
findMatchingMenuItem,
|
||||
} from './helper/tagInputHelper';
|
||||
|
||||
const props = defineProps({
|
||||
placeholder: { type: String, default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
type: { type: String, default: 'text' },
|
||||
type: { type: String, default: INPUT_TYPES.TEXT },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
menuItems: {
|
||||
type: Array,
|
||||
@@ -23,8 +31,8 @@ const props = defineProps({
|
||||
showDropdown: { type: Boolean, default: false },
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'multiple',
|
||||
validator: value => ['single', 'multiple'].includes(value),
|
||||
default: MODE.MULTIPLE,
|
||||
validator: value => [MODE.SINGLE, MODE.MULTIPLE].includes(value),
|
||||
},
|
||||
focusOnMount: { type: Boolean, default: false },
|
||||
allowCreate: { type: Boolean, default: false },
|
||||
@@ -45,22 +53,17 @@ const modelValue = defineModel({
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
const MODE = {
|
||||
SINGLE: 'single',
|
||||
MULTIPLE: 'multiple',
|
||||
};
|
||||
|
||||
const tagInputRef = ref(null);
|
||||
const tags = ref(props.modelValue);
|
||||
const newTag = ref('');
|
||||
const isFocused = ref(true);
|
||||
|
||||
const rules = computed(() => ({
|
||||
newTag: props.type === 'email' ? { email } : {},
|
||||
}));
|
||||
|
||||
const rules = computed(() => getValidationRules(props.type));
|
||||
const v$ = useVuelidate(rules, { newTag });
|
||||
const isNewTagInValidType = computed(() => v$.value.$invalid);
|
||||
|
||||
const isNewTagInValidType = computed(() =>
|
||||
checkTagTypeValidity(props.type, newTag.value, v$.value)
|
||||
);
|
||||
|
||||
const showInput = computed(() =>
|
||||
props.mode === MODE.SINGLE
|
||||
@@ -74,68 +77,49 @@ const showDropdownMenu = computed(() =>
|
||||
: props.showDropdown
|
||||
);
|
||||
|
||||
const filteredMenuItems = computed(() => {
|
||||
if (props.mode === MODE.SINGLE && tags.value.length >= 1) return [];
|
||||
|
||||
const availableMenuItems = props.menuItems.filter(
|
||||
item => !tags.value.includes(item.label)
|
||||
);
|
||||
|
||||
// Show typed value as suggestion only if:
|
||||
// 1. There's a value being typed
|
||||
// 2. The value isn't already in the tags
|
||||
// 3. Email validation passes (if type is email) and There are no menu items available
|
||||
const trimmedNewTag = newTag.value?.trim();
|
||||
const shouldShowTypedValue =
|
||||
trimmedNewTag &&
|
||||
!tags.value.includes(trimmedNewTag) &&
|
||||
!props.isLoading &&
|
||||
!availableMenuItems.length &&
|
||||
(props.type === 'email' ? !isNewTagInValidType.value : true);
|
||||
|
||||
if (shouldShowTypedValue) {
|
||||
return [
|
||||
{
|
||||
label: trimmedNewTag,
|
||||
value: trimmedNewTag,
|
||||
email: trimmedNewTag,
|
||||
thumbnail: { name: trimmedNewTag, src: '' },
|
||||
action: 'create',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return availableMenuItems;
|
||||
});
|
||||
|
||||
const emitDataOnAdd = emailValue => {
|
||||
const matchingMenuItem = props.menuItems.find(
|
||||
item => item.email === emailValue
|
||||
);
|
||||
const filteredMenuItems = computed(() =>
|
||||
buildTagMenuItems({
|
||||
mode: props.mode,
|
||||
tags: tags.value,
|
||||
menuItems: props.menuItems,
|
||||
newTag: newTag.value,
|
||||
isLoading: props.isLoading,
|
||||
type: props.type,
|
||||
isNewTagInValidType: isNewTagInValidType.value,
|
||||
})
|
||||
);
|
||||
|
||||
const emitDataOnAdd = value => {
|
||||
const matchingMenuItem = findMatchingMenuItem(props.menuItems, value);
|
||||
return matchingMenuItem
|
||||
? emit('add', { email: emailValue, ...matchingMenuItem })
|
||||
: emit('add', { value: emailValue, action: 'create' });
|
||||
? emit('add', { value: value, ...matchingMenuItem })
|
||||
: emit('add', { value: value, action: 'create' });
|
||||
};
|
||||
|
||||
const updateValueAndFocus = value => {
|
||||
tags.value.push(value);
|
||||
newTag.value = '';
|
||||
modelValue.value = tags.value;
|
||||
tagInputRef.value?.focus();
|
||||
};
|
||||
|
||||
const addTag = async () => {
|
||||
const trimmedTag = newTag.value?.trim();
|
||||
if (!trimmedTag) return;
|
||||
|
||||
if (props.mode === MODE.SINGLE && tags.value.length >= 1) {
|
||||
if (!canAddTag(props.mode, tags.value.length)) {
|
||||
newTag.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.type === 'email' || props.allowCreate) {
|
||||
if (
|
||||
[INPUT_TYPES.EMAIL, INPUT_TYPES.TEL].includes(props.type) ||
|
||||
props.allowCreate
|
||||
) {
|
||||
if (!(await v$.value.$validate())) return;
|
||||
emitDataOnAdd(trimmedTag);
|
||||
}
|
||||
|
||||
tags.value.push(trimmedTag);
|
||||
newTag.value = '';
|
||||
modelValue.value = tags.value;
|
||||
tagInputRef.value?.focus();
|
||||
updateValueAndFocus(trimmedTag);
|
||||
};
|
||||
|
||||
const removeTag = index => {
|
||||
@@ -144,19 +128,25 @@ const removeTag = index => {
|
||||
emit('remove');
|
||||
};
|
||||
|
||||
const handleDropdownAction = async ({ email: emailAddress, ...rest }) => {
|
||||
const handleDropdownAction = async ({
|
||||
email: emailAddress,
|
||||
phoneNumber,
|
||||
...rest
|
||||
}) => {
|
||||
if (props.mode === MODE.SINGLE && tags.value.length >= 1) return;
|
||||
if (!props.showDropdown) return;
|
||||
|
||||
if (props.type === 'email' && props.showDropdown) {
|
||||
newTag.value = emailAddress;
|
||||
if (!(await v$.value.$validate())) return;
|
||||
emit('add', { email: emailAddress, ...rest });
|
||||
}
|
||||
const isEmail = props.type === 'email';
|
||||
newTag.value = isEmail ? emailAddress : phoneNumber;
|
||||
|
||||
tags.value.push(emailAddress);
|
||||
newTag.value = '';
|
||||
modelValue.value = tags.value;
|
||||
tagInputRef.value?.focus();
|
||||
if (!(await v$.value.$validate())) return;
|
||||
|
||||
const payload = isEmail
|
||||
? { email: emailAddress, ...rest }
|
||||
: { phoneNumber, ...rest };
|
||||
|
||||
emit('add', payload);
|
||||
updateValueAndFocus(emailAddress);
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
@@ -226,7 +216,6 @@ const handleBlur = e => emit('blur', e);
|
||||
ref="tagInputRef"
|
||||
v-model="newTag"
|
||||
:placeholder="placeholder"
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
class="w-full"
|
||||
:focus-on-mount="focusOnMount"
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
import {
|
||||
validatePhoneNumber,
|
||||
formatPhoneNumber,
|
||||
buildTagMenuItems,
|
||||
MODE,
|
||||
INPUT_TYPES,
|
||||
getValidationRules,
|
||||
validateAndFormatNewTag,
|
||||
createNewTagMenuItem,
|
||||
canAddTag,
|
||||
findMatchingMenuItem,
|
||||
} from '../tagInputHelper';
|
||||
import { email } from '@vuelidate/validators';
|
||||
|
||||
describe('tagInputHelper', () => {
|
||||
describe('validatePhoneNumber', () => {
|
||||
it('returns true for empty value', () => {
|
||||
expect(validatePhoneNumber('')).toBe(true);
|
||||
});
|
||||
|
||||
it('validates correct phone number', () => {
|
||||
expect(validatePhoneNumber('+918283838283')).toBe(true);
|
||||
});
|
||||
|
||||
it('validates correct phone number id + is present and number is not valid', () => {
|
||||
expect(validatePhoneNumber('+91828383834283')).toBe(false);
|
||||
});
|
||||
|
||||
it('validates correct phone number if + is not present', () => {
|
||||
expect(validatePhoneNumber('91828383834283')).toBe(false);
|
||||
});
|
||||
|
||||
it('invalidates incorrect phone number', () => {
|
||||
expect(validatePhoneNumber('invalid')).toBe(false);
|
||||
});
|
||||
|
||||
it('handles null value', () => {
|
||||
expect(validatePhoneNumber(null)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPhoneNumber', () => {
|
||||
it('formats valid phone number', () => {
|
||||
const result = formatPhoneNumber('+918283838283');
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.formattedValue).toBe('+91 82838 38283');
|
||||
});
|
||||
|
||||
it('handles invalid phone number', () => {
|
||||
const result = formatPhoneNumber('invalid');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.formattedValue).toBe('invalid');
|
||||
});
|
||||
|
||||
it('handles error case', () => {
|
||||
const result = formatPhoneNumber(null);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.formattedValue).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidationRules', () => {
|
||||
it('returns email validation for email type', () => {
|
||||
const rules = getValidationRules(INPUT_TYPES.EMAIL);
|
||||
expect(rules.newTag).toHaveProperty('email');
|
||||
expect(rules.newTag).not.toHaveProperty('isValidPhone');
|
||||
expect(rules.newTag.email).toBe(email);
|
||||
});
|
||||
|
||||
it('returns phone validation for tel type', () => {
|
||||
const rules = getValidationRules(INPUT_TYPES.TEL);
|
||||
expect(rules.newTag).toHaveProperty('isValidPhone');
|
||||
expect(rules.newTag).not.toHaveProperty('email');
|
||||
expect(rules.newTag.isValidPhone).toBe(validatePhoneNumber);
|
||||
});
|
||||
|
||||
it('returns empty rules for text type', () => {
|
||||
const rules = getValidationRules(INPUT_TYPES.TEXT);
|
||||
expect(Object.keys(rules.newTag)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAndFormatNewTag', () => {
|
||||
it('validates and formats email tag', () => {
|
||||
const result = validateAndFormatNewTag(
|
||||
'test@example.com',
|
||||
INPUT_TYPES.EMAIL,
|
||||
false
|
||||
);
|
||||
expect(result).toEqual({
|
||||
isValid: true,
|
||||
formattedValue: 'test@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('validates and formats phone tag', () => {
|
||||
const result = validateAndFormatNewTag(
|
||||
'+918283838283',
|
||||
INPUT_TYPES.TEL,
|
||||
false
|
||||
);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.formattedValue).toBe('+91 82838 38283');
|
||||
});
|
||||
|
||||
it('handles invalid email', () => {
|
||||
const result = validateAndFormatNewTag(
|
||||
'test@example.com',
|
||||
INPUT_TYPES.EMAIL,
|
||||
true
|
||||
);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.formattedValue).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('handles text type', () => {
|
||||
const result = validateAndFormatNewTag(
|
||||
'sample text',
|
||||
INPUT_TYPES.TEXT,
|
||||
false
|
||||
);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.formattedValue).toBe('sample text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewTagMenuItem', () => {
|
||||
it('creates email menu item', () => {
|
||||
const result = createNewTagMenuItem(
|
||||
'test@example.com',
|
||||
'test@example.com',
|
||||
INPUT_TYPES.EMAIL
|
||||
);
|
||||
expect(result).toEqual({
|
||||
label: 'test@example.com',
|
||||
value: 'test@example.com',
|
||||
email: 'test@example.com',
|
||||
thumbnail: { name: 'test@example.com', src: '' },
|
||||
action: 'create',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates phone menu item', () => {
|
||||
const result = createNewTagMenuItem(
|
||||
'+91 82838 38283',
|
||||
'+918283838283',
|
||||
INPUT_TYPES.TEL
|
||||
);
|
||||
expect(result).toEqual({
|
||||
label: '+91 82838 38283',
|
||||
value: '+918283838283',
|
||||
phoneNumber: '+918283838283',
|
||||
thumbnail: { name: '+91 82838 38283', src: '' },
|
||||
action: 'create',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates text menu item', () => {
|
||||
const result = createNewTagMenuItem(
|
||||
'sample text',
|
||||
'sample text',
|
||||
INPUT_TYPES.TEXT
|
||||
);
|
||||
expect(result).toEqual({
|
||||
label: 'sample text',
|
||||
value: 'sample text',
|
||||
thumbnail: { name: 'sample text', src: '' },
|
||||
action: 'create',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTagMenuItems', () => {
|
||||
const baseParams = {
|
||||
mode: MODE.MULTIPLE,
|
||||
tags: [],
|
||||
menuItems: [],
|
||||
newTag: '',
|
||||
isLoading: false,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
isNewTagInValidType: false,
|
||||
};
|
||||
|
||||
it('returns empty array in single mode with existing tag', () => {
|
||||
const result = buildTagMenuItems({
|
||||
...baseParams,
|
||||
mode: MODE.SINGLE,
|
||||
tags: ['existing'],
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters out existing tags', () => {
|
||||
const result = buildTagMenuItems({
|
||||
...baseParams,
|
||||
menuItems: [
|
||||
{ label: 'item1', value: '1' },
|
||||
{ label: 'item2', value: '2' },
|
||||
],
|
||||
tags: ['item1'],
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('item2');
|
||||
});
|
||||
|
||||
it('creates new email item when valid', () => {
|
||||
const result = buildTagMenuItems({
|
||||
...baseParams,
|
||||
type: INPUT_TYPES.EMAIL,
|
||||
newTag: 'test@example.com',
|
||||
menuItems: [],
|
||||
});
|
||||
expect(result[0]).toMatchObject({
|
||||
label: 'test@example.com',
|
||||
email: 'test@example.com',
|
||||
action: 'create',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates new phone item when valid', () => {
|
||||
const result = buildTagMenuItems({
|
||||
...baseParams,
|
||||
type: INPUT_TYPES.TEL,
|
||||
newTag: '+918283838283',
|
||||
menuItems: [],
|
||||
});
|
||||
expect(result[0]).toMatchObject({
|
||||
value: '+918283838283',
|
||||
label: '+91 82838 38283',
|
||||
action: 'create',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty array when loading', () => {
|
||||
const result = buildTagMenuItems({
|
||||
...baseParams,
|
||||
isLoading: true,
|
||||
newTag: 'test',
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array for invalid tag', () => {
|
||||
const result = buildTagMenuItems({
|
||||
...baseParams,
|
||||
type: INPUT_TYPES.EMAIL,
|
||||
newTag: 'invalid-email',
|
||||
isNewTagInValidType: true,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns available menu items when no new tag', () => {
|
||||
const menuItems = [
|
||||
{ label: 'item1', value: '1' },
|
||||
{ label: 'item2', value: '2' },
|
||||
];
|
||||
const result = buildTagMenuItems({
|
||||
...baseParams,
|
||||
menuItems,
|
||||
});
|
||||
expect(result).toEqual(menuItems);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canAddTag', () => {
|
||||
it('prevents adding tags in single mode when tag exists', () => {
|
||||
expect(canAddTag(MODE.SINGLE, 1)).toBe(false);
|
||||
expect(canAddTag(MODE.SINGLE, 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('allows adding tags in multiple mode', () => {
|
||||
expect(canAddTag(MODE.MULTIPLE, 1)).toBe(true);
|
||||
expect(canAddTag(MODE.MULTIPLE, 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findMatchingMenuItem', () => {
|
||||
const menuItems = [
|
||||
{ email: 'test1@example.com', label: 'Test 1' },
|
||||
{ email: 'test2@example.com', label: 'Test 2' },
|
||||
];
|
||||
|
||||
it('finds matching menu item by email', () => {
|
||||
const result = findMatchingMenuItem(menuItems, 'test1@example.com');
|
||||
expect(result).toEqual(menuItems[0]);
|
||||
});
|
||||
|
||||
it('returns undefined when no match found', () => {
|
||||
const result = findMatchingMenuItem(menuItems, 'nonexistent@example.com');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles empty menu items', () => {
|
||||
const result = findMatchingMenuItem([], 'test@example.com');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { parsePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js';
|
||||
import { email } from '@vuelidate/validators';
|
||||
|
||||
export const MODE = {
|
||||
SINGLE: 'single',
|
||||
MULTIPLE: 'multiple',
|
||||
};
|
||||
|
||||
export const INPUT_TYPES = {
|
||||
EMAIL: 'email',
|
||||
TEL: 'tel',
|
||||
TEXT: 'text',
|
||||
};
|
||||
|
||||
export const validatePhoneNumber = value => {
|
||||
if (!value) return true;
|
||||
try {
|
||||
return isValidPhoneNumber(value);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatPhoneNumber = value => {
|
||||
try {
|
||||
const phoneNumber = parsePhoneNumber(value);
|
||||
return {
|
||||
isValid: phoneNumber?.isValid() || false,
|
||||
formattedValue: phoneNumber?.formatInternational() || value,
|
||||
};
|
||||
} catch (error) {
|
||||
return { isValid: false, formattedValue: value };
|
||||
}
|
||||
};
|
||||
|
||||
export const getValidationRules = type => ({
|
||||
newTag: {
|
||||
...(type === INPUT_TYPES.EMAIL ? { email } : {}),
|
||||
...(type === INPUT_TYPES.TEL ? { isValidPhone: validatePhoneNumber } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
export const checkTagTypeValidity = (type, value, v$) => {
|
||||
if (type === INPUT_TYPES.TEL) {
|
||||
return !validatePhoneNumber(value);
|
||||
}
|
||||
return v$.$invalid;
|
||||
};
|
||||
|
||||
export const validateAndFormatNewTag = (
|
||||
trimmedNewTag,
|
||||
type,
|
||||
isNewTagInValidType
|
||||
) => {
|
||||
let isValid = true;
|
||||
let formattedValue = trimmedNewTag;
|
||||
|
||||
if (type === INPUT_TYPES.EMAIL) {
|
||||
isValid = !isNewTagInValidType;
|
||||
} else if (type === INPUT_TYPES.TEL) {
|
||||
const { isValid: phoneValid, formattedValue: phoneFormatted } =
|
||||
formatPhoneNumber(trimmedNewTag);
|
||||
isValid = phoneValid;
|
||||
formattedValue = phoneFormatted;
|
||||
}
|
||||
|
||||
return { isValid, formattedValue };
|
||||
};
|
||||
|
||||
export const createNewTagMenuItem = (formattedValue, trimmedNewTag, type) => ({
|
||||
label: formattedValue,
|
||||
value: trimmedNewTag,
|
||||
...(type === INPUT_TYPES.EMAIL ? { email: trimmedNewTag } : {}),
|
||||
...(type === INPUT_TYPES.TEL ? { phoneNumber: trimmedNewTag } : {}),
|
||||
thumbnail: { name: formattedValue, src: '' },
|
||||
action: 'create',
|
||||
});
|
||||
|
||||
export const buildTagMenuItems = ({
|
||||
mode,
|
||||
tags,
|
||||
menuItems,
|
||||
newTag,
|
||||
isLoading,
|
||||
type,
|
||||
isNewTagInValidType,
|
||||
}) => {
|
||||
if (mode === MODE.SINGLE && tags.length >= 1) return [];
|
||||
|
||||
const availableMenuItems = menuItems.filter(
|
||||
item => !tags.includes(item.label)
|
||||
);
|
||||
|
||||
// Show typed value as suggestion only if:
|
||||
// 1. There's a value being typed
|
||||
// 2. The value isn't already in the tags
|
||||
// 3. Validation passes (email/phone) and There are no menu items available
|
||||
const trimmedNewTag = newTag?.trim();
|
||||
const shouldShowTypedValue =
|
||||
trimmedNewTag &&
|
||||
!tags.includes(trimmedNewTag) &&
|
||||
!isLoading &&
|
||||
!availableMenuItems.length;
|
||||
|
||||
if (shouldShowTypedValue) {
|
||||
const { isValid, formattedValue } = validateAndFormatNewTag(
|
||||
trimmedNewTag,
|
||||
type,
|
||||
isNewTagInValidType
|
||||
);
|
||||
|
||||
if (isValid) {
|
||||
return [createNewTagMenuItem(formattedValue, trimmedNewTag, type)];
|
||||
}
|
||||
}
|
||||
|
||||
return availableMenuItems;
|
||||
};
|
||||
|
||||
export const canAddTag = (mode, tagsLength) =>
|
||||
!(mode === MODE.SINGLE && tagsLength >= 1);
|
||||
|
||||
export const findMatchingMenuItem = (menuItems, value) =>
|
||||
menuItems.find(item => item.email === value);
|
||||
Reference in New Issue
Block a user