chore: Remove vue-multiselect and migrate to next components (#13506)
# Pull Request Template ## Description This PR includes: 1. Removes multiselect usage from the Merge Contact modal (Conversation sidebar) and replaces it with the existing component used on the Contact Details page. 2. Replaces legacy form and multiselect elements in Add and Edit automations flows with next components.**(Also check Macros)** 3. Replace multiselect with ComboBox in contact form country field. 4. Replace multiselect with TagInput in create/edit attribute form. 5. Replace multiselect with TagInput for agent selection in inbox creation. 6. Replace multiselect with ComboBox in Facebook channel page selection ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? **Screenshots** 1. **Merge modal** <img width="741" height="449" alt="image" src="https://github.com/user-attachments/assets/a05a96ec-0692-4d94-9e27-d3e85fd143e4" /> <img width="741" height="449" alt="image" src="https://github.com/user-attachments/assets/fc1dc977-689d-4440-869d-2124e4ca9083" /> 2. **Automations** <img width="849" height="1089" alt="image" src="https://github.com/user-attachments/assets/b0155f06-ab21-4f90-a2c8-5bfbd97b08f7" /> <img width="813" height="879" alt="image" src="https://github.com/user-attachments/assets/0921ac4a-88f5-49ac-a776-cc02941b479c" /> <img width="849" height="826" alt="image" src="https://github.com/user-attachments/assets/44358dae-a076-4e10-b7ba-a4e40ccd817f" /> 3. **Country field** <img width="462" height="483" alt="image" src="https://github.com/user-attachments/assets/d5db9aa1-b859-4327-9960-957d7091678f" /> 4. **Add/Edit attribute form** <img width="619" height="646" alt="image" src="https://github.com/user-attachments/assets/6ab2ea94-73e5-40b8-ac29-399c0543fa7b" /> <img width="619" height="646" alt="image" src="https://github.com/user-attachments/assets/b4c5bb0e-baa0-4ef7-a6a2-adb0f0203243" /> <img width="635" height="731" alt="image" src="https://github.com/user-attachments/assets/74890c80-b213-4567-bf5f-4789dda39d2d" /> 5. **Agent selection in inbox creation** <img width="635" height="534" alt="image" src="https://github.com/user-attachments/assets/0003bad1-1a75-4f20-b014-587e1c19a620" /> <img width="809" height="602" alt="image" src="https://github.com/user-attachments/assets/5e7ab635-7340-420a-a191-e6cd49c02704" /> 7. **Facebook channel page selection** <img width="597" height="444" alt="image" src="https://github.com/user-attachments/assets/f7ec8d84-0a7d-4bc6-92a1-a1365178e319" /> <img width="597" height="444" alt="image" src="https://github.com/user-attachments/assets/d0596c4d-94c1-4544-8b50-e7103ff207a6" /> <img width="597" height="444" alt="image" src="https://github.com/user-attachments/assets/be097921-011b-4dbe-b5f1-5d1306e25349" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
parent
138840a23f
commit
229f56d6e3
@ -68,8 +68,7 @@ const hasActiveSegments = computed(
|
|||||||
);
|
);
|
||||||
const activeSegmentName = computed(() => props.activeSegment?.name);
|
const activeSegmentName = computed(() => props.activeSegment?.name);
|
||||||
|
|
||||||
const openCreateNewContactDialog = async () => {
|
const openCreateNewContactDialog = () => {
|
||||||
await createNewContactDialogRef.value?.contactsFormRef.resetValidation();
|
|
||||||
createNewContactDialogRef.value?.dialogRef.open();
|
createNewContactDialogRef.value?.dialogRef.open();
|
||||||
};
|
};
|
||||||
const openContactImportDialog = () =>
|
const openContactImportDialog = () =>
|
||||||
|
|||||||
@ -95,6 +95,7 @@ const inputClass = computed(() => {
|
|||||||
:show-dropdown="showCcEmailsDropdown"
|
:show-dropdown="showCcEmailsDropdown"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
type="email"
|
type="email"
|
||||||
|
allow-create
|
||||||
class="flex-1 min-h-7"
|
class="flex-1 min-h-7"
|
||||||
@focus="emit('updateDropdown', 'cc', true)"
|
@focus="emit('updateDropdown', 'cc', true)"
|
||||||
@input="emit('searchCcEmails', $event)"
|
@input="emit('searchCcEmails', $event)"
|
||||||
@ -127,6 +128,7 @@ const inputClass = computed(() => {
|
|||||||
:show-dropdown="showBccEmailsDropdown"
|
:show-dropdown="showBccEmailsDropdown"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
type="email"
|
type="email"
|
||||||
|
allow-create
|
||||||
class="flex-1 min-h-7"
|
class="flex-1 min-h-7"
|
||||||
focus-on-mount
|
focus-on-mount
|
||||||
@focus="emit('updateDropdown', 'bcc', true)"
|
@focus="emit('updateDropdown', 'bcc', true)"
|
||||||
|
|||||||
@ -56,8 +56,13 @@ const selectedLabel = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const selectOption = option => {
|
const selectOption = option => {
|
||||||
selectedValue.value = option.value;
|
if (selectedValue.value === option.value) {
|
||||||
emit('update:modelValue', option.value);
|
selectedValue.value = '';
|
||||||
|
emit('update:modelValue', '');
|
||||||
|
} else {
|
||||||
|
selectedValue.value = option.value;
|
||||||
|
emit('update:modelValue', option.value);
|
||||||
|
}
|
||||||
open.value = false;
|
open.value = false;
|
||||||
search.value = '';
|
search.value = '';
|
||||||
};
|
};
|
||||||
|
|||||||
@ -53,6 +53,11 @@ const props = defineProps({
|
|||||||
default: 'lg',
|
default: 'lg',
|
||||||
validator: value => ['3xl', '2xl', 'xl', 'lg', 'md', 'sm'].includes(value),
|
validator: value => ['3xl', '2xl', 'xl', 'lg', 'md', 'sm'].includes(value),
|
||||||
},
|
},
|
||||||
|
position: {
|
||||||
|
type: String,
|
||||||
|
default: 'center',
|
||||||
|
validator: value => ['center', 'top'].includes(value),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['confirm', 'close']);
|
const emit = defineEmits(['confirm', 'close']);
|
||||||
@ -61,6 +66,7 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const dialogRef = ref(null);
|
const dialogRef = ref(null);
|
||||||
const dialogContentRef = ref(null);
|
const dialogContentRef = ref(null);
|
||||||
|
const isOpen = ref(false);
|
||||||
|
|
||||||
const maxWidthClass = computed(() => {
|
const maxWidthClass = computed(() => {
|
||||||
const classesMap = {
|
const classesMap = {
|
||||||
@ -75,13 +81,19 @@ const maxWidthClass = computed(() => {
|
|||||||
return classesMap[props.width] ?? 'max-w-md';
|
return classesMap[props.width] ?? 'max-w-md';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const positionClass = computed(() =>
|
||||||
|
props.position === 'top' ? 'dialog-position-top' : ''
|
||||||
|
);
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
|
isOpen.value = true;
|
||||||
dialogRef.value?.showModal();
|
dialogRef.value?.showModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
emit('close');
|
emit('close');
|
||||||
dialogRef.value?.close();
|
dialogRef.value?.close();
|
||||||
|
isOpen.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirm = () => {
|
const confirm = () => {
|
||||||
@ -98,6 +110,7 @@ defineExpose({ open, close });
|
|||||||
class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl"
|
class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl"
|
||||||
:class="[
|
:class="[
|
||||||
maxWidthClass,
|
maxWidthClass,
|
||||||
|
positionClass,
|
||||||
overflowYAuto ? 'overflow-y-auto' : 'overflow-visible',
|
overflowYAuto ? 'overflow-y-auto' : 'overflow-visible',
|
||||||
]"
|
]"
|
||||||
@close="close"
|
@close="close"
|
||||||
@ -105,7 +118,7 @@ defineExpose({ open, close });
|
|||||||
<OnClickOutside @trigger="close">
|
<OnClickOutside @trigger="close">
|
||||||
<form
|
<form
|
||||||
ref="dialogContentRef"
|
ref="dialogContentRef"
|
||||||
class="flex flex-col w-full h-auto gap-6 p-6 overflow-visible text-left align-middle transition-all duration-300 ease-in-out transform bg-n-alpha-3 backdrop-blur-[100px] shadow-xl rounded-xl"
|
class="flex flex-col w-full h-auto gap-6 p-6 overflow-visible text-start align-middle transition-all duration-300 ease-in-out transform bg-n-alpha-3 backdrop-blur-[100px] shadow-xl rounded-xl"
|
||||||
@submit.prevent="confirm"
|
@submit.prevent="confirm"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
@ -119,7 +132,7 @@ defineExpose({ open, close });
|
|||||||
</p>
|
</p>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot v-if="isOpen" />
|
||||||
<!-- Dialog content will be injected here -->
|
<!-- Dialog content will be injected here -->
|
||||||
<slot name="footer">
|
<slot name="footer">
|
||||||
<div
|
<div
|
||||||
@ -156,4 +169,9 @@ defineExpose({ open, close });
|
|||||||
dialog::backdrop {
|
dialog::backdrop {
|
||||||
@apply bg-n-alpha-black1 backdrop-blur-[4px];
|
@apply bg-n-alpha-black1 backdrop-blur-[4px];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-position-top {
|
||||||
|
margin-top: clamp(2rem, 5vh, 5rem);
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -4,6 +4,10 @@ defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: 'max-h-96',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -15,7 +19,10 @@ defineProps({
|
|||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
<ul class="gap-2 grid reset-base list-none px-2 max-h-96 overflow-y-auto">
|
<ul
|
||||||
|
class="gap-2 grid reset-base list-none px-2 overflow-y-auto"
|
||||||
|
:class="height"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -50,12 +50,12 @@ const currentFilter = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getOperator = (filter, selectedOperator) => {
|
const getOperator = (filter, selectedOperator) => {
|
||||||
const operatorFromOptions = filter.filterOperators.find(
|
const operatorFromOptions = filter?.filterOperators?.find(
|
||||||
operator => operator.value === selectedOperator
|
operator => operator.value === selectedOperator
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!operatorFromOptions) {
|
if (!operatorFromOptions) {
|
||||||
return filter.filterOperators[0];
|
return filter?.filterOperators?.[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return operatorFromOptions;
|
return operatorFromOptions;
|
||||||
@ -138,7 +138,11 @@ const validate = () => {
|
|||||||
return !validationError.value;
|
return !validationError.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ validate });
|
const resetValidation = () => {
|
||||||
|
showErrors.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ validate, resetValidation });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -166,18 +170,20 @@ defineExpose({ validate });
|
|||||||
<FilterSelect
|
<FilterSelect
|
||||||
v-model="filterOperator"
|
v-model="filterOperator"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:options="currentFilter.filterOperators"
|
:options="currentFilter?.filterOperators"
|
||||||
/>
|
/>
|
||||||
<template v-if="currentOperator.hasInput">
|
<template v-if="currentOperator?.hasInput">
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-if="inputType === 'multiSelect'"
|
v-if="inputType === 'multiSelect'"
|
||||||
v-model="values"
|
v-model="values"
|
||||||
:options="currentFilter.options"
|
:options="currentFilter.options"
|
||||||
|
dropdown-max-height="max-h-72"
|
||||||
/>
|
/>
|
||||||
<SingleSelect
|
<SingleSelect
|
||||||
v-else-if="inputType === 'searchSelect'"
|
v-else-if="inputType === 'searchSelect'"
|
||||||
v-model="values"
|
v-model="values"
|
||||||
:options="currentFilter.options"
|
:options="currentFilter.options"
|
||||||
|
dropdown-max-height="max-h-64"
|
||||||
/>
|
/>
|
||||||
<SingleSelect
|
<SingleSelect
|
||||||
v-else-if="inputType === 'booleanSelect'"
|
v-else-if="inputType === 'booleanSelect'"
|
||||||
|
|||||||
@ -45,7 +45,7 @@ const { height } = useWindowSize();
|
|||||||
const { height: dropdownHeight } = useElementBounding(dropdownRef);
|
const { height: dropdownHeight } = useElementBounding(dropdownRef);
|
||||||
|
|
||||||
const selectedOption = computed(() => {
|
const selectedOption = computed(() => {
|
||||||
return props.options.find(o => o.value === selected.value) || {};
|
return props.options?.find(o => o.value === selected.value) || {};
|
||||||
});
|
});
|
||||||
|
|
||||||
const iconToRender = computed(() => {
|
const iconToRender = computed(() => {
|
||||||
@ -87,18 +87,25 @@ const updateSelected = newValue => {
|
|||||||
</template>
|
</template>
|
||||||
<DropdownBody
|
<DropdownBody
|
||||||
ref="dropdownRef"
|
ref="dropdownRef"
|
||||||
class="min-w-48 z-50"
|
class="min-w-56 z-50"
|
||||||
:class="dropdownPosition"
|
:class="dropdownPosition"
|
||||||
strong
|
strong
|
||||||
>
|
>
|
||||||
<DropdownSection class="[&>ul]:max-h-80">
|
<DropdownSection class="[&>ul]:max-h-72">
|
||||||
<DropdownItem
|
<template v-for="option in options" :key="option.value">
|
||||||
v-for="option in options"
|
<li
|
||||||
:key="option.value"
|
v-if="option.disabled"
|
||||||
:label="option.label"
|
class="px-2 py-1.5 text-xs font-medium text-n-slate-10 select-none"
|
||||||
:icon="option.icon"
|
>
|
||||||
@click="updateSelected(option.value)"
|
{{ option.label }}
|
||||||
/>
|
</li>
|
||||||
|
<DropdownItem
|
||||||
|
v-else
|
||||||
|
:label="option.label"
|
||||||
|
:icon="option.icon"
|
||||||
|
@click="updateSelected(option.value)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</DropdownSection>
|
</DropdownSection>
|
||||||
</DropdownBody>
|
</DropdownBody>
|
||||||
</DropdownContainer>
|
</DropdownContainer>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue';
|
|||||||
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
|
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
|
||||||
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
||||||
|
|
||||||
const { options, maxChips } = defineProps({
|
const { options, maxChips, dropdownMaxHeight } = defineProps({
|
||||||
options: {
|
options: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
@ -17,6 +17,10 @@ const { options, maxChips } = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 3,
|
default: 3,
|
||||||
},
|
},
|
||||||
|
dropdownMaxHeight: {
|
||||||
|
type: String,
|
||||||
|
default: 'max-h-80',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@ -123,7 +127,7 @@ const toggleOption = option => {
|
|||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<DropdownBody class="top-0 min-w-48 z-50" strong>
|
<DropdownBody class="top-0 min-w-48 z-50" strong>
|
||||||
<DropdownSection class="[&>ul]:max-h-80">
|
<DropdownSection :height="dropdownMaxHeight">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
:key="option.id"
|
:key="option.id"
|
||||||
|
|||||||
@ -12,10 +12,12 @@ import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
|||||||
const {
|
const {
|
||||||
options,
|
options,
|
||||||
disableSearch,
|
disableSearch,
|
||||||
|
disableDeselect,
|
||||||
placeholderIcon,
|
placeholderIcon,
|
||||||
placeholder,
|
placeholder,
|
||||||
placeholderTrailingIcon,
|
placeholderTrailingIcon,
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
|
dropdownMaxHeight,
|
||||||
} = defineProps({
|
} = defineProps({
|
||||||
options: {
|
options: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -41,6 +43,14 @@ const {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
dropdownMaxHeight: {
|
||||||
|
type: String,
|
||||||
|
default: 'max-h-80',
|
||||||
|
},
|
||||||
|
disableDeselect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@ -63,6 +73,8 @@ const selectedItem = computed(() => {
|
|||||||
const optionToSearch = Array.isArray(selected.value)
|
const optionToSearch = Array.isArray(selected.value)
|
||||||
? selected.value[0]
|
? selected.value[0]
|
||||||
: selected.value;
|
: selected.value;
|
||||||
|
|
||||||
|
if (!optionToSearch) return null;
|
||||||
// extract the selected item from the options array
|
// extract the selected item from the options array
|
||||||
// this ensures that options like icon is also included
|
// this ensures that options like icon is also included
|
||||||
return options.find(option => option.id === optionToSearch.id);
|
return options.find(option => option.id === optionToSearch.id);
|
||||||
@ -77,7 +89,7 @@ const toggleSelected = option => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (selected.value && selected.value.id === optionToToggle.id) {
|
if (selected.value && selected.value.id === optionToToggle.id) {
|
||||||
selected.value = null;
|
if (!disableDeselect) selected.value = null;
|
||||||
} else {
|
} else {
|
||||||
selected.value = optionToToggle;
|
selected.value = optionToToggle;
|
||||||
}
|
}
|
||||||
@ -124,7 +136,7 @@ const toggleSelected = option => {
|
|||||||
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DropdownSection class="[&>ul]:max-h-80">
|
<DropdownSection :height="dropdownMaxHeight">
|
||||||
<template v-if="searchResults.length">
|
<template v-if="searchResults.length">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-for="option in searchResults"
|
v-for="option in searchResults"
|
||||||
|
|||||||
@ -36,6 +36,11 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
focusOnMount: { type: Boolean, default: false },
|
focusOnMount: { type: Boolean, default: false },
|
||||||
allowCreate: { type: Boolean, default: false },
|
allowCreate: { type: Boolean, default: false },
|
||||||
|
// Skip label-based dedup when the consumer already filters menuItems by ID.
|
||||||
|
// Prevents removing all same-name items when one is selected (e.g. duplicate agent names).
|
||||||
|
skipLabelDedup: { type: Boolean, default: false },
|
||||||
|
// When false, the dropdown won't auto-open on mount; it opens only on click/focus.
|
||||||
|
autoOpenDropdown: { type: Boolean, default: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@ -56,7 +61,7 @@ const modelValue = defineModel({
|
|||||||
const tagInputRef = ref(null);
|
const tagInputRef = ref(null);
|
||||||
const tags = ref(props.modelValue);
|
const tags = ref(props.modelValue);
|
||||||
const newTag = ref('');
|
const newTag = ref('');
|
||||||
const isFocused = ref(true);
|
const isFocused = ref(props.autoOpenDropdown);
|
||||||
|
|
||||||
const rules = computed(() => getValidationRules(props.type));
|
const rules = computed(() => getValidationRules(props.type));
|
||||||
const v$ = useVuelidate(rules, { newTag });
|
const v$ = useVuelidate(rules, { newTag });
|
||||||
@ -74,11 +79,11 @@ const showInput = computed(() =>
|
|||||||
const showDropdownMenu = computed(() =>
|
const showDropdownMenu = computed(() =>
|
||||||
props.mode === MODE.SINGLE && tags.value.length >= 1
|
props.mode === MODE.SINGLE && tags.value.length >= 1
|
||||||
? false
|
? false
|
||||||
: props.showDropdown
|
: props.showDropdown && isFocused.value
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredMenuItems = computed(() =>
|
const filteredMenuItems = computed(() => {
|
||||||
buildTagMenuItems({
|
const items = buildTagMenuItems({
|
||||||
mode: props.mode,
|
mode: props.mode,
|
||||||
tags: tags.value,
|
tags: tags.value,
|
||||||
menuItems: props.menuItems,
|
menuItems: props.menuItems,
|
||||||
@ -86,8 +91,14 @@ const filteredMenuItems = computed(() =>
|
|||||||
isLoading: props.isLoading,
|
isLoading: props.isLoading,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
isNewTagInValidType: isNewTagInValidType.value,
|
isNewTagInValidType: isNewTagInValidType.value,
|
||||||
})
|
allowCreate: props.allowCreate,
|
||||||
);
|
skipLabelDedup: props.skipLabelDedup,
|
||||||
|
});
|
||||||
|
if (props.type !== INPUT_TYPES.TEXT) return items;
|
||||||
|
const query = newTag.value?.trim()?.toLowerCase();
|
||||||
|
if (!query) return items;
|
||||||
|
return items.filter(item => item.label?.toLowerCase().includes(query));
|
||||||
|
});
|
||||||
|
|
||||||
const emitDataOnAdd = value => {
|
const emitDataOnAdd = value => {
|
||||||
const matchingMenuItem = findMatchingMenuItem(props.menuItems, value);
|
const matchingMenuItem = findMatchingMenuItem(props.menuItems, value);
|
||||||
@ -112,10 +123,13 @@ const addTag = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const isValidatedType = [INPUT_TYPES.EMAIL, INPUT_TYPES.TEL].includes(
|
||||||
[INPUT_TYPES.EMAIL, INPUT_TYPES.TEL].includes(props.type) ||
|
props.type
|
||||||
props.allowCreate
|
);
|
||||||
) {
|
|
||||||
|
if (!isValidatedType && !props.allowCreate && props.showDropdown) return;
|
||||||
|
|
||||||
|
if (isValidatedType || props.allowCreate) {
|
||||||
if (!(await v$.value.$validate())) return;
|
if (!(await v$.value.$validate())) return;
|
||||||
emitDataOnAdd(trimmedTag);
|
emitDataOnAdd(trimmedTag);
|
||||||
}
|
}
|
||||||
@ -125,28 +139,31 @@ const addTag = async () => {
|
|||||||
const removeTag = index => {
|
const removeTag = index => {
|
||||||
tags.value.splice(index, 1);
|
tags.value.splice(index, 1);
|
||||||
modelValue.value = tags.value;
|
modelValue.value = tags.value;
|
||||||
emit('remove');
|
emit('remove', index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDropdownAction = async ({
|
const handleDropdownAction = async ({
|
||||||
email: emailAddress,
|
email: emailAddress,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
|
label,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
if (props.mode === MODE.SINGLE && tags.value.length >= 1) return;
|
if (props.mode === MODE.SINGLE && tags.value.length >= 1) return;
|
||||||
if (!props.showDropdown) return;
|
if (!props.showDropdown) return;
|
||||||
|
|
||||||
const isEmail = props.type === 'email';
|
const isEmail = props.type === INPUT_TYPES.EMAIL;
|
||||||
newTag.value = isEmail ? emailAddress : phoneNumber;
|
const tagValue = isEmail ? emailAddress : phoneNumber || label;
|
||||||
|
|
||||||
if (!(await v$.value.$validate())) return;
|
if (isEmail || props.type === INPUT_TYPES.TEL) {
|
||||||
|
newTag.value = tagValue;
|
||||||
|
if (!(await v$.value.$validate())) return;
|
||||||
|
}
|
||||||
|
|
||||||
const payload = isEmail
|
emit(
|
||||||
? { email: emailAddress, ...rest }
|
'add',
|
||||||
: { phoneNumber, ...rest };
|
isEmail ? { email: emailAddress, ...rest } : { phoneNumber, label, ...rest }
|
||||||
|
);
|
||||||
emit('add', payload);
|
updateValueAndFocus(tagValue);
|
||||||
updateValueAndFocus(emailAddress);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
@ -163,7 +180,7 @@ const handleKeydown = event => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside = () => {
|
const handleClickOutside = () => {
|
||||||
if (tags.value.length) isFocused.value = false;
|
isFocused.value = false;
|
||||||
emit('onClickOutside');
|
emit('onClickOutside');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -261,6 +261,71 @@ describe('tagInputHelper', () => {
|
|||||||
});
|
});
|
||||||
expect(result).toEqual(menuItems);
|
expect(result).toEqual(menuItems);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not create suggestion when allowCreate is false', () => {
|
||||||
|
const result = buildTagMenuItems({
|
||||||
|
...baseParams,
|
||||||
|
type: INPUT_TYPES.EMAIL,
|
||||||
|
newTag: 'test@example.com',
|
||||||
|
allowCreate: false,
|
||||||
|
});
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still returns available menu items when allowCreate is false', () => {
|
||||||
|
const menuItems = [
|
||||||
|
{ label: 'Agent 1', value: '1' },
|
||||||
|
{ label: 'Agent 2', value: '2' },
|
||||||
|
];
|
||||||
|
const result = buildTagMenuItems({
|
||||||
|
...baseParams,
|
||||||
|
menuItems,
|
||||||
|
newTag: 'Agent',
|
||||||
|
allowCreate: false,
|
||||||
|
});
|
||||||
|
expect(result).toEqual(menuItems);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates new item when allowCreate is true (default)', () => {
|
||||||
|
const result = buildTagMenuItems({
|
||||||
|
...baseParams,
|
||||||
|
type: INPUT_TYPES.EMAIL,
|
||||||
|
newTag: 'test@example.com',
|
||||||
|
allowCreate: true,
|
||||||
|
});
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
label: 'test@example.com',
|
||||||
|
action: 'create',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips label dedup when skipLabelDedup is true', () => {
|
||||||
|
const menuItems = [
|
||||||
|
{ label: 'HDMA', value: 1 },
|
||||||
|
{ label: 'HDMA', value: 2 },
|
||||||
|
];
|
||||||
|
const result = buildTagMenuItems({
|
||||||
|
...baseParams,
|
||||||
|
tags: ['HDMA'],
|
||||||
|
menuItems,
|
||||||
|
skipLabelDedup: true,
|
||||||
|
});
|
||||||
|
expect(result).toEqual(menuItems);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters by label when skipLabelDedup is false (default)', () => {
|
||||||
|
const menuItems = [
|
||||||
|
{ label: 'HDMA', value: 1 },
|
||||||
|
{ label: 'HDMA', value: 2 },
|
||||||
|
];
|
||||||
|
const result = buildTagMenuItems({
|
||||||
|
...baseParams,
|
||||||
|
tags: ['HDMA'],
|
||||||
|
menuItems,
|
||||||
|
});
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canAddTag', () => {
|
describe('canAddTag', () => {
|
||||||
|
|||||||
@ -84,25 +84,29 @@ export const buildTagMenuItems = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
type,
|
type,
|
||||||
isNewTagInValidType,
|
isNewTagInValidType,
|
||||||
|
allowCreate = true,
|
||||||
|
skipLabelDedup = false,
|
||||||
}) => {
|
}) => {
|
||||||
if (mode === MODE.SINGLE && tags.length >= 1) return [];
|
if (mode === MODE.SINGLE && tags.length >= 1) return [];
|
||||||
|
|
||||||
const availableMenuItems = menuItems.filter(
|
const availableMenuItems = skipLabelDedup
|
||||||
item => !tags.includes(item.label)
|
? menuItems
|
||||||
);
|
: menuItems.filter(item => !tags.includes(item.label));
|
||||||
|
|
||||||
// Show typed value as suggestion only if:
|
// Show typed value as suggestion only if:
|
||||||
// 1. There's a value being typed
|
// 1. There's a value being typed
|
||||||
// 2. The value isn't already in the tags
|
// 2. The value isn't already in the tags
|
||||||
// 3. Validation passes (email/phone) and There are no menu items available
|
// 3. Validation passes (email/phone) and There are no menu items available
|
||||||
|
// 4. allowCreate is enabled
|
||||||
const trimmedNewTag = newTag?.trim();
|
const trimmedNewTag = newTag?.trim();
|
||||||
const shouldShowTypedValue =
|
const shouldShowCreateSuggestion =
|
||||||
|
allowCreate &&
|
||||||
trimmedNewTag &&
|
trimmedNewTag &&
|
||||||
!tags.includes(trimmedNewTag) &&
|
!tags.includes(trimmedNewTag) &&
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
!availableMenuItems.length;
|
!availableMenuItems.length;
|
||||||
|
|
||||||
if (shouldShowTypedValue) {
|
if (shouldShowCreateSuggestion) {
|
||||||
const { isValid, formattedValue } = validateAndFormatNewTag(
|
const { isValid, formattedValue } = validateAndFormatNewTag(
|
||||||
trimmedNewTag,
|
trimmedNewTag,
|
||||||
type,
|
type,
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput
|
|||||||
import AutomationActionFileInput from './AutomationFileInput.vue';
|
import AutomationActionFileInput from './AutomationFileInput.vue';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import SingleSelect from 'dashboard/components-next/filter/inputs/SingleSelect.vue';
|
||||||
|
import MultiSelect from 'dashboard/components-next/filter/inputs/MultiSelect.vue';
|
||||||
|
import NextInput from 'dashboard/components-next/input/Input.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -10,6 +13,9 @@ export default {
|
|||||||
AutomationActionFileInput,
|
AutomationActionFileInput,
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
NextButton,
|
NextButton,
|
||||||
|
SingleSelect,
|
||||||
|
MultiSelect,
|
||||||
|
NextInput,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@ -40,6 +46,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
dropdownMaxHeight: {
|
||||||
|
type: String,
|
||||||
|
default: 'max-h-80',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'input', 'removeAction', 'resetAction'],
|
emits: ['update:modelValue', 'input', 'removeAction', 'resetAction'],
|
||||||
computed: {
|
computed: {
|
||||||
@ -69,11 +79,21 @@ export default {
|
|||||||
return this.actionTypes.find(action => action.key === this.action_name)
|
return this.actionTypes.find(action => action.key === this.action_name)
|
||||||
.inputType;
|
.inputType;
|
||||||
},
|
},
|
||||||
actionInputStyles() {
|
actionNameAsSelectModel: {
|
||||||
return {
|
get() {
|
||||||
'has-error': this.errorMessage,
|
if (!this.action_name) return null;
|
||||||
'is-a-macro': this.isMacro,
|
const found = this.actionTypes.find(a => a.key === this.action_name);
|
||||||
};
|
return found ? { id: found.key, name: found.label } : null;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.action_name = value?.id || value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actionTypesAsOptions() {
|
||||||
|
return this.actionTypes.map(a => ({ id: a.key, name: a.label }));
|
||||||
|
},
|
||||||
|
isVerticalLayout() {
|
||||||
|
return ['team_message', 'textarea'].includes(this.inputType);
|
||||||
},
|
},
|
||||||
castMessageVmodel: {
|
castMessageVmodel: {
|
||||||
get() {
|
get() {
|
||||||
@ -94,203 +114,89 @@ export default {
|
|||||||
resetAction() {
|
resetAction() {
|
||||||
this.$emit('resetAction');
|
this.$emit('resetAction');
|
||||||
},
|
},
|
||||||
|
onActionNameChange(value) {
|
||||||
|
this.actionNameAsSelectModel = value;
|
||||||
|
this.resetAction();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="filter" :class="actionInputStyles">
|
<li class="list-none py-2 first:pt-0 last:pb-0">
|
||||||
<div class="filter-inputs">
|
<div
|
||||||
<select
|
class="flex flex-col gap-2"
|
||||||
v-model="action_name"
|
:class="{ 'animate-wiggle': errorMessage }"
|
||||||
class="action__question"
|
>
|
||||||
:class="{ 'full-width': !showActionInput }"
|
<div class="flex items-center gap-2">
|
||||||
@change="resetAction()"
|
<SingleSelect
|
||||||
>
|
:model-value="actionNameAsSelectModel"
|
||||||
<option
|
:options="actionTypesAsOptions"
|
||||||
v-for="attribute in actionTypes"
|
:dropdown-max-height="dropdownMaxHeight"
|
||||||
:key="attribute.key"
|
disable-deselect
|
||||||
:value="attribute.key"
|
class="flex-shrink-0"
|
||||||
>
|
@update:model-value="onActionNameChange"
|
||||||
{{ attribute.label }}
|
/>
|
||||||
</option>
|
<template v-if="showActionInput && !isVerticalLayout">
|
||||||
</select>
|
<SingleSelect
|
||||||
<div v-if="showActionInput" class="filter__answer--wrap">
|
|
||||||
<div v-if="inputType" class="w-full">
|
|
||||||
<div
|
|
||||||
v-if="inputType === 'search_select'"
|
v-if="inputType === 'search_select'"
|
||||||
class="multiselect-wrap--small"
|
v-model="action_params"
|
||||||
>
|
:options="dropdownValues"
|
||||||
<multiselect
|
:dropdown-max-height="dropdownMaxHeight"
|
||||||
v-model="action_params"
|
/>
|
||||||
track-by="id"
|
<MultiSelect
|
||||||
label="name"
|
|
||||||
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
|
||||||
selected-label
|
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
|
||||||
deselect-label=""
|
|
||||||
:max-height="160"
|
|
||||||
:options="dropdownValues"
|
|
||||||
:allow-empty="false"
|
|
||||||
:option-height="104"
|
|
||||||
>
|
|
||||||
<template #noOptions>
|
|
||||||
{{ $t('FORMS.MULTISELECT.NO_OPTIONS') }}
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="inputType === 'multi_select'"
|
v-else-if="inputType === 'multi_select'"
|
||||||
class="multiselect-wrap--small"
|
v-model="action_params"
|
||||||
>
|
:options="dropdownValues"
|
||||||
<multiselect
|
:dropdown-max-height="dropdownMaxHeight"
|
||||||
v-model="action_params"
|
/>
|
||||||
track-by="id"
|
<NextInput
|
||||||
label="name"
|
|
||||||
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
|
||||||
multiple
|
|
||||||
selected-label
|
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
|
||||||
deselect-label=""
|
|
||||||
:max-height="160"
|
|
||||||
:options="dropdownValues"
|
|
||||||
:allow-empty="false"
|
|
||||||
:option-height="104"
|
|
||||||
>
|
|
||||||
<template #noOptions>
|
|
||||||
{{ $t('FORMS.MULTISELECT.NO_OPTIONS') }}
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
v-else-if="inputType === 'email'"
|
v-else-if="inputType === 'email'"
|
||||||
v-model="action_params"
|
v-model="action_params"
|
||||||
type="email"
|
type="email"
|
||||||
class="answer--text-input"
|
size="sm"
|
||||||
:placeholder="$t('AUTOMATION.ACTION.EMAIL_INPUT_PLACEHOLDER')"
|
:placeholder="$t('AUTOMATION.ACTION.EMAIL_INPUT_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
<input
|
<NextInput
|
||||||
v-else-if="inputType === 'url'"
|
v-else-if="inputType === 'url'"
|
||||||
v-model="action_params"
|
v-model="action_params"
|
||||||
type="url"
|
type="url"
|
||||||
class="answer--text-input"
|
size="sm"
|
||||||
:placeholder="$t('AUTOMATION.ACTION.URL_INPUT_PLACEHOLDER')"
|
:placeholder="$t('AUTOMATION.ACTION.URL_INPUT_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
<AutomationActionFileInput
|
<AutomationActionFileInput
|
||||||
v-if="inputType === 'attachment'"
|
v-else-if="inputType === 'attachment'"
|
||||||
v-model="action_params"
|
v-model="action_params"
|
||||||
:initial-file-name="initialFileName"
|
:initial-file-name="initialFileName"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
|
<NextButton
|
||||||
|
v-if="!isMacro"
|
||||||
|
sm
|
||||||
|
solid
|
||||||
|
slate
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
@click="removeAction"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NextButton
|
<AutomationActionTeamMessageInput
|
||||||
v-if="!isMacro"
|
v-if="inputType === 'team_message'"
|
||||||
icon="i-lucide-x"
|
v-model="action_params"
|
||||||
slate
|
:teams="dropdownValues"
|
||||||
ghost
|
:dropdown-max-height="dropdownMaxHeight"
|
||||||
class="flex-shrink-0"
|
/>
|
||||||
@click="removeAction"
|
<WootMessageEditor
|
||||||
|
v-if="inputType === 'textarea'"
|
||||||
|
v-model="castMessageVmodel"
|
||||||
|
rows="4"
|
||||||
|
enable-variables
|
||||||
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
||||||
|
class="[&_.ProseMirror-menubar]:hidden px-3 py-1 bg-n-alpha-1 rounded-lg outline outline-1 outline-n-weak dark:outline-n-strong"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AutomationActionTeamMessageInput
|
<span v-if="errorMessage" class="text-sm text-n-ruby-11">
|
||||||
v-if="inputType === 'team_message'"
|
|
||||||
v-model="action_params"
|
|
||||||
:teams="dropdownValues"
|
|
||||||
/>
|
|
||||||
<WootMessageEditor
|
|
||||||
v-if="inputType === 'textarea'"
|
|
||||||
v-model="castMessageVmodel"
|
|
||||||
rows="4"
|
|
||||||
enable-variables
|
|
||||||
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
|
||||||
class="action-message"
|
|
||||||
/>
|
|
||||||
<p v-if="errorMessage" class="filter-error">
|
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</p>
|
</span>
|
||||||
</div>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.filter {
|
|
||||||
@apply bg-n-background p-2 border border-solid border-n-strong dark:border-n-strong rounded-lg mb-2;
|
|
||||||
|
|
||||||
&.is-a-macro {
|
|
||||||
@apply mb-0 bg-n-background dark:bg-n-solid-1 p-0 border-0 rounded-none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-margin-bottom {
|
|
||||||
@apply mb-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter.has-error {
|
|
||||||
@apply bg-n-ruby-8/20 border-n-ruby-5 dark:border-n-ruby-5;
|
|
||||||
|
|
||||||
&.is-a-macro {
|
|
||||||
@apply bg-transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-inputs {
|
|
||||||
@apply flex gap-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-error {
|
|
||||||
@apply text-n-ruby-9 dark:text-n-ruby-9 block my-1 mx-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action__question,
|
|
||||||
.filter__operator {
|
|
||||||
@apply mb-0 mr-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action__question {
|
|
||||||
@apply max-w-[50%];
|
|
||||||
}
|
|
||||||
|
|
||||||
.action__question.full-width {
|
|
||||||
@apply max-w-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter__answer--wrap {
|
|
||||||
@apply max-w-[50%] flex-grow mr-1 flex w-full items-center justify-start;
|
|
||||||
|
|
||||||
input {
|
|
||||||
@apply mb-0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter__answer {
|
|
||||||
&.answer--text-input {
|
|
||||||
@apply mb-0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter__join-operator-wrap {
|
|
||||||
@apply relative z-20 m-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter__join-operator {
|
|
||||||
@apply flex items-center justify-center relative my-2.5 mx-0;
|
|
||||||
|
|
||||||
.operator__line {
|
|
||||||
@apply absolute w-full border-b border-solid border-n-weak;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operator__select {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
@apply relative w-auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect {
|
|
||||||
@apply mb-0;
|
|
||||||
}
|
|
||||||
.action-message {
|
|
||||||
@apply mt-2 mx-0 mb-0;
|
|
||||||
}
|
|
||||||
// Prosemirror does not have a native way of hiding the menu bar, hence
|
|
||||||
::v-deep .ProseMirror-menubar {
|
|
||||||
@apply hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import MultiSelect from 'dashboard/components-next/filter/inputs/MultiSelect.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
MultiSelect,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
teams: { type: Array, required: true },
|
teams: { type: Array, required: true },
|
||||||
modelValue: { type: Object, required: true },
|
modelValue: { type: Object, required: true },
|
||||||
|
dropdownMaxHeight: { type: String, default: 'max-h-80' },
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
data() {
|
data() {
|
||||||
@ -12,9 +18,9 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const { team_ids: teamIds } = this.modelValue;
|
const { team_ids: teamIds, message } = this.modelValue || {};
|
||||||
this.selectedTeams = teamIds;
|
this.selectedTeams = teamIds || [];
|
||||||
this.message = this.modelValue.message;
|
this.message = message || '';
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateValue() {
|
updateValue() {
|
||||||
@ -28,37 +34,19 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex flex-col gap-2">
|
||||||
<div class="multiselect-wrap--small flex flex-col gap-1 mt-1">
|
<MultiSelect
|
||||||
<multiselect
|
v-model="selectedTeams"
|
||||||
v-model="selectedTeams"
|
:options="teams"
|
||||||
track-by="id"
|
:dropdown-max-height="dropdownMaxHeight"
|
||||||
label="name"
|
@update:model-value="updateValue"
|
||||||
:placeholder="$t('AUTOMATION.ACTION.TEAM_DROPDOWN_PLACEHOLDER')"
|
/>
|
||||||
multiple
|
<textarea
|
||||||
selected-label
|
v-model="message"
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
class="mb-0 !text-sm"
|
||||||
deselect-label=""
|
rows="4"
|
||||||
:max-height="160"
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
||||||
:options="teams"
|
@input="updateValue"
|
||||||
:allow-empty="false"
|
/>
|
||||||
@update:model-value="updateValue"
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
v-model="message"
|
|
||||||
rows="4"
|
|
||||||
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
|
||||||
@input="updateValue"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.multiselect {
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -79,7 +79,7 @@ input[type='file'] {
|
|||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
@apply flex h-9 bg-n-background py-1 px-2 items-center text-xs cursor-pointer rounded-sm border border-dashed border-n-strong;
|
@apply flex h-8 bg-n-background py-1 px-2 items-center text-xs cursor-pointer rounded-lg border border-dashed border-n-strong;
|
||||||
}
|
}
|
||||||
.success-icon {
|
.success-icon {
|
||||||
@apply text-n-teal-9 mr-2;
|
@apply text-n-teal-9 mr-2;
|
||||||
|
|||||||
@ -1,305 +0,0 @@
|
|||||||
<script>
|
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'FilterInput',
|
|
||||||
components: {
|
|
||||||
NextButton,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
filterAttributes: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
inputType: {
|
|
||||||
type: String,
|
|
||||||
default: 'plain_text',
|
|
||||||
},
|
|
||||||
operators: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
dropdownValues: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
showQueryOperator: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
showUserInput: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
groupedFilters: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
filterGroups: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
customAttributeType: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
errorMessage: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue', 'removeFilter', 'resetFilter'],
|
|
||||||
computed: {
|
|
||||||
attributeKey: {
|
|
||||||
get() {
|
|
||||||
if (!this.modelValue) return null;
|
|
||||||
return this.modelValue.attribute_key;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
const payload = this.modelValue || {};
|
|
||||||
this.$emit('update:modelValue', { ...payload, attribute_key: value });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
filterOperator: {
|
|
||||||
get() {
|
|
||||||
if (!this.modelValue) return null;
|
|
||||||
return this.modelValue.filter_operator;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
const payload = this.modelValue || {};
|
|
||||||
this.$emit('update:modelValue', { ...payload, filter_operator: value });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
values: {
|
|
||||||
get() {
|
|
||||||
if (!this.modelValue) return null;
|
|
||||||
return this.modelValue.values;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
const payload = this.modelValue || {};
|
|
||||||
this.$emit('update:modelValue', { ...payload, values: value });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query_operator: {
|
|
||||||
get() {
|
|
||||||
if (!this.modelValue) return null;
|
|
||||||
return this.modelValue.query_operator;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
const payload = this.modelValue || {};
|
|
||||||
this.$emit('update:modelValue', { ...payload, query_operator: value });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
custom_attribute_type: {
|
|
||||||
get() {
|
|
||||||
if (!this.customAttributeType) return '';
|
|
||||||
return this.customAttributeType;
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
const payload = this.modelValue || {};
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
...payload,
|
|
||||||
custom_attribute_type: this.customAttributeType,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
customAttributeType: {
|
|
||||||
handler(value) {
|
|
||||||
if (
|
|
||||||
value === 'conversation_attribute' ||
|
|
||||||
value === 'contact_attribute'
|
|
||||||
) {
|
|
||||||
// eslint-disable-next-line vue/no-mutating-props
|
|
||||||
this.modelValue.custom_attribute_type = this.customAttributeType;
|
|
||||||
// eslint-disable-next-line vue/no-mutating-props
|
|
||||||
} else this.modelValue.custom_attribute_type = '';
|
|
||||||
},
|
|
||||||
immediate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
removeFilter() {
|
|
||||||
this.$emit('removeFilter');
|
|
||||||
},
|
|
||||||
resetFilter() {
|
|
||||||
this.$emit('resetFilter');
|
|
||||||
},
|
|
||||||
getInputErrorClass(errorMessage) {
|
|
||||||
return errorMessage
|
|
||||||
? 'bg-n-ruby-8/20 border-n-ruby-5 dark:border-n-ruby-5'
|
|
||||||
: 'bg-n-background border-n-weak dark:border-n-weak';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-mutating-props -->
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="p-2 border border-solid rounded-lg"
|
|
||||||
:class="getInputErrorClass(errorMessage)"
|
|
||||||
>
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<select
|
|
||||||
v-if="groupedFilters"
|
|
||||||
v-model="attributeKey"
|
|
||||||
class="max-w-[30%] mb-0 mr-1"
|
|
||||||
@change="resetFilter()"
|
|
||||||
>
|
|
||||||
<optgroup
|
|
||||||
v-for="(group, i) in filterGroups"
|
|
||||||
:key="i"
|
|
||||||
:label="group.name"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="attribute in group.attributes"
|
|
||||||
:key="attribute.key"
|
|
||||||
:value="attribute.key"
|
|
||||||
:selected="true"
|
|
||||||
>
|
|
||||||
{{ attribute.name }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
v-else
|
|
||||||
v-model="attributeKey"
|
|
||||||
class="max-w-[30%] mb-0 mr-1"
|
|
||||||
@change="resetFilter()"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="attribute in filterAttributes"
|
|
||||||
:key="attribute.key"
|
|
||||||
:value="attribute.key"
|
|
||||||
:disabled="attribute.disabled"
|
|
||||||
>
|
|
||||||
{{ attribute.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select v-model="filterOperator" class="max-w-[20%] mb-0 mr-1">
|
|
||||||
<option
|
|
||||||
v-for="(operator, o) in operators"
|
|
||||||
:key="o"
|
|
||||||
:value="operator.value"
|
|
||||||
>
|
|
||||||
{{ $t(`FILTER.OPERATOR_LABELS.${operator.value}`) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div v-if="showUserInput" class="flex-grow mr-1 filter__answer--wrap">
|
|
||||||
<div
|
|
||||||
v-if="inputType === 'multi_select'"
|
|
||||||
class="multiselect-wrap--small"
|
|
||||||
>
|
|
||||||
<multiselect
|
|
||||||
v-model="values"
|
|
||||||
track-by="id"
|
|
||||||
label="name"
|
|
||||||
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
|
||||||
multiple
|
|
||||||
selected-label
|
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
|
||||||
deselect-label=""
|
|
||||||
:max-height="160"
|
|
||||||
:options="dropdownValues"
|
|
||||||
:allow-empty="false"
|
|
||||||
>
|
|
||||||
<template #noOptions>
|
|
||||||
{{ $t('FORMS.MULTISELECT.NO_OPTIONS') }}
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="inputType === 'search_select'"
|
|
||||||
class="multiselect-wrap--small"
|
|
||||||
>
|
|
||||||
<multiselect
|
|
||||||
v-model="values"
|
|
||||||
track-by="id"
|
|
||||||
label="name"
|
|
||||||
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
|
||||||
selected-label
|
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
|
||||||
deselect-label=""
|
|
||||||
:max-height="160"
|
|
||||||
:options="dropdownValues"
|
|
||||||
:allow-empty="false"
|
|
||||||
:option-height="104"
|
|
||||||
>
|
|
||||||
<template #noOptions>
|
|
||||||
{{ $t('FORMS.MULTISELECT.NO_OPTIONS') }}
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="inputType === 'date'" class="multiselect-wrap--small">
|
|
||||||
<input
|
|
||||||
v-model="values"
|
|
||||||
type="date"
|
|
||||||
:editable="false"
|
|
||||||
class="!mb-0 datepicker"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
v-else
|
|
||||||
v-model="values"
|
|
||||||
type="text"
|
|
||||||
class="!mb-0"
|
|
||||||
:placeholder="$t('FILTER.INPUT_PLACEHOLDER')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<NextButton
|
|
||||||
icon="i-lucide-x"
|
|
||||||
slate
|
|
||||||
ghost
|
|
||||||
class="flex-shrink-0"
|
|
||||||
@click="removeFilter"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p v-if="errorMessage" class="filter-error">
|
|
||||||
{{ errorMessage }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="showQueryOperator"
|
|
||||||
class="flex items-center justify-center relative my-2.5 mx-0"
|
|
||||||
>
|
|
||||||
<hr class="absolute w-full border-b border-solid border-n-weak" />
|
|
||||||
<select
|
|
||||||
v-model="query_operator"
|
|
||||||
class="relative w-auto mb-0 bg-n-background text-n-slate-12 border-n-weak"
|
|
||||||
>
|
|
||||||
<option value="and">
|
|
||||||
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.AND') }}
|
|
||||||
</option>
|
|
||||||
<option value="or">
|
|
||||||
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.OR') }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.filter__answer--wrap {
|
|
||||||
input {
|
|
||||||
@apply bg-n-background mb-0 text-n-slate-12 border-n-weak;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-error {
|
|
||||||
@apply text-n-ruby-9 dark:text-n-ruby-9 block my-1 mx-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect {
|
|
||||||
@apply mb-0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { ref, computed } from 'vue';
|
import { ref, reactive, computed } from 'vue';
|
||||||
import { useStoreGetters } from 'dashboard/composables/store';
|
import { useStoreGetters } from 'dashboard/composables/store';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@ -40,7 +40,7 @@ export function useAutomation(startValue = null) {
|
|||||||
} = useAutomationValues();
|
} = useAutomationValues();
|
||||||
|
|
||||||
const automation = ref(startValue);
|
const automation = ref(startValue);
|
||||||
const automationTypes = structuredClone(AUTOMATIONS);
|
const automationTypes = reactive(structuredClone(AUTOMATIONS));
|
||||||
const eventName = computed(() => automation.value?.event_name);
|
const eventName = computed(() => automation.value?.event_name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -160,14 +160,24 @@ export function useAutomation(startValue = null) {
|
|||||||
t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
|
t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const CUSTOM_ATTR_HEADER_KEYS = new Set([
|
||||||
|
'conversation_custom_attribute',
|
||||||
|
'contact_custom_attribute',
|
||||||
|
]);
|
||||||
|
|
||||||
[
|
[
|
||||||
'message_created',
|
'message_created',
|
||||||
'conversation_created',
|
'conversation_created',
|
||||||
'conversation_updated',
|
'conversation_updated',
|
||||||
'conversation_opened',
|
'conversation_opened',
|
||||||
].forEach(eventToUpdate => {
|
].forEach(eventToUpdate => {
|
||||||
|
const standardConditions = automationTypes[
|
||||||
|
eventToUpdate
|
||||||
|
].conditions.filter(
|
||||||
|
c => !c.customAttributeType && !CUSTOM_ATTR_HEADER_KEYS.has(c.key)
|
||||||
|
);
|
||||||
automationTypes[eventToUpdate].conditions = [
|
automationTypes[eventToUpdate].conditions = [
|
||||||
...automationTypes[eventToUpdate].conditions,
|
...standardConditions,
|
||||||
...manifestedCustomAttributes,
|
...manifestedCustomAttributes,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
@ -169,19 +169,19 @@ export const getFileName = (action, files = []) => {
|
|||||||
|
|
||||||
export const getDefaultConditions = eventName => {
|
export const getDefaultConditions = eventName => {
|
||||||
if (eventName === 'message_created') {
|
if (eventName === 'message_created') {
|
||||||
return DEFAULT_MESSAGE_CREATED_CONDITION;
|
return structuredClone(DEFAULT_MESSAGE_CREATED_CONDITION);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
eventName === 'conversation_opened' ||
|
eventName === 'conversation_opened' ||
|
||||||
eventName === 'conversation_resolved'
|
eventName === 'conversation_resolved'
|
||||||
) {
|
) {
|
||||||
return DEFAULT_CONVERSATION_CONDITION;
|
return structuredClone(DEFAULT_CONVERSATION_CONDITION);
|
||||||
}
|
}
|
||||||
return DEFAULT_OTHER_CONDITION;
|
return structuredClone(DEFAULT_OTHER_CONDITION);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDefaultActions = () => {
|
export const getDefaultActions = () => {
|
||||||
return DEFAULT_ACTIONS;
|
return structuredClone(DEFAULT_ACTIONS);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterCustomAttributes = customAttributes => {
|
export const filterCustomAttributes = customAttributes => {
|
||||||
|
|||||||
@ -1,91 +1,101 @@
|
|||||||
<script>
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
import { useAlert, useTrack } from 'dashboard/composables';
|
import { useAlert, useTrack } from 'dashboard/composables';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
|
||||||
import MergeContact from 'dashboard/modules/contact/components/MergeContact.vue';
|
import MergeContact from 'dashboard/modules/contact/components/MergeContact.vue';
|
||||||
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
import ContactAPI from 'dashboard/api/contacts';
|
import ContactAPI from 'dashboard/api/contacts';
|
||||||
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { CONTACTS_EVENTS } from '../../helper/AnalyticsHelper/events';
|
import { CONTACTS_EVENTS } from '../../helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
components: { MergeContact },
|
primaryContact: {
|
||||||
props: {
|
type: Object,
|
||||||
show: {
|
required: true,
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
primaryContact: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['close', 'update:show'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isSearching: false,
|
|
||||||
searchResults: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
uiFlags: 'contacts/getUIFlags',
|
|
||||||
}),
|
|
||||||
localShow: {
|
|
||||||
get() {
|
|
||||||
return this.show;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$emit('update:show', value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
methods: {
|
const emit = defineEmits(['close']);
|
||||||
onClose() {
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
async onContactSearch(query) {
|
|
||||||
this.isSearching = true;
|
|
||||||
this.searchResults = [];
|
|
||||||
|
|
||||||
try {
|
const { t } = useI18n();
|
||||||
const {
|
const store = useStore();
|
||||||
data: { payload },
|
const uiFlags = useMapGetter('contacts/getUIFlags');
|
||||||
} = await ContactAPI.search(query);
|
|
||||||
this.searchResults = payload.filter(
|
const dialogRef = ref(null);
|
||||||
contact => contact.id !== this.primaryContact.id
|
const isSearching = ref(false);
|
||||||
);
|
const searchResults = ref([]);
|
||||||
} catch (error) {
|
|
||||||
useAlert(this.$t('MERGE_CONTACTS.SEARCH.ERROR_MESSAGE'));
|
watch(
|
||||||
} finally {
|
() => props.primaryContact.id,
|
||||||
this.isSearching = false;
|
() => {
|
||||||
}
|
isSearching.value = false;
|
||||||
},
|
searchResults.value = [];
|
||||||
async onMergeContacts(parentContactId) {
|
}
|
||||||
useTrack(CONTACTS_EVENTS.MERGED_CONTACTS);
|
);
|
||||||
try {
|
|
||||||
await this.$store.dispatch('contacts/merge', {
|
const open = () => {
|
||||||
childId: this.primaryContact.id,
|
dialogRef.value?.open();
|
||||||
parentId: parentContactId,
|
};
|
||||||
});
|
|
||||||
useAlert(this.$t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
|
const close = () => {
|
||||||
this.onClose();
|
dialogRef.value?.close();
|
||||||
} catch (error) {
|
};
|
||||||
useAlert(this.$t('MERGE_CONTACTS.FORM.ERROR_MESSAGE'));
|
|
||||||
}
|
defineExpose({ open, close });
|
||||||
},
|
|
||||||
},
|
const onClose = () => {
|
||||||
|
close();
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onContactSearch = async query => {
|
||||||
|
isSearching.value = true;
|
||||||
|
searchResults.value = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { payload },
|
||||||
|
} = await ContactAPI.search(query);
|
||||||
|
searchResults.value = payload.filter(
|
||||||
|
contact => contact.id !== props.primaryContact.id
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t('MERGE_CONTACTS.SEARCH.ERROR_MESSAGE'));
|
||||||
|
} finally {
|
||||||
|
isSearching.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMergeContacts = async parentContactId => {
|
||||||
|
useTrack(CONTACTS_EVENTS.MERGED_CONTACTS);
|
||||||
|
try {
|
||||||
|
await store.dispatch('contacts/merge', {
|
||||||
|
childId: props.primaryContact.id,
|
||||||
|
parentId: parentContactId,
|
||||||
|
});
|
||||||
|
useAlert(t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
|
||||||
|
close();
|
||||||
|
emit('close');
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t('MERGE_CONTACTS.FORM.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<woot-modal v-model:show="localShow" :on-close="onClose">
|
<Dialog
|
||||||
<woot-modal-header
|
ref="dialogRef"
|
||||||
:header-title="$t('MERGE_CONTACTS.TITLE')"
|
type="edit"
|
||||||
:header-content="$t('MERGE_CONTACTS.DESCRIPTION')"
|
width="2xl"
|
||||||
/>
|
:title="$t('MERGE_CONTACTS.TITLE')"
|
||||||
|
:description="$t('MERGE_CONTACTS.DESCRIPTION')"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
<MergeContact
|
<MergeContact
|
||||||
|
:key="primaryContact.id"
|
||||||
:primary-contact="primaryContact"
|
:primary-contact="primaryContact"
|
||||||
:is-searching="isSearching"
|
:is-searching="isSearching"
|
||||||
:is-merging="uiFlags.isMerging"
|
:is-merging="uiFlags.isMerging"
|
||||||
@ -94,5 +104,5 @@ export default {
|
|||||||
@cancel="onClose"
|
@cancel="onClose"
|
||||||
@submit="onMergeContacts"
|
@submit="onMergeContacts"
|
||||||
/>
|
/>
|
||||||
</woot-modal>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import Avatar from 'next/avatar/Avatar.vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
thumbnail: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
phoneNumber: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
identifier: {
|
|
||||||
type: [String, Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="option-item--user">
|
|
||||||
<Avatar :src="thumbnail" :size="28" :name="name" rounded-full />
|
|
||||||
<div class="option__user-data">
|
|
||||||
<h5 class="option__title">
|
|
||||||
{{ name }}
|
|
||||||
<span v-if="identifier" class="user-identifier">
|
|
||||||
{{ $t('MERGE_CONTACTS.DROPDOWN_ITEM.ID', { identifier }) }}
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
<p class="option__body">
|
|
||||||
<span v-if="email" class="email-icon-wrap">
|
|
||||||
<fluent-icon class="merge-contact--icon" icon="mail" size="12" />
|
|
||||||
{{ email }}
|
|
||||||
</span>
|
|
||||||
<span v-if="phoneNumber" class="phone-icon-wrap">
|
|
||||||
<fluent-icon class="merge-contact--icon" icon="call" size="12" />
|
|
||||||
{{ phoneNumber }}
|
|
||||||
</span>
|
|
||||||
<span v-if="!phoneNumber && !email">{{ '---' }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.option-item--user {
|
|
||||||
@apply flex items-center;
|
|
||||||
}
|
|
||||||
.user-identifier {
|
|
||||||
@apply text-xs ml-0.5 text-n-slate-12;
|
|
||||||
}
|
|
||||||
.option__user-data {
|
|
||||||
@apply flex flex-col flex-grow ml-2 mr-2;
|
|
||||||
}
|
|
||||||
.option__body,
|
|
||||||
.option__title {
|
|
||||||
@apply flex items-center justify-start leading-[1.2] text-sm;
|
|
||||||
}
|
|
||||||
.option__body .icon {
|
|
||||||
@apply relative top-px mr-0.5 rtl:mr-0 rtl:ml-0.5;
|
|
||||||
}
|
|
||||||
.option__title {
|
|
||||||
@apply text-n-slate-12 font-medium mb-0.5;
|
|
||||||
}
|
|
||||||
.option__body {
|
|
||||||
@apply text-xs text-n-slate-12 mt-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option__user-data .option__body {
|
|
||||||
> .phone-icon-wrap,
|
|
||||||
> .email-icon-wrap {
|
|
||||||
@apply w-auto flex items-center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.merge-contact--icon {
|
|
||||||
@apply -mb-0.5 mr-0.5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,174 +1,105 @@
|
|||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
import { required } from '@vuelidate/validators';
|
import { required } from '@vuelidate/validators';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import MergeContactSummary from 'dashboard/modules/contact/components/MergeContactSummary.vue';
|
import MergeContactSummary from 'dashboard/modules/contact/components/MergeContactSummary.vue';
|
||||||
import ContactDropdownItem from './ContactDropdownItem.vue';
|
import ContactMergeForm from 'dashboard/components-next/Contacts/ContactsForm/ContactMergeForm.vue';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
components: { MergeContactSummary, ContactDropdownItem, NextButton },
|
primaryContact: {
|
||||||
props: {
|
type: Object,
|
||||||
primaryContact: {
|
required: true,
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isSearching: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isMerging: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
searchResults: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: ['search', 'submit', 'cancel'],
|
isSearching: {
|
||||||
setup() {
|
type: Boolean,
|
||||||
return { v$: useVuelidate() };
|
default: false,
|
||||||
},
|
},
|
||||||
validations: {
|
isMerging: {
|
||||||
primaryContact: {
|
type: Boolean,
|
||||||
required,
|
default: false,
|
||||||
},
|
|
||||||
parentContact: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
searchResults: {
|
||||||
return {
|
type: Array,
|
||||||
parentContact: undefined,
|
default: () => [],
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
computed: {
|
const emit = defineEmits(['search', 'submit', 'cancel']);
|
||||||
parentContactName() {
|
|
||||||
return this.parentContact ? this.parentContact.name : '';
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const parentContactId = ref(null);
|
||||||
|
|
||||||
|
const validationRules = {
|
||||||
|
parentContactId: { required },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(validationRules, { parentContactId });
|
||||||
|
|
||||||
|
const parentContact = computed(() => {
|
||||||
|
if (!parentContactId.value) return null;
|
||||||
|
return props.searchResults.find(
|
||||||
|
contact => contact.id === parentContactId.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentContactName = computed(() => {
|
||||||
|
return parentContact.value ? parentContact.value.name : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const primaryContactList = computed(() => {
|
||||||
|
return props.searchResults.map(contact => ({
|
||||||
|
id: contact.id,
|
||||||
|
label: contact.name,
|
||||||
|
value: contact.id,
|
||||||
|
meta: {
|
||||||
|
thumbnail: contact.thumbnail,
|
||||||
|
email: contact.email,
|
||||||
|
phoneNumber: contact.phone_number,
|
||||||
},
|
},
|
||||||
},
|
}));
|
||||||
methods: {
|
});
|
||||||
searchChange(query) {
|
|
||||||
this.$emit('search', query);
|
const hasValidationError = computed(() => v$.value.parentContactId.$error);
|
||||||
},
|
const validationErrorMessage = computed(() => {
|
||||||
onSubmit() {
|
if (v$.value.parentContactId.$error) {
|
||||||
this.v$.$touch();
|
return t('MERGE_CONTACTS.FORM.CHILD_CONTACT.ERROR');
|
||||||
if (this.v$.$invalid) {
|
}
|
||||||
return;
|
return '';
|
||||||
}
|
});
|
||||||
this.$emit('submit', this.parentContact.id);
|
|
||||||
},
|
const onSearch = query => {
|
||||||
onCancel() {
|
emit('search', query);
|
||||||
this.$emit('cancel');
|
};
|
||||||
},
|
|
||||||
},
|
const onSubmit = () => {
|
||||||
|
v$.value.$touch();
|
||||||
|
if (v$.value.$invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('submit', parentContactId.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
emit('cancel');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="onSubmit">
|
<form @submit.prevent="onSubmit">
|
||||||
<div>
|
<ContactMergeForm
|
||||||
<div>
|
:selected-contact="primaryContact"
|
||||||
<div
|
:primary-contact-id="parentContactId"
|
||||||
class="mt-1 multiselect-wrap--medium"
|
:primary-contact-list="primaryContactList"
|
||||||
:class="{ error: v$.parentContact.$error }"
|
:is-searching="isSearching"
|
||||||
>
|
:has-error="hasValidationError"
|
||||||
<label class="multiselect__label">
|
:error-message="validationErrorMessage"
|
||||||
{{ $t('MERGE_CONTACTS.PARENT.TITLE') }}
|
@update:primary-contact-id="parentContactId = $event"
|
||||||
<woot-label
|
@search="onSearch"
|
||||||
:title="$t('MERGE_CONTACTS.PARENT.HELP_LABEL')"
|
/>
|
||||||
color-scheme="success"
|
|
||||||
small
|
|
||||||
class="ml-2"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<multiselect
|
|
||||||
v-model="parentContact"
|
|
||||||
:options="searchResults"
|
|
||||||
label="name"
|
|
||||||
track-by="id"
|
|
||||||
:internal-search="false"
|
|
||||||
:clear-on-select="false"
|
|
||||||
:show-labels="false"
|
|
||||||
:placeholder="$t('MERGE_CONTACTS.PARENT.PLACEHOLDER')"
|
|
||||||
allow-empty
|
|
||||||
:loading="isSearching"
|
|
||||||
:max-height="150"
|
|
||||||
open-direction="top"
|
|
||||||
@search-change="searchChange"
|
|
||||||
>
|
|
||||||
<template #singleLabel="props">
|
|
||||||
<ContactDropdownItem
|
|
||||||
:thumbnail="props.option.thumbnail"
|
|
||||||
:identifier="props.option.id"
|
|
||||||
:name="props.option.name"
|
|
||||||
:email="props.option.email"
|
|
||||||
:phone-number="props.option.phone_number"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #option="props">
|
|
||||||
<ContactDropdownItem
|
|
||||||
:thumbnail="props.option.thumbnail"
|
|
||||||
:identifier="props.option.id"
|
|
||||||
:name="props.option.name"
|
|
||||||
:email="props.option.email"
|
|
||||||
:phone-number="props.option.phone_number"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #noResult>
|
|
||||||
<span>
|
|
||||||
{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
<span v-if="v$.parentContact.$error" class="message">
|
|
||||||
{{ $t('MERGE_CONTACTS.FORM.CHILD_CONTACT.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex multiselect-wrap--medium">
|
|
||||||
<div
|
|
||||||
class="w-8 relative text-base text-n-strong after:content-[''] after:h-12 after:w-0 ltr:after:left-4 rtl:after:right-4 after:absolute after:border-l after:border-solid after:border-n-strong before:content-[''] before:h-0 before:w-4 ltr:before:left-4 rtl:before:right-4 before:top-12 before:absolute before:border-b before:border-solid before:border-n-strong"
|
|
||||||
>
|
|
||||||
<fluent-icon
|
|
||||||
icon="arrow-up"
|
|
||||||
class="absolute -top-1 ltr:left-2 rtl:right-2"
|
|
||||||
size="17"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col w-full ltr:pl-8 rtl:pr-8">
|
|
||||||
<label class="multiselect__label">
|
|
||||||
{{ $t('MERGE_CONTACTS.PRIMARY.TITLE') }}
|
|
||||||
<woot-label
|
|
||||||
:title="$t('MERGE_CONTACTS.PRIMARY.HELP_LABEL')"
|
|
||||||
color-scheme="alert"
|
|
||||||
small
|
|
||||||
class="ml-2"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<multiselect
|
|
||||||
:model-value="primaryContact"
|
|
||||||
disabled
|
|
||||||
:options="[]"
|
|
||||||
:show-labels="false"
|
|
||||||
label="name"
|
|
||||||
track-by="id"
|
|
||||||
>
|
|
||||||
<template #singleLabel="props">
|
|
||||||
<ContactDropdownItem
|
|
||||||
:thumbnail="props.option.thumbnail"
|
|
||||||
:name="props.option.name"
|
|
||||||
:identifier="props.option.id"
|
|
||||||
:email="props.option.email"
|
|
||||||
:phone-number="props.option.phoneNumber"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MergeContactSummary
|
<MergeContactSummary
|
||||||
:primary-contact-name="primaryContact.name"
|
:primary-contact-name="primaryContact.name"
|
||||||
:parent-contact-name="parentContactName"
|
:parent-contact-name="parentContactName"
|
||||||
@ -189,32 +120,3 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
/* TDOD: Clean errors in forms style */
|
|
||||||
.error .message {
|
|
||||||
@apply mt-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep {
|
|
||||||
.multiselect {
|
|
||||||
@apply rounded-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect--disabled {
|
|
||||||
@apply border-0;
|
|
||||||
|
|
||||||
.multiselect__tags {
|
|
||||||
@apply border;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__tags {
|
|
||||||
@apply h-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__select {
|
|
||||||
@apply mt-px mr-1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -11,11 +11,13 @@ import { isPhoneNumberValid } from 'shared/helpers/Validators';
|
|||||||
import parsePhoneNumber from 'libphonenumber-js';
|
import parsePhoneNumber from 'libphonenumber-js';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
import Avatar from 'next/avatar/Avatar.vue';
|
import Avatar from 'next/avatar/Avatar.vue';
|
||||||
|
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NextButton,
|
NextButton,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
ComboBox,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
contact: {
|
contact: {
|
||||||
@ -133,6 +135,12 @@ export default {
|
|||||||
if (!name && !id) return '';
|
if (!name && !id) return '';
|
||||||
return `${name} (${id})`;
|
return `${name} (${id})`;
|
||||||
},
|
},
|
||||||
|
onCountryChange(value) {
|
||||||
|
const selected = this.countries.find(c => c.id === value);
|
||||||
|
this.country = selected
|
||||||
|
? { id: selected.id, name: selected.name }
|
||||||
|
: { id: '', name: '' };
|
||||||
|
},
|
||||||
setDialCode() {
|
setDialCode() {
|
||||||
if (
|
if (
|
||||||
this.phoneNumber !== '' &&
|
this.phoneNumber !== '' &&
|
||||||
@ -363,26 +371,23 @@ export default {
|
|||||||
:label="$t('CONTACT_FORM.FORM.COMPANY_NAME.LABEL')"
|
:label="$t('CONTACT_FORM.FORM.COMPANY_NAME.LABEL')"
|
||||||
:placeholder="$t('CONTACT_FORM.FORM.COMPANY_NAME.PLACEHOLDER')"
|
:placeholder="$t('CONTACT_FORM.FORM.COMPANY_NAME.PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div class="w-full mb-4">
|
||||||
<div class="w-full">
|
<label>
|
||||||
<label>
|
{{ $t('CONTACT_FORM.FORM.COUNTRY.LABEL') }}
|
||||||
{{ $t('CONTACT_FORM.FORM.COUNTRY.LABEL') }}
|
</label>
|
||||||
</label>
|
<ComboBox
|
||||||
<multiselect
|
:model-value="country.id"
|
||||||
v-model="country"
|
:options="
|
||||||
track-by="id"
|
countries.map(c => ({
|
||||||
label="name"
|
value: c.id,
|
||||||
:placeholder="$t('CONTACT_FORM.FORM.COUNTRY.PLACEHOLDER')"
|
label: countryNameWithCode(c),
|
||||||
selected-label
|
}))
|
||||||
:select-label="$t('CONTACT_FORM.FORM.COUNTRY.SELECT_PLACEHOLDER')"
|
"
|
||||||
:deselect-label="$t('CONTACT_FORM.FORM.COUNTRY.REMOVE')"
|
class="[&>div>button]:!bg-n-alpha-black2"
|
||||||
:custom-label="countryNameWithCode"
|
:placeholder="$t('CONTACT_FORM.FORM.COUNTRY.PLACEHOLDER')"
|
||||||
:max-height="160"
|
:search-placeholder="$t('CONTACT_FORM.FORM.COUNTRY.SELECT_PLACEHOLDER')"
|
||||||
:options="countries"
|
@update:model-value="onCountryChange"
|
||||||
allow-empty
|
/>
|
||||||
:option-height="104"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<woot-input
|
<woot-input
|
||||||
v-model="city"
|
v-model="city"
|
||||||
@ -426,11 +431,3 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
::v-deep {
|
|
||||||
.multiselect .multiselect__tags .multiselect__single {
|
|
||||||
@apply pl-0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -51,7 +51,6 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
showMergeModal: false,
|
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -167,11 +166,8 @@ export default {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeMergeModal() {
|
|
||||||
this.showMergeModal = false;
|
|
||||||
},
|
|
||||||
openMergeModal() {
|
openMergeModal() {
|
||||||
this.showMergeModal = true;
|
this.$refs.mergeModal?.open();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -324,12 +320,7 @@ export default {
|
|||||||
:contact="contact"
|
:contact="contact"
|
||||||
@cancel="toggleEditModal"
|
@cancel="toggleEditModal"
|
||||||
/>
|
/>
|
||||||
<ContactMergeModal
|
<ContactMergeModal ref="mergeModal" :primary-contact="contact" />
|
||||||
v-if="showMergeModal"
|
|
||||||
:primary-contact="contact"
|
|
||||||
:show="showMergeModal"
|
|
||||||
@close="closeMergeModal"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<woot-delete-modal
|
<woot-delete-modal
|
||||||
v-if="showDeleteModal"
|
v-if="showDeleteModal"
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import { convertToAttributeSlug } from 'dashboard/helper/commons.js';
|
|||||||
import { ATTRIBUTE_MODELS, ATTRIBUTE_TYPES } from './constants';
|
import { ATTRIBUTE_MODELS, ATTRIBUTE_TYPES } from './constants';
|
||||||
|
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import TagInput from 'dashboard/components-next/taginput/TagInput.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NextButton,
|
NextButton,
|
||||||
|
TagInput,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
onClose: {
|
onClose: {
|
||||||
@ -41,9 +43,8 @@ export default {
|
|||||||
regexCue: null,
|
regexCue: null,
|
||||||
regexEnabled: false,
|
regexEnabled: false,
|
||||||
values: [],
|
values: [],
|
||||||
options: [],
|
|
||||||
show: true,
|
show: true,
|
||||||
isTouched: false,
|
tagInputTouched: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -63,21 +64,21 @@ export default {
|
|||||||
option: this.$t(`ATTRIBUTES_MGMT.ATTRIBUTE_TYPES.${item.key}`),
|
option: this.$t(`ATTRIBUTES_MGMT.ATTRIBUTE_TYPES.${item.key}`),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
isMultiselectInvalid() {
|
isTagInputEmpty() {
|
||||||
return this.isTouched && this.values.length === 0;
|
|
||||||
},
|
|
||||||
isTagInputInvalid() {
|
|
||||||
return this.isAttributeTypeList && this.values.length === 0;
|
return this.isAttributeTypeList && this.values.length === 0;
|
||||||
},
|
},
|
||||||
|
isTagInputInvalid() {
|
||||||
|
return this.tagInputTouched && this.isTagInputEmpty;
|
||||||
|
},
|
||||||
attributeListValues() {
|
attributeListValues() {
|
||||||
return this.values.map(item => item.name);
|
return this.values;
|
||||||
},
|
},
|
||||||
isButtonDisabled() {
|
isButtonDisabled() {
|
||||||
return (
|
return (
|
||||||
this.v$.displayName.$invalid ||
|
this.v$.displayName.$invalid ||
|
||||||
this.v$.description.$invalid ||
|
this.v$.description.$invalid ||
|
||||||
this.uiFlags.isCreating ||
|
this.uiFlags.isCreating ||
|
||||||
this.isTagInputInvalid
|
this.isTagInputEmpty
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
keyErrorMessage() {
|
keyErrorMessage() {
|
||||||
@ -119,17 +120,14 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
attributeType() {
|
||||||
|
this.tagInputTouched = false;
|
||||||
|
this.values = [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
addTagValue(tagValue) {
|
|
||||||
const tag = {
|
|
||||||
name: tagValue,
|
|
||||||
};
|
|
||||||
this.values.push(tag);
|
|
||||||
this.$refs.tagInput.$el.focus();
|
|
||||||
},
|
|
||||||
onTouch() {
|
|
||||||
this.isTouched = true;
|
|
||||||
},
|
|
||||||
onDisplayNameChange() {
|
onDisplayNameChange() {
|
||||||
this.attributeKey = convertToAttributeSlug(this.displayName);
|
this.attributeKey = convertToAttributeSlug(this.displayName);
|
||||||
},
|
},
|
||||||
@ -237,27 +235,25 @@ export default {
|
|||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
|
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div v-if="isAttributeTypeList" class="multiselect--wrap">
|
<div v-if="isAttributeTypeList" class="mb-4">
|
||||||
<label>
|
<label class="mb-1 block">
|
||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.LABEL') }}
|
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.LABEL') }}
|
||||||
</label>
|
</label>
|
||||||
<multiselect
|
<div
|
||||||
ref="tagInput"
|
class="rounded-xl border px-3 py-2"
|
||||||
v-model="values"
|
:class="isTagInputInvalid ? 'border-n-ruby-9' : 'border-n-weak'"
|
||||||
:placeholder="
|
>
|
||||||
$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')
|
<TagInput
|
||||||
"
|
v-model="values"
|
||||||
label="name"
|
:placeholder="
|
||||||
track-by="name"
|
$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')
|
||||||
:class="{ invalid: isMultiselectInvalid }"
|
"
|
||||||
:options="options"
|
allow-create
|
||||||
multiple
|
@blur="tagInputTouched = true"
|
||||||
taggable
|
/>
|
||||||
@close="onTouch"
|
</div>
|
||||||
@tag="addTagValue"
|
|
||||||
/>
|
|
||||||
<label
|
<label
|
||||||
v-show="isMultiselectInvalid"
|
v-show="isTagInputInvalid"
|
||||||
class="text-n-ruby-9 dark:text-n-ruby-9 text-sm font-normal mt-1"
|
class="text-n-ruby-9 dark:text-n-ruby-9 text-sm font-normal mt-1"
|
||||||
>
|
>
|
||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
|
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
|
||||||
@ -312,22 +308,4 @@ export default {
|
|||||||
padding: 0 0.5rem 0.5rem 0;
|
padding: 0 0.5rem 0.5rem 0;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect--wrap {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep {
|
|
||||||
.multiselect {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__content-wrapper {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect--active .multiselect__tags {
|
|
||||||
border-radius: 0.3125rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -5,10 +5,12 @@ import { required, minLength } from '@vuelidate/validators';
|
|||||||
import { getRegexp } from 'shared/helpers/Validators';
|
import { getRegexp } from 'shared/helpers/Validators';
|
||||||
import { ATTRIBUTE_TYPES } from './constants';
|
import { ATTRIBUTE_TYPES } from './constants';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import TagInput from 'dashboard/components-next/taginput/TagInput.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NextButton,
|
NextButton,
|
||||||
|
TagInput,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
selectedAttribute: {
|
selectedAttribute: {
|
||||||
@ -35,8 +37,7 @@ export default {
|
|||||||
show: true,
|
show: true,
|
||||||
attributeKey: '',
|
attributeKey: '',
|
||||||
values: [],
|
values: [],
|
||||||
options: [],
|
tagInputTouched: false,
|
||||||
isTouched: true,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
@ -65,20 +66,19 @@ export default {
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
setAttributeListValue() {
|
setAttributeListValue() {
|
||||||
return this.selectedAttribute.attribute_values.map(values => ({
|
return this.selectedAttribute.attribute_values || [];
|
||||||
name: values,
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
updatedAttributeListValues() {
|
updatedAttributeListValues() {
|
||||||
return this.values.map(item => item.name);
|
return this.values;
|
||||||
},
|
},
|
||||||
isButtonDisabled() {
|
isButtonDisabled() {
|
||||||
return this.v$.description.$invalid || this.isMultiselectInvalid;
|
return this.v$.description.$invalid || this.isTagInputEmpty;
|
||||||
},
|
},
|
||||||
isMultiselectInvalid() {
|
isTagInputEmpty() {
|
||||||
return (
|
return this.isAttributeTypeList && this.values.length === 0;
|
||||||
this.isAttributeTypeList && this.isTouched && this.values.length === 0
|
},
|
||||||
);
|
isTagInputInvalid() {
|
||||||
|
return this.tagInputTouched && this.isTagInputEmpty;
|
||||||
},
|
},
|
||||||
|
|
||||||
pageTitle() {
|
pageTitle() {
|
||||||
@ -116,13 +116,6 @@ export default {
|
|||||||
onClose() {
|
onClose() {
|
||||||
this.$emit('onClose');
|
this.$emit('onClose');
|
||||||
},
|
},
|
||||||
addTagValue(tagValue) {
|
|
||||||
const tag = {
|
|
||||||
name: tagValue,
|
|
||||||
};
|
|
||||||
this.values.push(tag);
|
|
||||||
this.$refs.tagInput.$el.focus();
|
|
||||||
},
|
|
||||||
setFormValues() {
|
setFormValues() {
|
||||||
const regexPattern = this.selectedAttribute.regex_pattern
|
const regexPattern = this.selectedAttribute.regex_pattern
|
||||||
? getRegexp(this.selectedAttribute.regex_pattern).source
|
? getRegexp(this.selectedAttribute.regex_pattern).source
|
||||||
@ -225,24 +218,25 @@ export default {
|
|||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
|
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div v-if="isAttributeTypeList" class="multiselect--wrap">
|
<div v-if="isAttributeTypeList" class="mb-4">
|
||||||
<label>
|
<label class="mb-1 block">
|
||||||
{{ $t('ATTRIBUTES_MGMT.EDIT.TYPE.LIST.LABEL') }}
|
{{ $t('ATTRIBUTES_MGMT.EDIT.TYPE.LIST.LABEL') }}
|
||||||
</label>
|
</label>
|
||||||
<multiselect
|
<div
|
||||||
ref="tagInput"
|
class="rounded-xl border px-3 py-2"
|
||||||
v-model="values"
|
:class="isTagInputInvalid ? 'border-n-ruby-9' : 'border-n-weak'"
|
||||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')"
|
>
|
||||||
label="name"
|
<TagInput
|
||||||
track-by="name"
|
v-model="values"
|
||||||
:class="{ invalid: isMultiselectInvalid }"
|
:placeholder="
|
||||||
:options="options"
|
$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')
|
||||||
multiple
|
"
|
||||||
taggable
|
allow-create
|
||||||
@tag="addTagValue"
|
@blur="tagInputTouched = true"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<label
|
<label
|
||||||
v-show="isMultiselectInvalid"
|
v-show="isTagInputInvalid"
|
||||||
class="text-n-ruby-9 dark:text-n-ruby-9 text-sm font-normal mt-1"
|
class="text-n-ruby-9 dark:text-n-ruby-9 text-sm font-normal mt-1"
|
||||||
>
|
>
|
||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
|
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
|
||||||
@ -297,22 +291,4 @@ export default {
|
|||||||
padding: 0 0.5rem 0.5rem 0;
|
padding: 0 0.5rem 0.5rem 0;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect--wrap {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep {
|
|
||||||
.multiselect {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__content-wrapper {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect--active .multiselect__tags {
|
|
||||||
border-radius: 0.3125rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,21 +1,12 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { mapGetters } from 'vuex';
|
import { ref, onMounted } from 'vue';
|
||||||
import FilterInputBox from 'dashboard/components/widgets/FilterInput/Index.vue';
|
import { useStore } from 'dashboard/composables/store';
|
||||||
import AutomationActionInput from 'dashboard/components/widgets/AutomationActionInput.vue';
|
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
|
||||||
import { useAutomation } from 'dashboard/composables/useAutomation';
|
import { useAutomation } from 'dashboard/composables/useAutomation';
|
||||||
import { validateAutomation } from 'dashboard/helper/validations';
|
import AutomationRuleForm from './AutomationRuleForm.vue';
|
||||||
import {
|
|
||||||
generateAutomationPayload,
|
|
||||||
getAttributes,
|
|
||||||
getInputType,
|
|
||||||
getOperators,
|
|
||||||
getCustomAttributeType,
|
|
||||||
showActionInput,
|
|
||||||
} from 'dashboard/helper/automationHelper';
|
|
||||||
import { AUTOMATION_RULE_EVENTS, AUTOMATION_ACTION_TYPES } from './constants';
|
|
||||||
|
|
||||||
const start_value = {
|
const emit = defineEmits(['saveAutomation']);
|
||||||
|
|
||||||
|
const START_VALUE = {
|
||||||
name: null,
|
name: null,
|
||||||
description: null,
|
description: null,
|
||||||
event_name: 'conversation_created',
|
event_name: 'conversation_created',
|
||||||
@ -36,318 +27,60 @@ const start_value = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
const store = useStore();
|
||||||
components: {
|
const formRef = ref(null);
|
||||||
FilterInputBox,
|
|
||||||
AutomationActionInput,
|
|
||||||
NextButton,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
onClose: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['saveAutomation'],
|
|
||||||
setup() {
|
|
||||||
const {
|
|
||||||
automation,
|
|
||||||
automationTypes,
|
|
||||||
onEventChange,
|
|
||||||
getConditionDropdownValues,
|
|
||||||
appendNewCondition,
|
|
||||||
appendNewAction,
|
|
||||||
removeFilter,
|
|
||||||
removeAction,
|
|
||||||
resetFilter,
|
|
||||||
resetAction,
|
|
||||||
getActionDropdownValues,
|
|
||||||
manifestCustomAttributes,
|
|
||||||
} = useAutomation(start_value);
|
|
||||||
return {
|
|
||||||
automation,
|
|
||||||
automationTypes,
|
|
||||||
onEventChange,
|
|
||||||
getConditionDropdownValues,
|
|
||||||
appendNewCondition,
|
|
||||||
appendNewAction,
|
|
||||||
removeFilter,
|
|
||||||
removeAction,
|
|
||||||
resetFilter,
|
|
||||||
resetAction,
|
|
||||||
getActionDropdownValues,
|
|
||||||
manifestCustomAttributes,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
automationRuleEvent: AUTOMATION_RULE_EVENTS[0].key,
|
|
||||||
automationMutated: false,
|
|
||||||
show: true,
|
|
||||||
showDeleteConfirmationModal: false,
|
|
||||||
allCustomAttributes: [],
|
|
||||||
mode: 'create',
|
|
||||||
errors: {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
accountId: 'getCurrentAccountId',
|
|
||||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
|
||||||
}),
|
|
||||||
automationRuleEvents() {
|
|
||||||
return AUTOMATION_RULE_EVENTS.map(event => ({
|
|
||||||
...event,
|
|
||||||
value: this.$t(`AUTOMATION.EVENTS.${event.value}`),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
hasAutomationMutated() {
|
|
||||||
if (
|
|
||||||
this.automation.conditions[0].values ||
|
|
||||||
this.automation.actions[0].action_params.length
|
|
||||||
)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
automationActionTypes() {
|
|
||||||
const actionTypes = this.isFeatureEnabled('sla')
|
|
||||||
? AUTOMATION_ACTION_TYPES
|
|
||||||
: AUTOMATION_ACTION_TYPES.filter(({ key }) => key !== 'add_sla');
|
|
||||||
|
|
||||||
return actionTypes.map(action => ({
|
const {
|
||||||
...action,
|
automation,
|
||||||
label: this.$t(`AUTOMATION.ACTIONS.${action.label}`),
|
automationTypes,
|
||||||
}));
|
onEventChange,
|
||||||
},
|
getConditionDropdownValues,
|
||||||
},
|
appendNewCondition,
|
||||||
mounted() {
|
appendNewAction,
|
||||||
this.$store.dispatch('inboxes/get');
|
removeFilter,
|
||||||
this.$store.dispatch('agents/get');
|
removeAction,
|
||||||
this.$store.dispatch('contacts/get');
|
resetAction,
|
||||||
this.$store.dispatch('teams/get');
|
getActionDropdownValues,
|
||||||
this.$store.dispatch('labels/get');
|
manifestCustomAttributes,
|
||||||
this.$store.dispatch('campaigns/get');
|
} = useAutomation(START_VALUE);
|
||||||
this.allCustomAttributes = this.$store.getters['attributes/getAttributes'];
|
|
||||||
this.manifestCustomAttributes();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getAttributes,
|
|
||||||
getInputType,
|
|
||||||
getOperators,
|
|
||||||
getCustomAttributeType,
|
|
||||||
showActionInput,
|
|
||||||
isFeatureEnabled(flag) {
|
|
||||||
return this.isFeatureEnabledonAccount(this.accountId, flag);
|
|
||||||
},
|
|
||||||
emitSaveAutomation() {
|
|
||||||
this.errors = validateAutomation(this.automation);
|
|
||||||
if (Object.keys(this.errors).length === 0) {
|
|
||||||
const automation = generateAutomationPayload(this.automation);
|
|
||||||
this.$emit('saveAutomation', automation, this.mode);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getTranslatedAttributes(type, event) {
|
|
||||||
return getAttributes(type, event).map(attribute => {
|
|
||||||
// Skip translation
|
|
||||||
// 1. If customAttributeType key is present then its rendering attributes from API
|
|
||||||
// 2. If contact_custom_attribute or conversation_custom_attribute is present then its rendering section title
|
|
||||||
const skipTranslation =
|
|
||||||
attribute.customAttributeType ||
|
|
||||||
[
|
|
||||||
'contact_custom_attribute',
|
|
||||||
'conversation_custom_attribute',
|
|
||||||
].includes(attribute.key);
|
|
||||||
|
|
||||||
return {
|
const open = () => {
|
||||||
...attribute,
|
automation.value = structuredClone(START_VALUE);
|
||||||
name: skipTranslation
|
manifestCustomAttributes();
|
||||||
? attribute.name
|
formRef.value?.open();
|
||||||
: this.$t(`AUTOMATION.ATTRIBUTES.${attribute.name}`),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
const close = () => formRef.value?.close();
|
||||||
|
|
||||||
|
const onSave = (payload, mode) => {
|
||||||
|
emit('saveAutomation', payload, mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.dispatch('inboxes/get');
|
||||||
|
store.dispatch('agents/get');
|
||||||
|
store.dispatch('contacts/get');
|
||||||
|
store.dispatch('teams/get');
|
||||||
|
store.dispatch('labels/get');
|
||||||
|
store.dispatch('campaigns/get');
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ open, close });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<AutomationRuleForm
|
||||||
<woot-modal-header :header-title="$t('AUTOMATION.ADD.TITLE')" />
|
ref="formRef"
|
||||||
<div class="flex flex-col modal-content">
|
v-model:automation="automation"
|
||||||
<div class="w-full">
|
mode="create"
|
||||||
<woot-input
|
:automation-types="automationTypes"
|
||||||
v-model="automation.name"
|
:get-condition-dropdown-values="getConditionDropdownValues"
|
||||||
:label="$t('AUTOMATION.ADD.FORM.NAME.LABEL')"
|
:get-action-dropdown-values="getActionDropdownValues"
|
||||||
type="text"
|
:append-new-condition="appendNewCondition"
|
||||||
:class="{ error: errors.name }"
|
:append-new-action="appendNewAction"
|
||||||
:error="errors.name ? $t('AUTOMATION.ADD.FORM.NAME.ERROR') : ''"
|
:remove-filter="removeFilter"
|
||||||
:placeholder="$t('AUTOMATION.ADD.FORM.NAME.PLACEHOLDER')"
|
:remove-action="removeAction"
|
||||||
/>
|
:reset-action="resetAction"
|
||||||
<woot-input
|
:on-event-change="onEventChange"
|
||||||
v-model="automation.description"
|
@save="onSave"
|
||||||
:label="$t('AUTOMATION.ADD.FORM.DESC.LABEL')"
|
/>
|
||||||
type="text"
|
|
||||||
:class="{ error: errors.description }"
|
|
||||||
:error="
|
|
||||||
errors.description ? $t('AUTOMATION.ADD.FORM.DESC.ERROR') : ''
|
|
||||||
"
|
|
||||||
:placeholder="$t('AUTOMATION.ADD.FORM.DESC.PLACEHOLDER')"
|
|
||||||
/>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label :class="{ error: errors.event_name }">
|
|
||||||
{{ $t('AUTOMATION.ADD.FORM.EVENT.LABEL') }}
|
|
||||||
<select
|
|
||||||
v-model="automation.event_name"
|
|
||||||
class="m-0"
|
|
||||||
@change="onEventChange(automation)"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="event in automationRuleEvents"
|
|
||||||
:key="event.key"
|
|
||||||
:value="event.key"
|
|
||||||
>
|
|
||||||
{{ event.value }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span v-if="errors.event_name" class="message">
|
|
||||||
{{ $t('AUTOMATION.ADD.FORM.EVENT.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<p
|
|
||||||
v-if="hasAutomationMutated"
|
|
||||||
class="text-xs text-right text-n-teal-10 pt-1"
|
|
||||||
>
|
|
||||||
{{ $t('AUTOMATION.FORM.RESET_MESSAGE') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- // Conditions Start -->
|
|
||||||
<section>
|
|
||||||
<label>
|
|
||||||
{{ $t('AUTOMATION.ADD.FORM.CONDITIONS.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="w-full p-4 mb-4 border border-solid rounded-lg bg-n-slate-2 dark:bg-n-solid-2 border-n-strong"
|
|
||||||
>
|
|
||||||
<FilterInputBox
|
|
||||||
v-for="(condition, i) in automation.conditions"
|
|
||||||
:key="i"
|
|
||||||
v-model="automation.conditions[i]"
|
|
||||||
:filter-attributes="
|
|
||||||
getTranslatedAttributes(automationTypes, automation.event_name)
|
|
||||||
"
|
|
||||||
:input-type="
|
|
||||||
getInputType(
|
|
||||||
allCustomAttributes,
|
|
||||||
automationTypes,
|
|
||||||
automation,
|
|
||||||
automation.conditions[i].attribute_key
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:operators="
|
|
||||||
getOperators(
|
|
||||||
allCustomAttributes,
|
|
||||||
automationTypes,
|
|
||||||
automation,
|
|
||||||
mode,
|
|
||||||
automation.conditions[i].attribute_key
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:dropdown-values="
|
|
||||||
getConditionDropdownValues(
|
|
||||||
automation.conditions[i].attribute_key
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:show-query-operator="i !== automation.conditions.length - 1"
|
|
||||||
:custom-attribute-type="
|
|
||||||
getCustomAttributeType(
|
|
||||||
automationTypes,
|
|
||||||
automation,
|
|
||||||
automation.conditions[i].attribute_key
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:error-message="
|
|
||||||
errors[`condition_${i}`]
|
|
||||||
? $t(`AUTOMATION.ERRORS.${errors[`condition_${i}`]}`)
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
@reset-filter="resetFilter(i, automation.conditions[i])"
|
|
||||||
@remove-filter="removeFilter(i)"
|
|
||||||
/>
|
|
||||||
<div class="mt-4">
|
|
||||||
<NextButton
|
|
||||||
icon="i-lucide-plus"
|
|
||||||
blue
|
|
||||||
faded
|
|
||||||
sm
|
|
||||||
:label="$t('AUTOMATION.ADD.CONDITION_BUTTON_LABEL')"
|
|
||||||
@click="appendNewCondition"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- // Conditions End -->
|
|
||||||
<!-- // Actions Start -->
|
|
||||||
<section>
|
|
||||||
<label>
|
|
||||||
{{ $t('AUTOMATION.ADD.FORM.ACTIONS.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="w-full p-4 mb-4 border border-solid rounded-lg bg-n-slate-2 dark:bg-n-solid-2 border-n-strong"
|
|
||||||
>
|
|
||||||
<AutomationActionInput
|
|
||||||
v-for="(action, i) in automation.actions"
|
|
||||||
:key="i"
|
|
||||||
v-model="automation.actions[i]"
|
|
||||||
:action-types="automationActionTypes"
|
|
||||||
:dropdown-values="
|
|
||||||
getActionDropdownValues(automation.actions[i].action_name)
|
|
||||||
"
|
|
||||||
:show-action-input="
|
|
||||||
showActionInput(
|
|
||||||
automationActionTypes,
|
|
||||||
automation.actions[i].action_name
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:error-message="
|
|
||||||
errors[`action_${i}`]
|
|
||||||
? $t(`AUTOMATION.ERRORS.${errors[`action_${i}`]}`)
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
@reset-action="resetAction(i)"
|
|
||||||
@remove-action="removeAction(i)"
|
|
||||||
/>
|
|
||||||
<div class="mt-4">
|
|
||||||
<NextButton
|
|
||||||
icon="i-lucide-plus"
|
|
||||||
blue
|
|
||||||
faded
|
|
||||||
sm
|
|
||||||
:label="$t('AUTOMATION.ADD.ACTION_BUTTON_LABEL')"
|
|
||||||
@click="appendNewAction"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- // Actions End -->
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
|
||||||
<NextButton
|
|
||||||
faded
|
|
||||||
slate
|
|
||||||
type="reset"
|
|
||||||
:label="$t('AUTOMATION.ADD.CANCEL_BUTTON_TEXT')"
|
|
||||||
@click.prevent="onClose"
|
|
||||||
/>
|
|
||||||
<NextButton
|
|
||||||
solid
|
|
||||||
blue
|
|
||||||
type="submit"
|
|
||||||
:label="$t('AUTOMATION.ADD.SUBMIT')"
|
|
||||||
@click="emitSaveAutomation"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -0,0 +1,414 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, h, useTemplateRef, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
|
import { useOperators } from 'dashboard/components-next/filter/operators';
|
||||||
|
import ConditionRow from 'dashboard/components-next/filter/ConditionRow.vue';
|
||||||
|
import AutomationActionInput from 'dashboard/components/widgets/AutomationActionInput.vue';
|
||||||
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
|
import {
|
||||||
|
generateAutomationPayload,
|
||||||
|
getAttributes,
|
||||||
|
getFileName,
|
||||||
|
showActionInput,
|
||||||
|
} from 'dashboard/helper/automationHelper';
|
||||||
|
import { validateAutomation } from 'dashboard/helper/validations';
|
||||||
|
import { AUTOMATION_RULE_EVENTS, AUTOMATION_ACTION_TYPES } from './constants';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validator: value => ['create', 'edit'].includes(value),
|
||||||
|
},
|
||||||
|
automationTypes: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
getConditionDropdownValues: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
getActionDropdownValues: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
appendNewCondition: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
appendNewAction: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
removeFilter: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
removeAction: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
resetAction: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
onEventChange: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['save']);
|
||||||
|
const automation = defineModel('automation', { type: Object, default: null });
|
||||||
|
|
||||||
|
const INPUT_TYPE_MAP = {
|
||||||
|
multi_select: 'multiSelect',
|
||||||
|
search_select: 'searchSelect',
|
||||||
|
plain_text: 'plainText',
|
||||||
|
comma_separated_plain_text: 'plainText',
|
||||||
|
date: 'date',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { isCloudFeatureEnabled } = useAccount();
|
||||||
|
const { operators } = useOperators();
|
||||||
|
|
||||||
|
const dialogRef = ref(null);
|
||||||
|
const conditionsRef = useTemplateRef('conditionsRef');
|
||||||
|
const errors = ref({});
|
||||||
|
|
||||||
|
const isEditMode = computed(() => props.mode === 'edit');
|
||||||
|
|
||||||
|
const titleKey = computed(() =>
|
||||||
|
isEditMode.value ? 'AUTOMATION.EDIT.TITLE' : 'AUTOMATION.ADD.TITLE'
|
||||||
|
);
|
||||||
|
const cancelKey = computed(() =>
|
||||||
|
isEditMode.value
|
||||||
|
? 'AUTOMATION.EDIT.CANCEL_BUTTON_TEXT'
|
||||||
|
: 'AUTOMATION.ADD.CANCEL_BUTTON_TEXT'
|
||||||
|
);
|
||||||
|
const submitKey = computed(() =>
|
||||||
|
isEditMode.value ? 'AUTOMATION.EDIT.SUBMIT' : 'AUTOMATION.ADD.SUBMIT'
|
||||||
|
);
|
||||||
|
|
||||||
|
const getTranslatedAttributes = (type, event) => {
|
||||||
|
return getAttributes(type, event).map(attribute => {
|
||||||
|
const skipTranslation =
|
||||||
|
attribute.customAttributeType ||
|
||||||
|
['contact_custom_attribute', 'conversation_custom_attribute'].includes(
|
||||||
|
attribute.key
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...attribute,
|
||||||
|
name: skipTranslation
|
||||||
|
? attribute.name
|
||||||
|
: t(`AUTOMATION.ATTRIBUTES.${attribute.name}`),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventName = computed(() => automation.value?.event_name);
|
||||||
|
|
||||||
|
const filterTypes = computed(() => {
|
||||||
|
const event = eventName.value;
|
||||||
|
if (!event || !props.automationTypes[event]) return [];
|
||||||
|
|
||||||
|
const attributes = getTranslatedAttributes(props.automationTypes, event);
|
||||||
|
|
||||||
|
return attributes.map(attr => {
|
||||||
|
if (attr.disabled) {
|
||||||
|
return { value: attr.key, label: attr.name, disabled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedInputType = INPUT_TYPE_MAP[attr.inputType] || 'plainText';
|
||||||
|
const options = props.getConditionDropdownValues(attr.key) || [];
|
||||||
|
|
||||||
|
const filterOperators = (attr.filterOperators || []).map(op => {
|
||||||
|
const enriched = operators.value[op.value];
|
||||||
|
if (enriched) return enriched;
|
||||||
|
return {
|
||||||
|
value: op.value,
|
||||||
|
label: t(`FILTER.OPERATOR_LABELS.${op.value}`),
|
||||||
|
hasInput: true,
|
||||||
|
inputOverride: null,
|
||||||
|
icon: h('span', { class: 'i-ph-equals-bold !text-n-blue-11' }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
attributeKey: attr.key,
|
||||||
|
value: attr.key,
|
||||||
|
attributeName: attr.name,
|
||||||
|
label: attr.name,
|
||||||
|
inputType: mappedInputType,
|
||||||
|
options,
|
||||||
|
filterOperators,
|
||||||
|
dataType: 'text',
|
||||||
|
attributeModel: attr.customAttributeType || 'standard',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const automationRuleEvents = computed(() =>
|
||||||
|
AUTOMATION_RULE_EVENTS.map(event => ({
|
||||||
|
...event,
|
||||||
|
value: t(`AUTOMATION.EVENTS.${event.value}`),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasAutomationMutated = computed(() => {
|
||||||
|
return Boolean(
|
||||||
|
automation.value?.conditions[0]?.values ||
|
||||||
|
automation.value?.actions[0]?.action_params?.length
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const automationActionTypes = computed(() => {
|
||||||
|
const actionTypes = isCloudFeatureEnabled('sla')
|
||||||
|
? AUTOMATION_ACTION_TYPES
|
||||||
|
: AUTOMATION_ACTION_TYPES.filter(({ key }) => key !== 'add_sla');
|
||||||
|
|
||||||
|
return actionTypes.map(action => ({
|
||||||
|
...action,
|
||||||
|
label: t(`AUTOMATION.ACTIONS.${action.label}`),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasConditionErrors = computed(() =>
|
||||||
|
Object.keys(errors.value).some(key => key.startsWith('condition_'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasActionErrors = computed(() =>
|
||||||
|
Object.keys(errors.value).some(key => key.startsWith('action_'))
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => automation.value,
|
||||||
|
() => {
|
||||||
|
if (Object.keys(errors.value).length) {
|
||||||
|
errors.value = {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const isConditionsValid = () => {
|
||||||
|
if (!conditionsRef.value) return true;
|
||||||
|
return conditionsRef.value.every(condition => condition.validate());
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetValidation = () => {
|
||||||
|
errors.value = {};
|
||||||
|
conditionsRef.value?.forEach(c => c.resetValidation());
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncCustomAttributeTypes = () => {
|
||||||
|
automation.value.conditions.forEach(condition => {
|
||||||
|
const filterType = filterTypes.value.find(
|
||||||
|
ft => ft.attributeKey === condition.attribute_key
|
||||||
|
);
|
||||||
|
condition.custom_attribute_type =
|
||||||
|
filterType?.attributeModel === 'standard'
|
||||||
|
? ''
|
||||||
|
: filterType?.attributeModel || '';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
resetValidation();
|
||||||
|
dialogRef.value?.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
resetValidation();
|
||||||
|
dialogRef.value?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitSaveAutomation = () => {
|
||||||
|
syncCustomAttributeTypes();
|
||||||
|
const conditionsValid = isConditionsValid();
|
||||||
|
errors.value = validateAutomation(automation.value);
|
||||||
|
if (Object.keys(errors.value).length === 0 && conditionsValid) {
|
||||||
|
const payload = generateAutomationPayload(automation.value);
|
||||||
|
emit('save', payload, props.mode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open, close });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
width="3xl"
|
||||||
|
position="top"
|
||||||
|
:title="$t(titleKey)"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
overflow-y-auto
|
||||||
|
>
|
||||||
|
<div v-if="automation" class="flex flex-col w-full">
|
||||||
|
<woot-input
|
||||||
|
v-model="automation.name"
|
||||||
|
:label="$t('AUTOMATION.ADD.FORM.NAME.LABEL')"
|
||||||
|
type="text"
|
||||||
|
:class="{ error: errors.name }"
|
||||||
|
:error="errors.name ? $t('AUTOMATION.ADD.FORM.NAME.ERROR') : ''"
|
||||||
|
:placeholder="$t('AUTOMATION.ADD.FORM.NAME.PLACEHOLDER')"
|
||||||
|
/>
|
||||||
|
<woot-input
|
||||||
|
v-model="automation.description"
|
||||||
|
:label="$t('AUTOMATION.ADD.FORM.DESC.LABEL')"
|
||||||
|
type="text"
|
||||||
|
:class="{ error: errors.description }"
|
||||||
|
:error="errors.description ? $t('AUTOMATION.ADD.FORM.DESC.ERROR') : ''"
|
||||||
|
:placeholder="$t('AUTOMATION.ADD.FORM.DESC.PLACEHOLDER')"
|
||||||
|
/>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label :class="{ error: errors.event_name }">
|
||||||
|
{{ $t('AUTOMATION.ADD.FORM.EVENT.LABEL') }}
|
||||||
|
<select
|
||||||
|
v-model="automation.event_name"
|
||||||
|
class="m-0"
|
||||||
|
@change="onEventChange()"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="event in automationRuleEvents"
|
||||||
|
:key="event.key"
|
||||||
|
:value="event.key"
|
||||||
|
>
|
||||||
|
{{ event.value }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<span v-if="errors.event_name" class="message">
|
||||||
|
{{ $t('AUTOMATION.ADD.FORM.EVENT.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<p
|
||||||
|
v-if="!isEditMode && hasAutomationMutated"
|
||||||
|
class="text-xs text-right text-n-teal-10 pt-1"
|
||||||
|
>
|
||||||
|
{{ $t('AUTOMATION.FORM.RESET_MESSAGE') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- Conditions Start -->
|
||||||
|
<section class="mb-5">
|
||||||
|
<label>
|
||||||
|
{{ $t('AUTOMATION.ADD.FORM.CONDITIONS.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
class="grid gap-4 list-none p-3 mb-4 outline outline-1 rounded-xl -outline-offset-1"
|
||||||
|
:class="
|
||||||
|
hasConditionErrors
|
||||||
|
? 'outline-n-ruby-5 bg-n-ruby-2/50'
|
||||||
|
: 'outline-n-weak dark:outline-n-strong'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template v-for="(condition, i) in automation.conditions" :key="i">
|
||||||
|
<ConditionRow
|
||||||
|
v-if="i === 0"
|
||||||
|
ref="conditionsRef"
|
||||||
|
v-model:attribute-key="automation.conditions[i].attribute_key"
|
||||||
|
v-model:filter-operator="automation.conditions[i].filter_operator"
|
||||||
|
v-model:values="automation.conditions[i].values"
|
||||||
|
:filter-types="filterTypes"
|
||||||
|
:show-query-operator="false"
|
||||||
|
@remove="removeFilter(i)"
|
||||||
|
/>
|
||||||
|
<ConditionRow
|
||||||
|
v-else
|
||||||
|
ref="conditionsRef"
|
||||||
|
v-model:attribute-key="automation.conditions[i].attribute_key"
|
||||||
|
v-model:filter-operator="automation.conditions[i].filter_operator"
|
||||||
|
v-model:query-operator="
|
||||||
|
automation.conditions[i - 1].query_operator
|
||||||
|
"
|
||||||
|
v-model:values="automation.conditions[i].values"
|
||||||
|
:filter-types="filterTypes"
|
||||||
|
show-query-operator
|
||||||
|
@remove="removeFilter(i)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<NextButton
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
blue
|
||||||
|
faded
|
||||||
|
sm
|
||||||
|
:label="$t('AUTOMATION.ADD.CONDITION_BUTTON_LABEL')"
|
||||||
|
@click="appendNewCondition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<!-- Conditions End -->
|
||||||
|
<!-- Actions Start -->
|
||||||
|
<section>
|
||||||
|
<label>
|
||||||
|
{{ $t('AUTOMATION.ADD.FORM.ACTIONS.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
class="grid list-none p-3 mb-4 outline outline-1 rounded-xl -outline-offset-1 border-solid"
|
||||||
|
:class="
|
||||||
|
hasActionErrors
|
||||||
|
? 'outline-n-ruby-5 bg-n-ruby-2/50'
|
||||||
|
: 'outline-n-weak dark:outline-n-strong'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AutomationActionInput
|
||||||
|
v-for="(action, i) in automation.actions"
|
||||||
|
:key="i"
|
||||||
|
v-model="automation.actions[i]"
|
||||||
|
:action-types="automationActionTypes"
|
||||||
|
dropdown-max-height="max-h-[7.5rem]"
|
||||||
|
:dropdown-values="getActionDropdownValues(action.action_name)"
|
||||||
|
:show-action-input="
|
||||||
|
showActionInput(automationActionTypes, action.action_name)
|
||||||
|
"
|
||||||
|
:error-message="
|
||||||
|
errors[`action_${i}`]
|
||||||
|
? $t(`AUTOMATION.ERRORS.${errors[`action_${i}`]}`)
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
:initial-file-name="
|
||||||
|
isEditMode ? getFileName(action, automation.files) : ''
|
||||||
|
"
|
||||||
|
@reset-action="resetAction(i)"
|
||||||
|
@remove-action="removeAction(i)"
|
||||||
|
/>
|
||||||
|
<div class="pt-2">
|
||||||
|
<NextButton
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
blue
|
||||||
|
faded
|
||||||
|
sm
|
||||||
|
:label="$t('AUTOMATION.ADD.ACTION_BUTTON_LABEL')"
|
||||||
|
@click="appendNewAction"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<!-- Actions End -->
|
||||||
|
<div class="w-full mt-8">
|
||||||
|
<div class="flex flex-row justify-end w-full gap-2 px-0 py-4">
|
||||||
|
<NextButton
|
||||||
|
faded
|
||||||
|
slate
|
||||||
|
type="reset"
|
||||||
|
:label="$t(cancelKey)"
|
||||||
|
@click.prevent="close"
|
||||||
|
/>
|
||||||
|
<NextButton
|
||||||
|
solid
|
||||||
|
blue
|
||||||
|
type="submit"
|
||||||
|
:label="$t(submitKey)"
|
||||||
|
@click="emitSaveAutomation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@ -1,345 +1,80 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { mapGetters } from 'vuex';
|
import { ref, watch } from 'vue';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useAutomation } from 'dashboard/composables/useAutomation';
|
import { useAutomation } from 'dashboard/composables/useAutomation';
|
||||||
import { useEditableAutomation } from 'dashboard/composables/useEditableAutomation';
|
import { useEditableAutomation } from 'dashboard/composables/useEditableAutomation';
|
||||||
import FilterInputBox from 'dashboard/components/widgets/FilterInput/Index.vue';
|
import AutomationRuleForm from './AutomationRuleForm.vue';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import { AUTOMATION_ACTION_TYPES } from './constants';
|
||||||
import AutomationActionInput from 'dashboard/components/widgets/AutomationActionInput.vue';
|
|
||||||
import {
|
|
||||||
getFileName,
|
|
||||||
generateAutomationPayload,
|
|
||||||
getAttributes,
|
|
||||||
getInputType,
|
|
||||||
getOperators,
|
|
||||||
getCustomAttributeType,
|
|
||||||
showActionInput,
|
|
||||||
} from 'dashboard/helper/automationHelper';
|
|
||||||
import { validateAutomation } from 'dashboard/helper/validations';
|
|
||||||
|
|
||||||
import { AUTOMATION_RULE_EVENTS, AUTOMATION_ACTION_TYPES } from './constants';
|
const props = defineProps({
|
||||||
|
selectedResponse: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
const emit = defineEmits(['saveAutomation']);
|
||||||
components: {
|
|
||||||
FilterInputBox,
|
const allCustomAttributes = useMapGetter('attributes/getAttributes');
|
||||||
NextButton,
|
const formRef = ref(null);
|
||||||
AutomationActionInput,
|
|
||||||
},
|
const {
|
||||||
props: {
|
automation,
|
||||||
onClose: {
|
automationTypes,
|
||||||
type: Function,
|
onEventChange,
|
||||||
default: () => {},
|
getConditionDropdownValues,
|
||||||
},
|
appendNewCondition,
|
||||||
selectedResponse: {
|
appendNewAction,
|
||||||
type: Object,
|
removeFilter,
|
||||||
default: () => {},
|
removeAction,
|
||||||
},
|
resetAction,
|
||||||
},
|
getActionDropdownValues,
|
||||||
emits: ['saveAutomation'],
|
manifestCustomAttributes,
|
||||||
setup() {
|
} = useAutomation();
|
||||||
const {
|
|
||||||
automation,
|
const { formatAutomation } = useEditableAutomation();
|
||||||
|
|
||||||
|
const open = () => formRef.value?.open();
|
||||||
|
const close = () => formRef.value?.close();
|
||||||
|
|
||||||
|
const onSave = (payload, mode) => {
|
||||||
|
emit('saveAutomation', payload, mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selectedResponse,
|
||||||
|
value => {
|
||||||
|
if (!value?.conditions) return;
|
||||||
|
|
||||||
|
manifestCustomAttributes();
|
||||||
|
|
||||||
|
automation.value = formatAutomation(
|
||||||
|
value,
|
||||||
|
allCustomAttributes.value,
|
||||||
automationTypes,
|
automationTypes,
|
||||||
onEventChange,
|
AUTOMATION_ACTION_TYPES
|
||||||
getConditionDropdownValues,
|
|
||||||
appendNewCondition,
|
|
||||||
appendNewAction,
|
|
||||||
removeFilter,
|
|
||||||
removeAction,
|
|
||||||
resetFilter,
|
|
||||||
resetAction,
|
|
||||||
getActionDropdownValues,
|
|
||||||
manifestCustomAttributes,
|
|
||||||
} = useAutomation();
|
|
||||||
const { formatAutomation } = useEditableAutomation();
|
|
||||||
return {
|
|
||||||
automation,
|
|
||||||
automationTypes,
|
|
||||||
onEventChange,
|
|
||||||
getConditionDropdownValues,
|
|
||||||
appendNewCondition,
|
|
||||||
appendNewAction,
|
|
||||||
removeFilter,
|
|
||||||
removeAction,
|
|
||||||
resetFilter,
|
|
||||||
resetAction,
|
|
||||||
getActionDropdownValues,
|
|
||||||
formatAutomation,
|
|
||||||
manifestCustomAttributes,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
automationRuleEvent: AUTOMATION_RULE_EVENTS[0].key,
|
|
||||||
automationMutated: false,
|
|
||||||
show: true,
|
|
||||||
showDeleteConfirmationModal: false,
|
|
||||||
allCustomAttributes: [],
|
|
||||||
mode: 'edit',
|
|
||||||
errors: {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
accountId: 'getCurrentAccountId',
|
|
||||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
|
||||||
}),
|
|
||||||
automationRuleEvents() {
|
|
||||||
return AUTOMATION_RULE_EVENTS.map(event => ({
|
|
||||||
...event,
|
|
||||||
value: this.$t(`AUTOMATION.EVENTS.${event.value}`),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
hasAutomationMutated() {
|
|
||||||
if (
|
|
||||||
this.automation.conditions[0].values ||
|
|
||||||
this.automation.actions[0].action_params.length
|
|
||||||
)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
automationActionTypes() {
|
|
||||||
const actionTypes = this.isFeatureEnabled('sla')
|
|
||||||
? AUTOMATION_ACTION_TYPES
|
|
||||||
: AUTOMATION_ACTION_TYPES.filter(({ key }) => key !== 'add_sla');
|
|
||||||
|
|
||||||
return actionTypes.map(action => ({
|
|
||||||
...action,
|
|
||||||
label: this.$t(`AUTOMATION.ACTIONS.${action.label}`),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.manifestCustomAttributes();
|
|
||||||
this.allCustomAttributes = this.$store.getters['attributes/getAttributes'];
|
|
||||||
|
|
||||||
this.automation = this.formatAutomation(
|
|
||||||
this.selectedResponse,
|
|
||||||
this.allCustomAttributes,
|
|
||||||
this.automationTypes,
|
|
||||||
this.automationActionTypes
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
methods: {
|
{ immediate: true }
|
||||||
getFileName,
|
);
|
||||||
getAttributes,
|
|
||||||
getInputType,
|
|
||||||
getOperators,
|
|
||||||
getCustomAttributeType,
|
|
||||||
showActionInput,
|
|
||||||
isFeatureEnabled(flag) {
|
|
||||||
return this.isFeatureEnabledonAccount(this.accountId, flag);
|
|
||||||
},
|
|
||||||
emitSaveAutomation() {
|
|
||||||
this.errors = validateAutomation(this.automation);
|
|
||||||
if (Object.keys(this.errors).length === 0) {
|
|
||||||
const automation = generateAutomationPayload(this.automation);
|
|
||||||
this.$emit('saveAutomation', automation, this.mode);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getTranslatedAttributes(type, event) {
|
|
||||||
return getAttributes(type, event).map(attribute => {
|
|
||||||
// Skip translation
|
|
||||||
// 1. If customAttributeType key is present then its rendering attributes from API
|
|
||||||
// 2. If contact_custom_attribute or conversation_custom_attribute is present then its rendering section title
|
|
||||||
const skipTranslation =
|
|
||||||
attribute.customAttributeType ||
|
|
||||||
[
|
|
||||||
'contact_custom_attribute',
|
|
||||||
'conversation_custom_attribute',
|
|
||||||
].includes(attribute.key);
|
|
||||||
|
|
||||||
return {
|
defineExpose({ open, close });
|
||||||
...attribute,
|
|
||||||
name: skipTranslation
|
|
||||||
? attribute.name
|
|
||||||
: this.$t(`AUTOMATION.ATTRIBUTES.${attribute.name}`),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<AutomationRuleForm
|
||||||
<woot-modal-header :header-title="$t('AUTOMATION.EDIT.TITLE')" />
|
ref="formRef"
|
||||||
<div class="flex flex-col modal-content">
|
v-model:automation="automation"
|
||||||
<div v-if="automation" class="w-full">
|
mode="edit"
|
||||||
<woot-input
|
:automation-types="automationTypes"
|
||||||
v-model="automation.name"
|
:get-condition-dropdown-values="getConditionDropdownValues"
|
||||||
:label="$t('AUTOMATION.ADD.FORM.NAME.LABEL')"
|
:get-action-dropdown-values="getActionDropdownValues"
|
||||||
type="text"
|
:append-new-condition="appendNewCondition"
|
||||||
:class="{ error: errors.name }"
|
:append-new-action="appendNewAction"
|
||||||
:error="errors.name ? $t('AUTOMATION.ADD.FORM.NAME.ERROR') : ''"
|
:remove-filter="removeFilter"
|
||||||
:placeholder="$t('AUTOMATION.ADD.FORM.NAME.PLACEHOLDER')"
|
:remove-action="removeAction"
|
||||||
/>
|
:reset-action="resetAction"
|
||||||
<woot-input
|
:on-event-change="onEventChange"
|
||||||
v-model="automation.description"
|
@save="onSave"
|
||||||
:label="$t('AUTOMATION.ADD.FORM.DESC.LABEL')"
|
/>
|
||||||
type="text"
|
|
||||||
:class="{ error: errors.description }"
|
|
||||||
:error="
|
|
||||||
errors.description ? $t('AUTOMATION.ADD.FORM.DESC.ERROR') : ''
|
|
||||||
"
|
|
||||||
:placeholder="$t('AUTOMATION.ADD.FORM.DESC.PLACEHOLDER')"
|
|
||||||
/>
|
|
||||||
<div class="event_wrapper">
|
|
||||||
<label :class="{ error: errors.event_name }">
|
|
||||||
{{ $t('AUTOMATION.ADD.FORM.EVENT.LABEL') }}
|
|
||||||
<select
|
|
||||||
v-model="automation.event_name"
|
|
||||||
@change="onEventChange(automation)"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="event in automationRuleEvents"
|
|
||||||
:key="event.key"
|
|
||||||
:value="event.key"
|
|
||||||
>
|
|
||||||
{{ event.value }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span v-if="errors.event_name" class="message">
|
|
||||||
{{ $t('AUTOMATION.ADD.FORM.EVENT.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<!-- // Conditions Start -->
|
|
||||||
<section>
|
|
||||||
<label>
|
|
||||||
{{ $t('AUTOMATION.ADD.FORM.CONDITIONS.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="w-full p-4 mb-4 border border-solid rounded-lg bg-n-slate-2 dark:bg-n-solid-2 border-n-strong"
|
|
||||||
>
|
|
||||||
<FilterInputBox
|
|
||||||
v-for="(condition, i) in automation.conditions"
|
|
||||||
:key="i"
|
|
||||||
v-model="automation.conditions[i]"
|
|
||||||
:filter-attributes="
|
|
||||||
getTranslatedAttributes(automationTypes, automation.event_name)
|
|
||||||
"
|
|
||||||
:input-type="
|
|
||||||
getInputType(
|
|
||||||
allCustomAttributes,
|
|
||||||
automationTypes,
|
|
||||||
automation,
|
|
||||||
automation.conditions[i].attribute_key
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:operators="
|
|
||||||
getOperators(
|
|
||||||
allCustomAttributes,
|
|
||||||
automationTypes,
|
|
||||||
automation,
|
|
||||||
mode,
|
|
||||||
automation.conditions[i].attribute_key
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:dropdown-values="
|
|
||||||
getConditionDropdownValues(
|
|
||||||
automation.conditions[i].attribute_key
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:custom-attribute-type="
|
|
||||||
getCustomAttributeType(
|
|
||||||
automationTypes,
|
|
||||||
automation,
|
|
||||||
automation.conditions[i].attribute_key
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:show-query-operator="i !== automation.conditions.length - 1"
|
|
||||||
:error-message="
|
|
||||||
errors[`condition_${i}`]
|
|
||||||
? $t(`AUTOMATION.ERRORS.${errors[`condition_${i}`]}`)
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
@reset-filter="resetFilter(i, automation.conditions[i])"
|
|
||||||
@remove-filter="removeFilter(i)"
|
|
||||||
/>
|
|
||||||
<div class="mt-4">
|
|
||||||
<NextButton
|
|
||||||
icon="i-lucide-plus"
|
|
||||||
blue
|
|
||||||
faded
|
|
||||||
sm
|
|
||||||
:label="$t('AUTOMATION.ADD.CONDITION_BUTTON_LABEL')"
|
|
||||||
@click="appendNewCondition"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- // Conditions End -->
|
|
||||||
<!-- // Actions Start -->
|
|
||||||
<section>
|
|
||||||
<label>
|
|
||||||
{{ $t('AUTOMATION.ADD.FORM.ACTIONS.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="w-full p-4 mb-4 border border-solid rounded-lg bg-n-slate-2 dark:bg-n-solid-2 border-n-strong"
|
|
||||||
>
|
|
||||||
<AutomationActionInput
|
|
||||||
v-for="(action, i) in automation.actions"
|
|
||||||
:key="i"
|
|
||||||
v-model="automation.actions[i]"
|
|
||||||
:action-types="automationActionTypes"
|
|
||||||
:dropdown-values="getActionDropdownValues(action.action_name)"
|
|
||||||
:show-action-input="
|
|
||||||
showActionInput(automationActionTypes, action.action_name)
|
|
||||||
"
|
|
||||||
:error-message="
|
|
||||||
errors[`action_${i}`]
|
|
||||||
? $t(`AUTOMATION.ERRORS.${errors[`action_${i}`]}`)
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
:initial-file-name="getFileName(action, automation.files)"
|
|
||||||
@reset-action="resetAction(i)"
|
|
||||||
@remove-action="removeAction(i)"
|
|
||||||
/>
|
|
||||||
<div class="mt-4">
|
|
||||||
<NextButton
|
|
||||||
icon="i-lucide-plus"
|
|
||||||
blue
|
|
||||||
faded
|
|
||||||
sm
|
|
||||||
:label="$t('AUTOMATION.ADD.ACTION_BUTTON_LABEL')"
|
|
||||||
@click="appendNewAction"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!-- // Actions End -->
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
|
||||||
<NextButton
|
|
||||||
faded
|
|
||||||
slate
|
|
||||||
type="reset"
|
|
||||||
:label="$t('AUTOMATION.EDIT.CANCEL_BUTTON_TEXT')"
|
|
||||||
@click.prevent="onClose"
|
|
||||||
/>
|
|
||||||
<NextButton
|
|
||||||
solid
|
|
||||||
blue
|
|
||||||
type="submit"
|
|
||||||
:label="$t('AUTOMATION.EDIT.SUBMIT')"
|
|
||||||
@click="emitSaveAutomation"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.event_wrapper {
|
|
||||||
select {
|
|
||||||
@apply m-0;
|
|
||||||
}
|
|
||||||
.info-message {
|
|
||||||
@apply text-xs text-n-teal-10 text-right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@apply mb-6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -16,8 +16,8 @@ const { t } = useI18n();
|
|||||||
const confirmDialog = ref(null);
|
const confirmDialog = ref(null);
|
||||||
|
|
||||||
const loading = ref({});
|
const loading = ref({});
|
||||||
const showAddPopup = ref(false);
|
const addDialogRef = ref(null);
|
||||||
const showEditPopup = ref(false);
|
const editDialogRef = ref(null);
|
||||||
const showDeleteConfirmationPopup = ref(false);
|
const showDeleteConfirmationPopup = ref(false);
|
||||||
const selectedAutomation = ref({});
|
const selectedAutomation = ref({});
|
||||||
const toggleModalTitle = ref(t('AUTOMATION.TOGGLE.ACTIVATION_TITLE'));
|
const toggleModalTitle = ref(t('AUTOMATION.TOGGLE.ACTIVATION_TITLE'));
|
||||||
@ -57,18 +57,18 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const openAddPopup = () => {
|
const openAddPopup = () => {
|
||||||
showAddPopup.value = true;
|
addDialogRef.value?.open();
|
||||||
};
|
};
|
||||||
const hideAddPopup = () => {
|
const hideAddPopup = () => {
|
||||||
showAddPopup.value = false;
|
addDialogRef.value?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditPopup = response => {
|
const openEditPopup = response => {
|
||||||
selectedAutomation.value = response;
|
selectedAutomation.value = { ...response };
|
||||||
showEditPopup.value = true;
|
editDialogRef.value?.open();
|
||||||
};
|
};
|
||||||
const hideEditPopup = () => {
|
const hideEditPopup = () => {
|
||||||
showEditPopup.value = false;
|
editDialogRef.value?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDeletePopup = response => {
|
const openDeletePopup = response => {
|
||||||
@ -221,17 +221,7 @@ const tableHeaders = computed(() => {
|
|||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<woot-modal
|
<AddAutomationRule ref="addDialogRef" @save-automation="submitAutomation" />
|
||||||
v-model:show="showAddPopup"
|
|
||||||
size="medium"
|
|
||||||
:on-close="hideAddPopup"
|
|
||||||
>
|
|
||||||
<AddAutomationRule
|
|
||||||
v-if="showAddPopup"
|
|
||||||
:on-close="hideAddPopup"
|
|
||||||
@save-automation="submitAutomation"
|
|
||||||
/>
|
|
||||||
</woot-modal>
|
|
||||||
|
|
||||||
<woot-delete-modal
|
<woot-delete-modal
|
||||||
v-model:show="showDeleteConfirmationPopup"
|
v-model:show="showDeleteConfirmationPopup"
|
||||||
@ -244,18 +234,11 @@ const tableHeaders = computed(() => {
|
|||||||
:reject-text="deleteRejectText"
|
:reject-text="deleteRejectText"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<woot-modal
|
<EditAutomationRule
|
||||||
v-model:show="showEditPopup"
|
ref="editDialogRef"
|
||||||
size="medium"
|
:selected-response="selectedAutomation"
|
||||||
:on-close="hideEditPopup"
|
@save-automation="submitAutomation"
|
||||||
>
|
/>
|
||||||
<EditAutomationRule
|
|
||||||
v-if="showEditPopup"
|
|
||||||
:on-close="hideEditPopup"
|
|
||||||
:selected-response="selectedAutomation"
|
|
||||||
@save-automation="submitAutomation"
|
|
||||||
/>
|
|
||||||
</woot-modal>
|
|
||||||
<woot-confirm-modal
|
<woot-confirm-modal
|
||||||
ref="confirmDialog"
|
ref="confirmDialog"
|
||||||
:title="toggleModalTitle"
|
:title="toggleModalTitle"
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useAlert } from 'dashboard/composables';
|
|||||||
|
|
||||||
import InboxMembersAPI from '../../../../api/inboxMembers';
|
import InboxMembersAPI from '../../../../api/inboxMembers';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import TagInput from 'dashboard/components-next/taginput/TagInput.vue';
|
||||||
import router from '../../../index';
|
import router from '../../../index';
|
||||||
import PageHeader from '../SettingsSubPageHeader.vue';
|
import PageHeader from '../SettingsSubPageHeader.vue';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
@ -13,11 +14,12 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
PageHeader,
|
PageHeader,
|
||||||
NextButton,
|
NextButton,
|
||||||
|
TagInput,
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
selectedAgents: {
|
selectedAgentIds: {
|
||||||
isEmpty() {
|
isEmpty() {
|
||||||
return !!this.selectedAgents.length;
|
return !!this.selectedAgentIds.length;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -26,7 +28,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedAgents: [],
|
selectedAgentIds: [],
|
||||||
isCreating: false,
|
isCreating: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -34,18 +36,43 @@ export default {
|
|||||||
...mapGetters({
|
...mapGetters({
|
||||||
agentList: 'agents/getAgents',
|
agentList: 'agents/getAgents',
|
||||||
}),
|
}),
|
||||||
|
selectedAgentNames() {
|
||||||
|
return this.selectedAgentIds.map(
|
||||||
|
id => this.agentList.find(a => a.id === id)?.name ?? ''
|
||||||
|
);
|
||||||
|
},
|
||||||
|
agentMenuItems() {
|
||||||
|
return this.agentList
|
||||||
|
.filter(({ id }) => !this.selectedAgentIds.includes(id))
|
||||||
|
.map(({ id, name, thumbnail, avatar_url }) => ({
|
||||||
|
label: name,
|
||||||
|
value: id,
|
||||||
|
action: 'select',
|
||||||
|
thumbnail: { name, src: thumbnail || avatar_url || '' },
|
||||||
|
}));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('agents/get');
|
this.$store.dispatch('agents/get');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleAgentAdd({ value }) {
|
||||||
|
if (!this.selectedAgentIds.includes(value)) {
|
||||||
|
this.selectedAgentIds.push(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleAgentRemove(index) {
|
||||||
|
this.selectedAgentIds.splice(index, 1);
|
||||||
|
},
|
||||||
async addAgents() {
|
async addAgents() {
|
||||||
this.isCreating = true;
|
this.isCreating = true;
|
||||||
const inboxId = this.$route.params.inbox_id;
|
const inboxId = this.$route.params.inbox_id;
|
||||||
const selectedAgents = this.selectedAgents.map(x => x.id);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await InboxMembersAPI.update({ inboxId, agentList: selectedAgents });
|
await InboxMembersAPI.update({
|
||||||
|
inboxId,
|
||||||
|
agentList: this.selectedAgentIds,
|
||||||
|
});
|
||||||
router.replace({
|
router.replace({
|
||||||
name: 'settings_inbox_finish',
|
name: 'settings_inbox_finish',
|
||||||
params: {
|
params: {
|
||||||
@ -72,25 +99,23 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="w-full">
|
<div class="w-full mb-4">
|
||||||
<label :class="{ error: v$.selectedAgents.$error }">
|
<label :class="{ error: v$.selectedAgentIds.$error }">
|
||||||
{{ $t('INBOX_MGMT.ADD.AGENTS.TITLE') }}
|
{{ $t('INBOX_MGMT.ADD.AGENTS.TITLE') }}
|
||||||
<multiselect
|
<div
|
||||||
v-model="selectedAgents"
|
class="rounded-xl outline outline-1 -outline-offset-1 outline-n-weak hover:outline-n-strong px-2 py-2"
|
||||||
:options="agentList"
|
>
|
||||||
track-by="id"
|
<TagInput
|
||||||
label="name"
|
:model-value="selectedAgentNames"
|
||||||
multiple
|
:placeholder="$t('INBOX_MGMT.ADD.AGENTS.PICK_AGENTS')"
|
||||||
:close-on-select="false"
|
:menu-items="agentMenuItems"
|
||||||
:clear-on-select="false"
|
show-dropdown
|
||||||
hide-selected
|
skip-label-dedup
|
||||||
selected-label
|
@add="handleAgentAdd"
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
@remove="handleAgentRemove"
|
||||||
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
/>
|
||||||
:placeholder="$t('INBOX_MGMT.ADD.AGENTS.PICK_AGENTS')"
|
</div>
|
||||||
@select="v$.selectedAgents.$touch"
|
<span v-if="v$.selectedAgentIds.$error" class="message">
|
||||||
/>
|
|
||||||
<span v-if="v$.selectedAgents.$error" class="message">
|
|
||||||
{{ $t('INBOX_MGMT.ADD.AGENTS.VALIDATION_ERROR') }}
|
{{ $t('INBOX_MGMT.ADD.AGENTS.VALIDATION_ERROR') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import PageHeader from '../../SettingsSubPageHeader.vue';
|
|||||||
import router from '../../../../index';
|
import router from '../../../../index';
|
||||||
import { useBranding } from 'shared/composables/useBranding';
|
import { useBranding } from 'shared/composables/useBranding';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||||
|
|
||||||
import { loadScript } from 'dashboard/helper/DOMHelpers';
|
import { loadScript } from 'dashboard/helper/DOMHelpers';
|
||||||
import * as Sentry from '@sentry/vue';
|
import * as Sentry from '@sentry/vue';
|
||||||
@ -21,6 +22,7 @@ export default {
|
|||||||
LoadingState,
|
LoadingState,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
NextButton,
|
NextButton,
|
||||||
|
ComboBox,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { accountId } = useAccount();
|
const { accountId } = useAccount();
|
||||||
@ -67,6 +69,12 @@ export default {
|
|||||||
getSelectablePages() {
|
getSelectablePages() {
|
||||||
return this.pageList.filter(item => !item.exists);
|
return this.pageList.filter(item => !item.exists);
|
||||||
},
|
},
|
||||||
|
comboBoxPageOptions() {
|
||||||
|
return this.getSelectablePages.map(({ id, name }) => ({
|
||||||
|
value: id,
|
||||||
|
label: name,
|
||||||
|
}));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -94,9 +102,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setPageName({ name }) {
|
setPageName(pageId) {
|
||||||
|
const page = this.pageList.find(p => p.id === pageId);
|
||||||
|
if (page) {
|
||||||
|
this.selectedPage = page;
|
||||||
|
this.pageName = page.name;
|
||||||
|
} else {
|
||||||
|
this.selectedPage = { name: null, id: null };
|
||||||
|
this.pageName = '';
|
||||||
|
}
|
||||||
this.v$.selectedPage.$touch();
|
this.v$.selectedPage.$touch();
|
||||||
this.pageName = name;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
initChannelAuth(channel) {
|
initChannelAuth(channel) {
|
||||||
@ -245,23 +260,20 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-3/5">
|
<div class="w-3/5">
|
||||||
<div class="w-full">
|
<div class="w-full mb-2">
|
||||||
<div class="input-wrap" :class="{ error: v$.selectedPage.$error }">
|
<div class="input-wrap" :class="{ error: v$.selectedPage.$error }">
|
||||||
{{ $t('INBOX_MGMT.ADD.FB.CHOOSE_PAGE') }}
|
<span class="text-n-slate-12 text-start">
|
||||||
<multiselect
|
{{ $t('INBOX_MGMT.ADD.FB.CHOOSE_PAGE') }}
|
||||||
v-model="selectedPage"
|
</span>
|
||||||
close-on-select
|
<ComboBox
|
||||||
allow-empty
|
:model-value="selectedPage.id"
|
||||||
:options="getSelectablePages"
|
:options="comboBoxPageOptions"
|
||||||
track-by="id"
|
|
||||||
label="name"
|
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
|
||||||
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
|
||||||
:placeholder="$t('INBOX_MGMT.ADD.FB.PICK_A_VALUE')"
|
:placeholder="$t('INBOX_MGMT.ADD.FB.PICK_A_VALUE')"
|
||||||
selected-label
|
:has-error="v$.selectedPage.$error"
|
||||||
@select="setPageName"
|
class="[&>div>button]:!bg-n-alpha-black2 mt-1"
|
||||||
|
@update:model-value="setPageName"
|
||||||
/>
|
/>
|
||||||
<span v-if="v$.selectedPage.$error" class="message">
|
<span v-if="v$.selectedPage.$error" class="message mt-0.5">
|
||||||
{{ $t('INBOX_MGMT.ADD.FB.CHOOSE_PLACEHOLDER') }}
|
{{ $t('INBOX_MGMT.ADD.FB.CHOOSE_PLACEHOLDER') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user