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:
Sivin Varghese
2026-02-17 16:40:12 +05:30
committed by GitHub
parent 138840a23f
commit 229f56d6e3
31 changed files with 1209 additions and 1795 deletions

View File

@@ -11,11 +11,13 @@ import { isPhoneNumberValid } from 'shared/helpers/Validators';
import parsePhoneNumber from 'libphonenumber-js';
import NextButton from 'dashboard/components-next/button/Button.vue';
import Avatar from 'next/avatar/Avatar.vue';
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
export default {
components: {
NextButton,
Avatar,
ComboBox,
},
props: {
contact: {
@@ -133,6 +135,12 @@ export default {
if (!name && !id) return '';
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() {
if (
this.phoneNumber !== '' &&
@@ -363,26 +371,23 @@ export default {
:label="$t('CONTACT_FORM.FORM.COMPANY_NAME.LABEL')"
:placeholder="$t('CONTACT_FORM.FORM.COMPANY_NAME.PLACEHOLDER')"
/>
<div>
<div class="w-full">
<label>
{{ $t('CONTACT_FORM.FORM.COUNTRY.LABEL') }}
</label>
<multiselect
v-model="country"
track-by="id"
label="name"
:placeholder="$t('CONTACT_FORM.FORM.COUNTRY.PLACEHOLDER')"
selected-label
:select-label="$t('CONTACT_FORM.FORM.COUNTRY.SELECT_PLACEHOLDER')"
:deselect-label="$t('CONTACT_FORM.FORM.COUNTRY.REMOVE')"
:custom-label="countryNameWithCode"
:max-height="160"
:options="countries"
allow-empty
:option-height="104"
/>
</div>
<div class="w-full mb-4">
<label>
{{ $t('CONTACT_FORM.FORM.COUNTRY.LABEL') }}
</label>
<ComboBox
:model-value="country.id"
:options="
countries.map(c => ({
value: c.id,
label: countryNameWithCode(c),
}))
"
class="[&>div>button]:!bg-n-alpha-black2"
:placeholder="$t('CONTACT_FORM.FORM.COUNTRY.PLACEHOLDER')"
:search-placeholder="$t('CONTACT_FORM.FORM.COUNTRY.SELECT_PLACEHOLDER')"
@update:model-value="onCountryChange"
/>
</div>
<woot-input
v-model="city"
@@ -426,11 +431,3 @@ export default {
</div>
</form>
</template>
<style scoped lang="scss">
::v-deep {
.multiselect .multiselect__tags .multiselect__single {
@apply pl-0;
}
}
</style>

View File

@@ -51,7 +51,6 @@ export default {
data() {
return {
showEditModal: false,
showMergeModal: false,
showDeleteModal: false,
};
},
@@ -167,11 +166,8 @@ export default {
);
}
},
closeMergeModal() {
this.showMergeModal = false;
},
openMergeModal() {
this.showMergeModal = true;
this.$refs.mergeModal?.open();
},
},
};
@@ -324,12 +320,7 @@ export default {
:contact="contact"
@cancel="toggleEditModal"
/>
<ContactMergeModal
v-if="showMergeModal"
:primary-contact="contact"
:show="showMergeModal"
@close="closeMergeModal"
/>
<ContactMergeModal ref="mergeModal" :primary-contact="contact" />
</div>
<woot-delete-modal
v-if="showDeleteModal"

View File

@@ -7,10 +7,12 @@ import { convertToAttributeSlug } from 'dashboard/helper/commons.js';
import { ATTRIBUTE_MODELS, ATTRIBUTE_TYPES } from './constants';
import NextButton from 'dashboard/components-next/button/Button.vue';
import TagInput from 'dashboard/components-next/taginput/TagInput.vue';
export default {
components: {
NextButton,
TagInput,
},
props: {
onClose: {
@@ -41,9 +43,8 @@ export default {
regexCue: null,
regexEnabled: false,
values: [],
options: [],
show: true,
isTouched: false,
tagInputTouched: false,
};
},
@@ -63,21 +64,21 @@ export default {
option: this.$t(`ATTRIBUTES_MGMT.ATTRIBUTE_TYPES.${item.key}`),
}));
},
isMultiselectInvalid() {
return this.isTouched && this.values.length === 0;
},
isTagInputInvalid() {
isTagInputEmpty() {
return this.isAttributeTypeList && this.values.length === 0;
},
isTagInputInvalid() {
return this.tagInputTouched && this.isTagInputEmpty;
},
attributeListValues() {
return this.values.map(item => item.name);
return this.values;
},
isButtonDisabled() {
return (
this.v$.displayName.$invalid ||
this.v$.description.$invalid ||
this.uiFlags.isCreating ||
this.isTagInputInvalid
this.isTagInputEmpty
);
},
keyErrorMessage() {
@@ -119,17 +120,14 @@ export default {
},
},
watch: {
attributeType() {
this.tagInputTouched = false;
this.values = [];
},
},
methods: {
addTagValue(tagValue) {
const tag = {
name: tagValue,
};
this.values.push(tag);
this.$refs.tagInput.$el.focus();
},
onTouch() {
this.isTouched = true;
},
onDisplayNameChange() {
this.attributeKey = convertToAttributeSlug(this.displayName);
},
@@ -237,27 +235,25 @@ export default {
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
</span>
</label>
<div v-if="isAttributeTypeList" class="multiselect--wrap">
<label>
<div v-if="isAttributeTypeList" class="mb-4">
<label class="mb-1 block">
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.LABEL') }}
</label>
<multiselect
ref="tagInput"
v-model="values"
:placeholder="
$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')
"
label="name"
track-by="name"
:class="{ invalid: isMultiselectInvalid }"
:options="options"
multiple
taggable
@close="onTouch"
@tag="addTagValue"
/>
<div
class="rounded-xl border px-3 py-2"
:class="isTagInputInvalid ? 'border-n-ruby-9' : 'border-n-weak'"
>
<TagInput
v-model="values"
:placeholder="
$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')
"
allow-create
@blur="tagInputTouched = true"
/>
</div>
<label
v-show="isMultiselectInvalid"
v-show="isTagInputInvalid"
class="text-n-ruby-9 dark:text-n-ruby-9 text-sm font-normal mt-1"
>
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
@@ -312,22 +308,4 @@ export default {
padding: 0 0.5rem 0.5rem 0;
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>

View File

@@ -5,10 +5,12 @@ import { required, minLength } from '@vuelidate/validators';
import { getRegexp } from 'shared/helpers/Validators';
import { ATTRIBUTE_TYPES } from './constants';
import NextButton from 'dashboard/components-next/button/Button.vue';
import TagInput from 'dashboard/components-next/taginput/TagInput.vue';
export default {
components: {
NextButton,
TagInput,
},
props: {
selectedAttribute: {
@@ -35,8 +37,7 @@ export default {
show: true,
attributeKey: '',
values: [],
options: [],
isTouched: true,
tagInputTouched: false,
};
},
validations: {
@@ -65,20 +66,19 @@ export default {
}));
},
setAttributeListValue() {
return this.selectedAttribute.attribute_values.map(values => ({
name: values,
}));
return this.selectedAttribute.attribute_values || [];
},
updatedAttributeListValues() {
return this.values.map(item => item.name);
return this.values;
},
isButtonDisabled() {
return this.v$.description.$invalid || this.isMultiselectInvalid;
return this.v$.description.$invalid || this.isTagInputEmpty;
},
isMultiselectInvalid() {
return (
this.isAttributeTypeList && this.isTouched && this.values.length === 0
);
isTagInputEmpty() {
return this.isAttributeTypeList && this.values.length === 0;
},
isTagInputInvalid() {
return this.tagInputTouched && this.isTagInputEmpty;
},
pageTitle() {
@@ -116,13 +116,6 @@ export default {
onClose() {
this.$emit('onClose');
},
addTagValue(tagValue) {
const tag = {
name: tagValue,
};
this.values.push(tag);
this.$refs.tagInput.$el.focus();
},
setFormValues() {
const regexPattern = this.selectedAttribute.regex_pattern
? getRegexp(this.selectedAttribute.regex_pattern).source
@@ -225,24 +218,25 @@ export default {
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
</span>
</label>
<div v-if="isAttributeTypeList" class="multiselect--wrap">
<label>
<div v-if="isAttributeTypeList" class="mb-4">
<label class="mb-1 block">
{{ $t('ATTRIBUTES_MGMT.EDIT.TYPE.LIST.LABEL') }}
</label>
<multiselect
ref="tagInput"
v-model="values"
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')"
label="name"
track-by="name"
:class="{ invalid: isMultiselectInvalid }"
:options="options"
multiple
taggable
@tag="addTagValue"
/>
<div
class="rounded-xl border px-3 py-2"
:class="isTagInputInvalid ? 'border-n-ruby-9' : 'border-n-weak'"
>
<TagInput
v-model="values"
:placeholder="
$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')
"
allow-create
@blur="tagInputTouched = true"
/>
</div>
<label
v-show="isMultiselectInvalid"
v-show="isTagInputInvalid"
class="text-n-ruby-9 dark:text-n-ruby-9 text-sm font-normal mt-1"
>
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
@@ -297,22 +291,4 @@ export default {
padding: 0 0.5rem 0.5rem 0;
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>

View File

@@ -1,21 +1,12 @@
<script>
import { mapGetters } from 'vuex';
import FilterInputBox from 'dashboard/components/widgets/FilterInput/Index.vue';
import AutomationActionInput from 'dashboard/components/widgets/AutomationActionInput.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
<script setup>
import { ref, onMounted } from 'vue';
import { useStore } from 'dashboard/composables/store';
import { useAutomation } from 'dashboard/composables/useAutomation';
import { validateAutomation } from 'dashboard/helper/validations';
import {
generateAutomationPayload,
getAttributes,
getInputType,
getOperators,
getCustomAttributeType,
showActionInput,
} from 'dashboard/helper/automationHelper';
import { AUTOMATION_RULE_EVENTS, AUTOMATION_ACTION_TYPES } from './constants';
import AutomationRuleForm from './AutomationRuleForm.vue';
const start_value = {
const emit = defineEmits(['saveAutomation']);
const START_VALUE = {
name: null,
description: null,
event_name: 'conversation_created',
@@ -36,318 +27,60 @@ const start_value = {
],
};
export default {
components: {
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');
const store = useStore();
const formRef = ref(null);
return actionTypes.map(action => ({
...action,
label: this.$t(`AUTOMATION.ACTIONS.${action.label}`),
}));
},
},
mounted() {
this.$store.dispatch('inboxes/get');
this.$store.dispatch('agents/get');
this.$store.dispatch('contacts/get');
this.$store.dispatch('teams/get');
this.$store.dispatch('labels/get');
this.$store.dispatch('campaigns/get');
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);
const {
automation,
automationTypes,
onEventChange,
getConditionDropdownValues,
appendNewCondition,
appendNewAction,
removeFilter,
removeAction,
resetAction,
getActionDropdownValues,
manifestCustomAttributes,
} = useAutomation(START_VALUE);
return {
...attribute,
name: skipTranslation
? attribute.name
: this.$t(`AUTOMATION.ATTRIBUTES.${attribute.name}`),
};
});
},
},
const open = () => {
automation.value = structuredClone(START_VALUE);
manifestCustomAttributes();
formRef.value?.open();
};
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>
<template>
<div>
<woot-modal-header :header-title="$t('AUTOMATION.ADD.TITLE')" />
<div class="flex flex-col modal-content">
<div class="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(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>
<AutomationRuleForm
ref="formRef"
v-model:automation="automation"
mode="create"
:automation-types="automationTypes"
:get-condition-dropdown-values="getConditionDropdownValues"
:get-action-dropdown-values="getActionDropdownValues"
:append-new-condition="appendNewCondition"
:append-new-action="appendNewAction"
:remove-filter="removeFilter"
:remove-action="removeAction"
:reset-action="resetAction"
:on-event-change="onEventChange"
@save="onSave"
/>
</template>

View File

@@ -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>

View File

@@ -1,345 +1,80 @@
<script>
import { mapGetters } from 'vuex';
<script setup>
import { ref, watch } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
import { useAutomation } from 'dashboard/composables/useAutomation';
import { useEditableAutomation } from 'dashboard/composables/useEditableAutomation';
import FilterInputBox from 'dashboard/components/widgets/FilterInput/Index.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
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 AutomationRuleForm from './AutomationRuleForm.vue';
import { AUTOMATION_ACTION_TYPES } from './constants';
import { AUTOMATION_RULE_EVENTS, AUTOMATION_ACTION_TYPES } from './constants';
const props = defineProps({
selectedResponse: {
type: Object,
default: () => ({}),
},
});
export default {
components: {
FilterInputBox,
NextButton,
AutomationActionInput,
},
props: {
onClose: {
type: Function,
default: () => {},
},
selectedResponse: {
type: Object,
default: () => {},
},
},
emits: ['saveAutomation'],
setup() {
const {
automation,
const emit = defineEmits(['saveAutomation']);
const allCustomAttributes = useMapGetter('attributes/getAttributes');
const formRef = ref(null);
const {
automation,
automationTypes,
onEventChange,
getConditionDropdownValues,
appendNewCondition,
appendNewAction,
removeFilter,
removeAction,
resetAction,
getActionDropdownValues,
manifestCustomAttributes,
} = useAutomation();
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,
onEventChange,
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
AUTOMATION_ACTION_TYPES
);
},
methods: {
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);
{ immediate: true }
);
return {
...attribute,
name: skipTranslation
? attribute.name
: this.$t(`AUTOMATION.ATTRIBUTES.${attribute.name}`),
};
});
},
},
};
defineExpose({ open, close });
</script>
<template>
<div>
<woot-modal-header :header-title="$t('AUTOMATION.EDIT.TITLE')" />
<div class="flex flex-col modal-content">
<div v-if="automation" class="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="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>
<AutomationRuleForm
ref="formRef"
v-model:automation="automation"
mode="edit"
:automation-types="automationTypes"
:get-condition-dropdown-values="getConditionDropdownValues"
:get-action-dropdown-values="getActionDropdownValues"
:append-new-condition="appendNewCondition"
:append-new-action="appendNewAction"
:remove-filter="removeFilter"
:remove-action="removeAction"
:reset-action="resetAction"
:on-event-change="onEventChange"
@save="onSave"
/>
</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>

View File

@@ -16,8 +16,8 @@ const { t } = useI18n();
const confirmDialog = ref(null);
const loading = ref({});
const showAddPopup = ref(false);
const showEditPopup = ref(false);
const addDialogRef = ref(null);
const editDialogRef = ref(null);
const showDeleteConfirmationPopup = ref(false);
const selectedAutomation = ref({});
const toggleModalTitle = ref(t('AUTOMATION.TOGGLE.ACTIVATION_TITLE'));
@@ -57,18 +57,18 @@ onMounted(() => {
});
const openAddPopup = () => {
showAddPopup.value = true;
addDialogRef.value?.open();
};
const hideAddPopup = () => {
showAddPopup.value = false;
addDialogRef.value?.close();
};
const openEditPopup = response => {
selectedAutomation.value = response;
showEditPopup.value = true;
selectedAutomation.value = { ...response };
editDialogRef.value?.open();
};
const hideEditPopup = () => {
showEditPopup.value = false;
editDialogRef.value?.close();
};
const openDeletePopup = response => {
@@ -221,17 +221,7 @@ const tableHeaders = computed(() => {
</table>
</template>
<woot-modal
v-model:show="showAddPopup"
size="medium"
:on-close="hideAddPopup"
>
<AddAutomationRule
v-if="showAddPopup"
:on-close="hideAddPopup"
@save-automation="submitAutomation"
/>
</woot-modal>
<AddAutomationRule ref="addDialogRef" @save-automation="submitAutomation" />
<woot-delete-modal
v-model:show="showDeleteConfirmationPopup"
@@ -244,18 +234,11 @@ const tableHeaders = computed(() => {
:reject-text="deleteRejectText"
/>
<woot-modal
v-model:show="showEditPopup"
size="medium"
:on-close="hideEditPopup"
>
<EditAutomationRule
v-if="showEditPopup"
:on-close="hideEditPopup"
:selected-response="selectedAutomation"
@save-automation="submitAutomation"
/>
</woot-modal>
<EditAutomationRule
ref="editDialogRef"
:selected-response="selectedAutomation"
@save-automation="submitAutomation"
/>
<woot-confirm-modal
ref="confirmDialog"
:title="toggleModalTitle"

View File

@@ -5,6 +5,7 @@ import { useAlert } from 'dashboard/composables';
import InboxMembersAPI from '../../../../api/inboxMembers';
import NextButton from 'dashboard/components-next/button/Button.vue';
import TagInput from 'dashboard/components-next/taginput/TagInput.vue';
import router from '../../../index';
import PageHeader from '../SettingsSubPageHeader.vue';
import { useVuelidate } from '@vuelidate/core';
@@ -13,11 +14,12 @@ export default {
components: {
PageHeader,
NextButton,
TagInput,
},
validations: {
selectedAgents: {
selectedAgentIds: {
isEmpty() {
return !!this.selectedAgents.length;
return !!this.selectedAgentIds.length;
},
},
},
@@ -26,7 +28,7 @@ export default {
},
data() {
return {
selectedAgents: [],
selectedAgentIds: [],
isCreating: false,
};
},
@@ -34,18 +36,43 @@ export default {
...mapGetters({
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() {
this.$store.dispatch('agents/get');
},
methods: {
handleAgentAdd({ value }) {
if (!this.selectedAgentIds.includes(value)) {
this.selectedAgentIds.push(value);
}
},
handleAgentRemove(index) {
this.selectedAgentIds.splice(index, 1);
},
async addAgents() {
this.isCreating = true;
const inboxId = this.$route.params.inbox_id;
const selectedAgents = this.selectedAgents.map(x => x.id);
try {
await InboxMembersAPI.update({ inboxId, agentList: selectedAgents });
await InboxMembersAPI.update({
inboxId,
agentList: this.selectedAgentIds,
});
router.replace({
name: 'settings_inbox_finish',
params: {
@@ -72,25 +99,23 @@ export default {
/>
</div>
<div>
<div class="w-full">
<label :class="{ error: v$.selectedAgents.$error }">
<div class="w-full mb-4">
<label :class="{ error: v$.selectedAgentIds.$error }">
{{ $t('INBOX_MGMT.ADD.AGENTS.TITLE') }}
<multiselect
v-model="selectedAgents"
:options="agentList"
track-by="id"
label="name"
multiple
:close-on-select="false"
:clear-on-select="false"
hide-selected
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
:placeholder="$t('INBOX_MGMT.ADD.AGENTS.PICK_AGENTS')"
@select="v$.selectedAgents.$touch"
/>
<span v-if="v$.selectedAgents.$error" class="message">
<div
class="rounded-xl outline outline-1 -outline-offset-1 outline-n-weak hover:outline-n-strong px-2 py-2"
>
<TagInput
:model-value="selectedAgentNames"
:placeholder="$t('INBOX_MGMT.ADD.AGENTS.PICK_AGENTS')"
:menu-items="agentMenuItems"
show-dropdown
skip-label-dedup
@add="handleAgentAdd"
@remove="handleAgentRemove"
/>
</div>
<span v-if="v$.selectedAgentIds.$error" class="message">
{{ $t('INBOX_MGMT.ADD.AGENTS.VALIDATION_ERROR') }}
</span>
</label>

View File

@@ -12,6 +12,7 @@ import PageHeader from '../../SettingsSubPageHeader.vue';
import router from '../../../../index';
import { useBranding } from 'shared/composables/useBranding';
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 * as Sentry from '@sentry/vue';
@@ -21,6 +22,7 @@ export default {
LoadingState,
PageHeader,
NextButton,
ComboBox,
},
setup() {
const { accountId } = useAccount();
@@ -67,6 +69,12 @@ export default {
getSelectablePages() {
return this.pageList.filter(item => !item.exists);
},
comboBoxPageOptions() {
return this.getSelectablePages.map(({ id, name }) => ({
value: id,
label: name,
}));
},
},
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.pageName = name;
},
initChannelAuth(channel) {
@@ -245,23 +260,20 @@ export default {
/>
</div>
<div class="w-3/5">
<div class="w-full">
<div class="w-full mb-2">
<div class="input-wrap" :class="{ error: v$.selectedPage.$error }">
{{ $t('INBOX_MGMT.ADD.FB.CHOOSE_PAGE') }}
<multiselect
v-model="selectedPage"
close-on-select
allow-empty
:options="getSelectablePages"
track-by="id"
label="name"
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
<span class="text-n-slate-12 text-start">
{{ $t('INBOX_MGMT.ADD.FB.CHOOSE_PAGE') }}
</span>
<ComboBox
:model-value="selectedPage.id"
:options="comboBoxPageOptions"
:placeholder="$t('INBOX_MGMT.ADD.FB.PICK_A_VALUE')"
selected-label
@select="setPageName"
:has-error="v$.selectedPage.$error"
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') }}
</span>
</div>