Merge branch 'feat/captain-animating-svg' into feat/CW-5624
This commit is contained in:
@@ -6,11 +6,11 @@ class CaptainResponses extends ApiClient {
|
||||
super('captain/assistant_responses', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page = 1, searchKey, assistantId, documentId, status } = {}) {
|
||||
get({ page = 1, search, assistantId, documentId, status } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
searchKey,
|
||||
search,
|
||||
assistant_id: assistantId,
|
||||
document_id: documentId,
|
||||
status,
|
||||
|
||||
@@ -7,6 +7,11 @@ const props = defineProps({
|
||||
placeholder: { type: String, default: '' },
|
||||
label: { type: String, default: '' },
|
||||
id: { type: String, default: '' },
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator: value => ['sm', 'md'].includes(value),
|
||||
},
|
||||
message: { type: String, default: '' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
messageType: {
|
||||
@@ -69,6 +74,17 @@ const handleFocus = event => {
|
||||
isFocused.value = true;
|
||||
};
|
||||
|
||||
const sizeClass = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'sm':
|
||||
return 'h-8 !px-3 !py-2';
|
||||
case 'md':
|
||||
return 'h-10 !px-3 !py-2.5';
|
||||
default:
|
||||
return 'h-10 !px-3 !py-2.5';
|
||||
}
|
||||
});
|
||||
|
||||
const handleBlur = event => {
|
||||
emit('blur', event);
|
||||
isFocused.value = false;
|
||||
@@ -105,6 +121,7 @@ onMounted(() => {
|
||||
:class="[
|
||||
customInputClass,
|
||||
inputOutlineClass,
|
||||
sizeClass,
|
||||
{
|
||||
error: messageType === 'error',
|
||||
focus: isFocused,
|
||||
@@ -119,7 +136,7 @@ onMounted(() => {
|
||||
? max
|
||||
: undefined
|
||||
"
|
||||
class="block w-full reset-base text-sm h-10 !px-3 !py-2.5 !mb-0 outline outline-1 border-none border-0 outline-offset-[-1px] rounded-lg bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
class="block w-full reset-base text-sm !mb-0 outline outline-1 border-none border-0 outline-offset-[-1px] rounded-lg bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
|
||||
@@ -72,6 +72,10 @@ const formatType = computed(() => {
|
||||
return format ? format.charAt(0) + format.slice(1).toLowerCase() : '';
|
||||
});
|
||||
|
||||
const isDocumentTemplate = computed(() => {
|
||||
return headerComponent.value?.format?.toLowerCase() === 'document';
|
||||
});
|
||||
|
||||
const hasVariables = computed(() => {
|
||||
return bodyText.value?.match(/{{([^}]+)}}/g) !== null;
|
||||
});
|
||||
@@ -126,6 +130,11 @@ const updateMediaUrl = value => {
|
||||
processedParams.value.header.media_url = value;
|
||||
};
|
||||
|
||||
const updateMediaName = value => {
|
||||
processedParams.value.header ??= {};
|
||||
processedParams.value.header.media_name = value;
|
||||
};
|
||||
|
||||
const sendMessage = () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
@@ -168,10 +177,12 @@ defineExpose({
|
||||
processedParams,
|
||||
hasVariables,
|
||||
hasMediaHeader,
|
||||
isDocumentTemplate,
|
||||
headerComponent,
|
||||
renderedTemplate,
|
||||
v$,
|
||||
updateMediaUrl,
|
||||
updateMediaName,
|
||||
sendMessage,
|
||||
resetTemplate,
|
||||
goBack,
|
||||
@@ -225,6 +236,17 @@ defineExpose({
|
||||
@update:model-value="updateMediaUrl"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isDocumentTemplate" class="flex items-center mb-2.5">
|
||||
<Input
|
||||
:model-value="processedParams.header?.media_name || ''"
|
||||
type="text"
|
||||
class="flex-1"
|
||||
:placeholder="
|
||||
t('WHATSAPP_TEMPLATES.PARSER.DOCUMENT_NAME_PLACEHOLDER')
|
||||
"
|
||||
@update:model-value="updateMediaName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body Variables Section -->
|
||||
|
||||
@@ -104,6 +104,7 @@ export default function useAutomationValues() {
|
||||
contacts: contacts.value,
|
||||
customAttributes: getters['attributes/getAttributes'].value,
|
||||
inboxes: inboxes.value,
|
||||
labels: labels.value,
|
||||
statusFilterOptions: statusFilterOptions.value,
|
||||
priorityOptions: priorityOptions.value,
|
||||
messageTypeOptions: messageTypeOptions.value,
|
||||
|
||||
@@ -124,6 +124,7 @@ export const getConditionOptions = ({
|
||||
customAttributes,
|
||||
inboxes,
|
||||
languages,
|
||||
labels,
|
||||
statusFilterOptions,
|
||||
teams,
|
||||
type,
|
||||
@@ -150,6 +151,7 @@ export const getConditionOptions = ({
|
||||
country_code: countries,
|
||||
message_type: messageTypeOptions,
|
||||
priority: priorityOptions,
|
||||
labels: generateConditionOptions(labels, 'title'),
|
||||
};
|
||||
|
||||
return conditionFilterMaps[type];
|
||||
|
||||
@@ -218,6 +218,7 @@ describe('templateHelper', () => {
|
||||
expect(result.header).toEqual({
|
||||
media_url: '',
|
||||
media_type: 'document',
|
||||
media_name: '',
|
||||
});
|
||||
expect(result.body).toEqual({
|
||||
1: '',
|
||||
|
||||
@@ -51,6 +51,11 @@ export const buildTemplateParameters = (template, hasMediaHeaderValue) => {
|
||||
if (!allVariables.header) allVariables.header = {};
|
||||
allVariables.header.media_url = '';
|
||||
allVariables.header.media_type = headerComponent.format.toLowerCase();
|
||||
|
||||
// For document templates, include media_name field for filename support
|
||||
if (headerComponent.format.toLowerCase() === 'document') {
|
||||
allVariables.header.media_name = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Process button variables
|
||||
|
||||
@@ -177,7 +177,8 @@
|
||||
"REFERER_LINK": "Referrer Link",
|
||||
"ASSIGNEE_NAME": "Assignee",
|
||||
"TEAM_NAME": "Team",
|
||||
"PRIORITY": "Priority"
|
||||
"PRIORITY": "Priority",
|
||||
"LABELS": "Labels"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,6 +826,7 @@
|
||||
"SELECTED": "{count} selected",
|
||||
"SELECT_ALL": "Select all ({count})",
|
||||
"UNSELECT_ALL": "Unselect all ({count})",
|
||||
"SEARCH_PLACEHOLDER": "Search FAQs...",
|
||||
"BULK_APPROVE_BUTTON": "Approve",
|
||||
"BULK_DELETE_BUTTON": "Delete",
|
||||
"BULK_APPROVE": {
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"BUTTON_LABEL": "Button {index}",
|
||||
"COUPON_CODE": "Enter coupon code (max 15 chars)",
|
||||
"MEDIA_URL_LABEL": "Enter {type} URL",
|
||||
"DOCUMENT_NAME_PLACEHOLDER": "Enter document filename (e.g., Invoice_2025.pdf)",
|
||||
"BUTTON_PARAMETER": "Enter button parameter"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import { useI18n } from 'vue-i18n';
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||
import BulkDeleteDialog from 'dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue';
|
||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||
@@ -36,6 +38,7 @@ const bulkDeleteDialog = ref(null);
|
||||
const selectedStatus = ref('all');
|
||||
const selectedAssistant = ref('all');
|
||||
const dialogType = ref('');
|
||||
const searchQuery = ref('');
|
||||
const { t } = useI18n();
|
||||
|
||||
const createDialog = ref(null);
|
||||
@@ -138,6 +141,9 @@ const fetchResponses = (page = 1) => {
|
||||
if (selectedAssistant.value !== 'all') {
|
||||
filterParams.assistantId = selectedAssistant.value;
|
||||
}
|
||||
if (searchQuery.value) {
|
||||
filterParams.search = searchQuery.value;
|
||||
}
|
||||
store.dispatch('captainResponses/get', filterParams);
|
||||
};
|
||||
|
||||
@@ -250,6 +256,10 @@ const handleAssistantFilterChange = assistant => {
|
||||
fetchResponses();
|
||||
};
|
||||
|
||||
const debouncedSearch = debounce(async () => {
|
||||
fetchResponses();
|
||||
}, 500);
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainAssistants/get');
|
||||
fetchResponses();
|
||||
@@ -292,34 +302,47 @@ onMounted(() => {
|
||||
<template #controls>
|
||||
<div
|
||||
v-if="shouldShowDropdown"
|
||||
class="mb-4 -mt-3 flex justify-between items-center w-fit py-1"
|
||||
class="mb-4 -mt-3 flex justify-between items-center py-1"
|
||||
:class="{
|
||||
'ltr:pl-3 rtl:pr-3 ltr:pr-1 rtl:pl-1 rounded-lg outline outline-1 outline-n-weak bg-n-solid-3':
|
||||
'ltr:pl-3 rtl:pr-3 ltr:pr-1 rtl:pl-1 rounded-lg outline outline-1 outline-n-weak bg-n-solid-3 w-fit':
|
||||
bulkSelectionState.hasSelected,
|
||||
}"
|
||||
>
|
||||
<div v-if="!bulkSelectionState.hasSelected" class="flex gap-3">
|
||||
<OnClickOutside @trigger="isStatusFilterOpen = false">
|
||||
<Button
|
||||
:label="selectedStatusLabel"
|
||||
icon="i-lucide-chevron-down"
|
||||
size="sm"
|
||||
color="slate"
|
||||
trailing-icon
|
||||
class="max-w-48"
|
||||
@click="isStatusFilterOpen = !isStatusFilterOpen"
|
||||
/>
|
||||
<div
|
||||
v-if="!bulkSelectionState.hasSelected"
|
||||
class="flex gap-3 justify-between w-full items-center"
|
||||
>
|
||||
<div class="flex gap-3">
|
||||
<OnClickOutside @trigger="isStatusFilterOpen = false">
|
||||
<Button
|
||||
:label="selectedStatusLabel"
|
||||
icon="i-lucide-chevron-down"
|
||||
size="sm"
|
||||
color="slate"
|
||||
trailing-icon
|
||||
class="max-w-48"
|
||||
@click="isStatusFilterOpen = !isStatusFilterOpen"
|
||||
/>
|
||||
|
||||
<DropdownMenu
|
||||
v-if="isStatusFilterOpen"
|
||||
:menu-items="statusOptions"
|
||||
class="mt-2"
|
||||
@action="handleStatusFilterChange"
|
||||
<DropdownMenu
|
||||
v-if="isStatusFilterOpen"
|
||||
:menu-items="statusOptions"
|
||||
class="mt-2"
|
||||
@action="handleStatusFilterChange"
|
||||
/>
|
||||
</OnClickOutside>
|
||||
<AssistantSelector
|
||||
:assistant-id="selectedAssistant"
|
||||
@update="handleAssistantFilterChange"
|
||||
/>
|
||||
</OnClickOutside>
|
||||
<AssistantSelector
|
||||
:assistant-id="selectedAssistant"
|
||||
@update="handleAssistantFilterChange"
|
||||
</div>
|
||||
<Input
|
||||
v-model="searchQuery"
|
||||
:placeholder="$t('CAPTAIN.RESPONSES.SEARCH_PLACEHOLDER')"
|
||||
class="w-64"
|
||||
size="sm"
|
||||
autofocus
|
||||
@input="debouncedSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -68,6 +68,12 @@ export const AUTOMATIONS = {
|
||||
inputType: 'plain_text',
|
||||
filterOperators: OPERATOR_TYPES_6,
|
||||
},
|
||||
{
|
||||
key: 'labels',
|
||||
name: 'LABELS',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_3,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
@@ -186,6 +192,12 @@ export const AUTOMATIONS = {
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
},
|
||||
{
|
||||
key: 'labels',
|
||||
name: 'LABELS',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_3,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
@@ -308,6 +320,12 @@ export const AUTOMATIONS = {
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
},
|
||||
{
|
||||
key: 'labels',
|
||||
name: 'LABELS',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_3,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
@@ -424,6 +442,12 @@ export const AUTOMATIONS = {
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
},
|
||||
{
|
||||
key: 'labels',
|
||||
name: 'LABELS',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_3,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user