mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 05:11:57 +00:00
fixes to pre chat form
This commit is contained in:
132
cmd/chat.go
132
cmd/chat.go
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -162,29 +163,11 @@ func handleGetChatSettings(r *fastglue.Request) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Collect custom attribute IDs from pre-chat form fields
|
||||
// Filter out pre-chat form fields for which custom attributes don't exist anymore
|
||||
if config.PreChatForm.Enabled && len(config.PreChatForm.Fields) > 0 {
|
||||
customAttrIDs := make(map[int]bool)
|
||||
for _, field := range config.PreChatForm.Fields {
|
||||
if field.Enabled && field.CustomAttributeID > 0 {
|
||||
customAttrIDs[field.CustomAttributeID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch custom attributes if any are referenced
|
||||
if len(customAttrIDs) > 0 {
|
||||
customAttributes := make(map[int]customAttributeWidget)
|
||||
for id := range customAttrIDs {
|
||||
attr, err := app.customAttribute.Get(id)
|
||||
if err != nil {
|
||||
app.lo.Error("failed to fetch custom attribute for widget", "id", id, "error", err)
|
||||
continue
|
||||
}
|
||||
customAttributes[id] = customAttributeWidget{
|
||||
ID: attr.ID,
|
||||
Values: attr.Values,
|
||||
}
|
||||
}
|
||||
filteredFields, customAttributes := filterPreChatFormFields(config.PreChatForm.Fields, app)
|
||||
response.PreChatForm.Fields = filteredFields
|
||||
if len(customAttributes) > 0 {
|
||||
response.CustomAttributes = customAttributes
|
||||
}
|
||||
}
|
||||
@@ -253,8 +236,8 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
lastName := claims.LastName
|
||||
email := claims.Email
|
||||
|
||||
// Process form custom attributes
|
||||
formCustomAttributes := processFormCustomAttributes(req.FormData, config, app)
|
||||
// Validate custom attribute
|
||||
formCustomAttributes := validateCustomAttributes(req.FormData, config, app)
|
||||
|
||||
// Merge JWT and form custom attributes (form takes precedence)
|
||||
mergedAttributes := mergeCustomAttributes(claims.CustomAttributes, formCustomAttributes)
|
||||
@@ -284,14 +267,13 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
// User exists, update custom attributes from both JWT and form
|
||||
// Don't override existing name and email.
|
||||
|
||||
// Process form custom attributes
|
||||
formCustomAttributes := processFormCustomAttributes(req.FormData, config, app)
|
||||
// Validate custom attribute
|
||||
formCustomAttributes := validateCustomAttributes(req.FormData, config, app)
|
||||
|
||||
// Merge JWT and form custom attributes (form takes precedence)
|
||||
mergedAttributes := mergeCustomAttributes(claims.CustomAttributes, formCustomAttributes)
|
||||
|
||||
if len(mergedAttributes) > 0 {
|
||||
fmt.Println("Updating custom attributes for user:", user.ID, "with attributes:", mergedAttributes)
|
||||
if err := app.user.SaveCustomAttributes(user.ID, mergedAttributes, false); err != nil {
|
||||
app.lo.Error("error updating contact custom attributes", "contact_id", user.ID, "error", err)
|
||||
// Don't fail the request for custom attributes update failure
|
||||
@@ -309,8 +291,8 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
// Process form custom attributes
|
||||
formCustomAttributes := processFormCustomAttributes(req.FormData, config, app)
|
||||
// Validate custom attribute
|
||||
formCustomAttributes := validateCustomAttributes(req.FormData, config, app)
|
||||
|
||||
// Merge JWT and form custom attributes (form takes precedence)
|
||||
mergedAttributes := mergeCustomAttributes(claims.CustomAttributes, formCustomAttributes)
|
||||
@@ -334,7 +316,7 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
}
|
||||
|
||||
// Process custom attributes from form data
|
||||
formCustomAttributes := processFormCustomAttributes(req.FormData, config, app)
|
||||
formCustomAttributes := validateCustomAttributes(req.FormData, config, app)
|
||||
|
||||
// Marshal custom attributes for storage
|
||||
var customAttribJSON []byte
|
||||
@@ -378,7 +360,7 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
}
|
||||
|
||||
if !allowStartConversation {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.T("globals.messages.notAllowed}"), nil, envelope.PermissionError)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.Ts("globals.messages.notAllowed", "name", ""), nil, envelope.PermissionError)
|
||||
}
|
||||
|
||||
if preventMultipleConversations {
|
||||
@@ -397,7 +379,7 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
userType = "user"
|
||||
}
|
||||
app.lo.Info(userType+" attempted to start new conversation but already has one", "contact_id", contactID, "conversations_count", len(conversations))
|
||||
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.T("globals.messages.notAllowed}"), nil, envelope.PermissionError)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.Ts("globals.messages.notAllowed", "name", ""), nil, envelope.PermissionError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,9 +938,8 @@ func mergeCustomAttributes(jwtAttributes, formAttributes map[string]interface{})
|
||||
return merged
|
||||
}
|
||||
|
||||
// processFormCustomAttributes processes form data and extracts custom attributes
|
||||
// based on the pre-chat form configuration, skipping default fields like name and email
|
||||
func processFormCustomAttributes(formData map[string]interface{}, config livechat.Config, app *App) map[string]interface{} {
|
||||
// validateCustomAttributes validates and processes custom attributes from form data
|
||||
func validateCustomAttributes(formData map[string]interface{}, config livechat.Config, app *App) map[string]interface{} {
|
||||
customAttributes := make(map[string]interface{})
|
||||
|
||||
if !config.PreChatForm.Enabled || len(formData) == 0 {
|
||||
@@ -973,17 +954,7 @@ func processFormCustomAttributes(formData map[string]interface{}, config livecha
|
||||
}
|
||||
|
||||
// Create a map of valid field keys for quick lookup
|
||||
validFields := make(map[string]struct {
|
||||
Key string `json:"key"`
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
Placeholder string `json:"placeholder"`
|
||||
Required bool `json:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Order int `json:"order"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
CustomAttributeID int `json:"custom_attribute_id,omitempty"`
|
||||
})
|
||||
validFields := make(map[string]livechat.PreChatFormField)
|
||||
for _, field := range config.PreChatForm.Fields {
|
||||
if field.Enabled {
|
||||
validFields[field.Key] = field
|
||||
@@ -1026,6 +997,24 @@ func processFormCustomAttributes(formData map[string]interface{}, config livecha
|
||||
}
|
||||
customAttributes[field.Key] = strValue
|
||||
}
|
||||
|
||||
// Numbers
|
||||
if numValue, ok := value.(float64); ok {
|
||||
if math.IsNaN(numValue) || math.IsInf(numValue, 0) {
|
||||
app.lo.Warn("form field contains invalid numeric value", "key", key, "value", numValue)
|
||||
continue
|
||||
}
|
||||
|
||||
if numValue > 1e12 || numValue < -1e12 {
|
||||
app.lo.Warn("form field numeric value out of acceptable range", "key", key, "value", numValue)
|
||||
continue
|
||||
}
|
||||
|
||||
customAttributes[field.Key] = numValue
|
||||
}
|
||||
|
||||
// Set rest as is
|
||||
customAttributes[field.Key] = value
|
||||
}
|
||||
|
||||
return customAttributes
|
||||
@@ -1087,3 +1076,54 @@ func validateFormData(formData map[string]interface{}, config livechat.Config, e
|
||||
|
||||
return finalName, finalEmail, nil
|
||||
}
|
||||
|
||||
// filterPreChatFormFields filters out pre-chat form fields that reference non-existent custom attributes while retaining the default fields
|
||||
func filterPreChatFormFields(fields []livechat.PreChatFormField, app *App) ([]livechat.PreChatFormField, map[int]customAttributeWidget) {
|
||||
if len(fields) == 0 {
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
// Collect custom attribute IDs and enabled fields
|
||||
customAttrIDs := make(map[int]bool)
|
||||
enabledFields := make([]livechat.PreChatFormField, 0, len(fields))
|
||||
|
||||
for _, field := range fields {
|
||||
if field.Enabled {
|
||||
enabledFields = append(enabledFields, field)
|
||||
if field.CustomAttributeID > 0 {
|
||||
customAttrIDs[field.CustomAttributeID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch existing custom attributes
|
||||
existingCustomAttrs := make(map[int]customAttributeWidget)
|
||||
for id := range customAttrIDs {
|
||||
attr, err := app.customAttribute.Get(id)
|
||||
if err != nil {
|
||||
app.lo.Warn("custom attribute referenced in pre-chat form no longer exists", "custom_attribute_id", id, "error", err)
|
||||
continue
|
||||
}
|
||||
existingCustomAttrs[id] = customAttributeWidget{
|
||||
ID: attr.ID,
|
||||
Values: attr.Values,
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out fields with non-existent custom attributes
|
||||
filteredFields := make([]livechat.PreChatFormField, 0, len(enabledFields))
|
||||
for _, field := range enabledFields {
|
||||
// Keep default fields
|
||||
if field.IsDefault {
|
||||
filteredFields = append(filteredFields, field)
|
||||
continue
|
||||
}
|
||||
|
||||
// Only keep custom fields if their custom attribute exists
|
||||
if _, exists := existingCustomAttrs[field.CustomAttributeID]; exists {
|
||||
filteredFields = append(filteredFields, field)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredFields, existingCustomAttrs
|
||||
}
|
||||
|
@@ -91,10 +91,14 @@
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger>
|
||||
<SelectValue :placeholder="$t('admin.inbox.livechat.emailFallbackInbox.placeholder')" />
|
||||
<SelectValue
|
||||
:placeholder="$t('admin.inbox.livechat.emailFallbackInbox.placeholder')"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="0">{{ $t('admin.inbox.livechat.emailFallbackInbox.none') }}</SelectItem>
|
||||
<SelectItem :value="0">{{
|
||||
$t('admin.inbox.livechat.emailFallbackInbox.none')
|
||||
}}</SelectItem>
|
||||
<SelectItem v-for="inbox in emailInboxes" :key="inbox.id" :value="inbox.id">
|
||||
{{ inbox.name }}
|
||||
</SelectItem>
|
||||
@@ -163,8 +167,12 @@
|
||||
<FormField v-slot="{ componentField, handleChange }" name="config.show_powered_by">
|
||||
<FormItem class="flex flex-row items-center justify-between box p-4">
|
||||
<div class="space-y-0.5">
|
||||
<FormLabel class="text-base">{{ $t('admin.inbox.livechat.showPoweredBy') }}</FormLabel>
|
||||
<FormDescription>{{ $t('admin.inbox.livechat.showPoweredBy.description') }}</FormDescription>
|
||||
<FormLabel class="text-base">{{
|
||||
$t('admin.inbox.livechat.showPoweredBy')
|
||||
}}</FormLabel>
|
||||
<FormDescription>{{
|
||||
$t('admin.inbox.livechat.showPoweredBy.description')
|
||||
}}</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch :checked="componentField.modelValue" @update:checked="handleChange" />
|
||||
@@ -543,11 +551,7 @@
|
||||
|
||||
<!-- Pre-Chat Form Tab -->
|
||||
<div v-show="activeTab === 'prechat'" class="space-y-6">
|
||||
<PreChatFormConfig
|
||||
:form="form"
|
||||
:custom-attributes="customAttributes"
|
||||
@fetch-custom-attributes="fetchCustomAttributes"
|
||||
/>
|
||||
<PreChatFormConfig v-model="prechatConfig" />
|
||||
</div>
|
||||
|
||||
<!-- Users Tab -->
|
||||
@@ -715,7 +719,6 @@ import { Tabs, TabsList, TabsTrigger } from '@shared-ui/components/ui/tabs'
|
||||
import { Plus, X } from 'lucide-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PreChatFormConfig from './PreChatFormConfig.vue'
|
||||
import api from '@/api'
|
||||
|
||||
const props = defineProps({
|
||||
initialValues: {
|
||||
@@ -740,7 +743,7 @@ const { t } = useI18n()
|
||||
const activeTab = ref('general')
|
||||
const selectedUserTab = ref('visitors')
|
||||
const externalLinks = ref([])
|
||||
const customAttributes = ref([])
|
||||
const prechatConfig = ref({})
|
||||
|
||||
const inboxStore = useInboxStore()
|
||||
const emailInboxes = computed(() =>
|
||||
@@ -846,24 +849,6 @@ const updateExternalLinks = () => {
|
||||
form.setFieldValue('config.external_links', externalLinks.value)
|
||||
}
|
||||
|
||||
const fetchCustomAttributes = async () => {
|
||||
try {
|
||||
// Fetch both contact and conversation custom attributes
|
||||
const [contactAttrs, conversationAttrs] = await Promise.all([
|
||||
api.getCustomAttributes('contact'),
|
||||
api.getCustomAttributes('conversation')
|
||||
])
|
||||
|
||||
customAttributes.value = [
|
||||
...(contactAttrs.data?.data || []),
|
||||
...(conversationAttrs.data?.data || [])
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('Error fetching custom attributes:', error)
|
||||
customAttributes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch inboxes on mount for the linked email inbox dropdown
|
||||
onMounted(() => {
|
||||
inboxStore.fetchInboxes()
|
||||
@@ -895,6 +880,15 @@ const onSubmit = form.handleSubmit(async (values) => {
|
||||
await props.submitForm(values)
|
||||
})
|
||||
|
||||
// Watch for prechat config changes and sync with form
|
||||
watch(
|
||||
prechatConfig,
|
||||
(newConfig) => {
|
||||
form.setFieldValue('config.prechat_form', newConfig)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.initialValues,
|
||||
(newValues) => {
|
||||
@@ -911,6 +905,12 @@ watch(
|
||||
if (newValues.config?.external_links) {
|
||||
externalLinks.value = [...newValues.config.external_links]
|
||||
}
|
||||
|
||||
// Set prechat config
|
||||
if (newValues.config?.prechat_form) {
|
||||
prechatConfig.value = { ...newValues.config.prechat_form }
|
||||
}
|
||||
|
||||
form.setValues(newValues)
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
|
@@ -1,35 +1,35 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Master Toggle -->
|
||||
<FormField v-slot="{ componentField, handleChange }" name="config.prechat_form.enabled">
|
||||
<FormItem class="flex flex-row items-center justify-between box p-4">
|
||||
<div class="space-y-0.5">
|
||||
<FormLabel class="text-base">{{ $t('admin.inbox.livechat.prechatForm.enabled') }}</FormLabel>
|
||||
<FormDescription>
|
||||
{{ $t('admin.inbox.livechat.prechatForm.enabled.description') }}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch :checked="componentField.modelValue" @update:checked="handleChange" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<div class="flex flex-row items-center justify-between box p-4">
|
||||
<div class="space-y-0.5">
|
||||
<label class="text-base font-medium">
|
||||
{{ $t('admin.inbox.livechat.prechatForm.enabled') }}
|
||||
</label>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ $t('admin.inbox.livechat.prechatForm.enabled.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch v-model:checked="prechatConfig.enabled" />
|
||||
</div>
|
||||
|
||||
<!-- Form Configuration -->
|
||||
<div v-if="form.values.config?.prechat_form?.enabled" class="space-y-6">
|
||||
<div v-if="prechatConfig.enabled" class="space-y-6">
|
||||
<!-- Form Title -->
|
||||
<FormField v-slot="{ componentField }" name="config.prechat_form.title">
|
||||
<FormItem>
|
||||
<FormLabel>{{ $t('admin.inbox.livechat.prechatForm.title') }}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" placeholder="Tell us about yourself" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{{ $t('admin.inbox.livechat.prechatForm.title.description') }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<div>
|
||||
<label class="text-sm font-medium">
|
||||
{{ $t('admin.inbox.livechat.prechatForm.title') }}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
v-model="prechatConfig.title"
|
||||
placeholder="Tell us about yourself"
|
||||
class="mt-1"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{{ $t('admin.inbox.livechat.prechatForm.title.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Fields Configuration -->
|
||||
<div class="space-y-4">
|
||||
@@ -37,11 +37,10 @@
|
||||
<h4 class="font-medium text-foreground">
|
||||
{{ $t('admin.inbox.livechat.prechatForm.fields') }}
|
||||
</h4>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="$emit('fetch-custom-attributes')"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="fetchCustomAttributes"
|
||||
:disabled="availableCustomAttributes.length === 0"
|
||||
>
|
||||
<Plus class="w-4 h-4 mr-2" />
|
||||
@@ -53,105 +52,66 @@
|
||||
<div class="space-y-3">
|
||||
<Draggable
|
||||
v-model="draggableFields"
|
||||
item-key="key"
|
||||
:item-key="(field) => field.key || `field_${field.custom_attribute_id || 'unknown'}`"
|
||||
:animation="200"
|
||||
class="space-y-3"
|
||||
>
|
||||
<template #item="{ element: field, index }">
|
||||
<div class="border rounded-lg p-4 space-y-4">
|
||||
<!-- Field Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="cursor-move text-muted-foreground">
|
||||
<GripVertical class="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium">{{ field.label }}</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ field.type }} {{ field.is_default ? '(Default)' : '(Custom)' }}
|
||||
<div :key="field.key || `field-${index}`" class="border rounded-lg p-4 space-y-4">
|
||||
<!-- Field Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="cursor-move text-muted-foreground">
|
||||
<GripVertical class="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium">{{ field.label }}</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ field.type }} {{ field.is_default ? '(Default)' : '(Custom)' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FormField
|
||||
:name="`config.prechat_form.fields.${index}.enabled`"
|
||||
v-slot="{ componentField, handleChange }"
|
||||
>
|
||||
<FormControl>
|
||||
<Switch
|
||||
:checked="componentField.modelValue"
|
||||
@update:checked="handleChange"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormField>
|
||||
<Button
|
||||
v-if="!field.is_default"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="removeField(index)"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Field Configuration -->
|
||||
<div v-if="field.enabled" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Label -->
|
||||
<FormField
|
||||
:name="`config.prechat_form.fields.${index}.label`"
|
||||
v-slot="{ componentField }"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">{{ $t('globals.terms.label') }}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
placeholder="Field label"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Placeholder -->
|
||||
<FormField
|
||||
:name="`config.prechat_form.fields.${index}.placeholder`"
|
||||
v-slot="{ componentField }"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">{{ $t('globals.terms.placeholder') }}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
placeholder="Field placeholder"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<!-- Required -->
|
||||
<FormField
|
||||
:name="`config.prechat_form.fields.${index}.required`"
|
||||
v-slot="{ componentField, handleChange }"
|
||||
>
|
||||
<FormItem>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
:checked="componentField.modelValue"
|
||||
@update:checked="handleChange"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel class="text-sm">{{ $t('globals.terms.required') }}</FormLabel>
|
||||
<Switch v-model:checked="field.enabled" />
|
||||
<Button
|
||||
v-if="!field.is_default"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="removeField(index)"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Field Configuration -->
|
||||
<div v-if="field.enabled" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Label -->
|
||||
<div>
|
||||
<label class="text-sm font-medium">{{ $t('globals.terms.label') }}</label>
|
||||
<Input v-model="field.label" placeholder="Field label" class="mt-1" />
|
||||
</div>
|
||||
|
||||
<!-- Placeholder -->
|
||||
<div>
|
||||
<label class="text-sm font-medium">
|
||||
{{ $t('globals.terms.placeholder') }}
|
||||
</label>
|
||||
<Input
|
||||
v-model="field.placeholder"
|
||||
placeholder="Field placeholder"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Required -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox v-model:checked="field.required" />
|
||||
<label class="text-sm">{{ $t('globals.terms.required') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
@@ -164,7 +124,9 @@
|
||||
|
||||
<!-- Custom Attributes Selection -->
|
||||
<div v-if="availableCustomAttributes.length > 0" class="space-y-3">
|
||||
<h5 class="font-medium text-sm">{{ $t('admin.inbox.livechat.prechatForm.availableFields') }}</h5>
|
||||
<h5 class="font-medium text-sm">
|
||||
{{ $t('admin.inbox.livechat.prechatForm.availableFields') }}
|
||||
</h5>
|
||||
<div class="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto">
|
||||
<div
|
||||
v-for="attr in availableCustomAttributes"
|
||||
@@ -186,68 +148,79 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@shared-ui/components/ui/form'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { Input } from '@shared-ui/components/ui/input'
|
||||
import { Button } from '@shared-ui/components/ui/button'
|
||||
import { Switch } from '@shared-ui/components/ui/switch'
|
||||
import { Checkbox } from '@shared-ui/components/ui/checkbox'
|
||||
import { Plus, X, GripVertical } from 'lucide-vue-next'
|
||||
import Draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
customAttributes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
const prechatConfig = defineModel({
|
||||
default: () => ({
|
||||
enabled: false,
|
||||
title: '',
|
||||
fields: [
|
||||
{
|
||||
key: 'name',
|
||||
type: 'text',
|
||||
label: 'Full name',
|
||||
placeholder: 'Enter your name',
|
||||
required: true,
|
||||
enabled: true,
|
||||
order: 1,
|
||||
is_default: true
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
type: 'email',
|
||||
label: 'Email address',
|
||||
placeholder: 'your@email.com',
|
||||
required: true,
|
||||
enabled: true,
|
||||
order: 2,
|
||||
is_default: true
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
const emit = defineEmits(['fetch-custom-attributes'])
|
||||
const customAttributes = ref([])
|
||||
|
||||
const formFields = computed(() => {
|
||||
return props.form.values.config?.prechat_form?.fields || []
|
||||
return prechatConfig.value.fields || []
|
||||
})
|
||||
|
||||
const availableCustomAttributes = computed(() => {
|
||||
const usedIds = formFields.value
|
||||
.filter(field => field.custom_attribute_id)
|
||||
.map(field => field.custom_attribute_id)
|
||||
|
||||
return props.customAttributes.filter(attr => !usedIds.includes(attr.id))
|
||||
.filter((field) => field.custom_attribute_id)
|
||||
.map((field) => field.custom_attribute_id)
|
||||
|
||||
return customAttributes.value.filter((attr) => !usedIds.includes(attr.id))
|
||||
})
|
||||
|
||||
const draggableFields = computed({
|
||||
get() {
|
||||
return formFields.value
|
||||
return prechatConfig.value.fields || []
|
||||
},
|
||||
set(newValue) {
|
||||
const fieldsWithUpdatedOrder = newValue.map((field, index) => ({
|
||||
...field,
|
||||
order: index + 1
|
||||
}))
|
||||
props.form.setFieldValue('config.prechat_form.fields', fieldsWithUpdatedOrder)
|
||||
prechatConfig.value.fields = fieldsWithUpdatedOrder
|
||||
}
|
||||
})
|
||||
|
||||
const removeField = (index) => {
|
||||
const fields = formFields.value.filter((_, i) => i !== index)
|
||||
props.form.setFieldValue('config.prechat_form.fields', fields)
|
||||
prechatConfig.value.fields = fields
|
||||
}
|
||||
|
||||
const addCustomAttributeToForm = (attribute) => {
|
||||
const newField = {
|
||||
key: attribute.key,
|
||||
key: attribute.key || `custom_attr_${attribute.id || Date.now()}`,
|
||||
type: attribute.data_type,
|
||||
label: attribute.name,
|
||||
placeholder: '',
|
||||
@@ -257,12 +230,49 @@ const addCustomAttributeToForm = (attribute) => {
|
||||
is_default: false,
|
||||
custom_attribute_id: attribute.id
|
||||
}
|
||||
|
||||
|
||||
const fields = [...formFields.value, newField]
|
||||
props.form.setFieldValue('config.prechat_form.fields', fields)
|
||||
prechatConfig.value.fields = fields
|
||||
}
|
||||
|
||||
const fetchCustomAttributes = async () => {
|
||||
try {
|
||||
// Fetch both contact and conversation custom attributes
|
||||
const [contactAttrs, conversationAttrs] = await Promise.all([
|
||||
api.getCustomAttributes('contact'),
|
||||
api.getCustomAttributes('conversation')
|
||||
])
|
||||
|
||||
customAttributes.value = [
|
||||
...(contactAttrs.data?.data || []),
|
||||
...(conversationAttrs.data?.data || [])
|
||||
]
|
||||
|
||||
// Clean up orphaned custom attribute fields
|
||||
const availableCustomAttrIds = customAttributes.value.map((attr) => attr.id)
|
||||
const cleanedFields = (prechatConfig.value.fields || []).filter((field) => {
|
||||
// Keep default fields
|
||||
if (field.is_default) return true
|
||||
|
||||
// Keep custom fields that still exist
|
||||
if (field.custom_attribute_id && availableCustomAttrIds.includes(field.custom_attribute_id))
|
||||
return true
|
||||
|
||||
// Remove orphaned custom fields
|
||||
return false
|
||||
})
|
||||
|
||||
// Update fields if any were removed
|
||||
if (cleanedFields.length !== (prechatConfig.value.fields || []).length) {
|
||||
prechatConfig.value.fields = cleanedFields
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching custom attributes:', error)
|
||||
customAttributes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emit('fetch-custom-attributes')
|
||||
fetchCustomAttributes()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
@@ -40,7 +40,7 @@ export const createFormSchema = (t) => z.object({
|
||||
colors: z.object({
|
||||
primary: z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, {
|
||||
message: t('globals.messages.invalid', {
|
||||
name: t('globals.terms.colors').toLowerCase()
|
||||
name: t('admin.inbox.livechat.colors').toLowerCase()
|
||||
})
|
||||
}),
|
||||
}),
|
||||
|
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div class="bg-background flex-1 flex flex-col">
|
||||
<div v-if="showForm" class="flex-1 flex flex-col max-h-full">
|
||||
<div class="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-muted-foreground/30 hover:scrollbar-thumb-muted-foreground/50 p-4 space-y-4">
|
||||
<div
|
||||
class="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-muted-foreground/30 hover:scrollbar-thumb-muted-foreground/50 p-4 space-y-4"
|
||||
>
|
||||
<!-- Form title -->
|
||||
<div v-if="formTitle" class="text-xl text-foreground mb-2 text-center">
|
||||
{{ formTitle }}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Default message -->
|
||||
<div v-else class="text-lg font-semibold text-foreground mb-2">
|
||||
{{ $t('globals.terms.helpUsServeYouBetter') }}
|
||||
@@ -15,152 +17,169 @@
|
||||
<form @submit.prevent="submitForm" class="space-y-4">
|
||||
<!-- Dynamic fields -->
|
||||
<div v-for="field in sortedFields" :key="field.key" class="space-y-2">
|
||||
<!-- Text input -->
|
||||
<FormField v-if="field.type === 'text'" v-slot="{ componentField }" :name="field.key">
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
type="text"
|
||||
:placeholder="field.placeholder || ''"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Email input -->
|
||||
<FormField v-else-if="field.type === 'email'" v-slot="{ componentField }" :name="field.key">
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
type="email"
|
||||
:placeholder="field.placeholder || ''"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Number input -->
|
||||
<FormField v-else-if="field.type === 'number'" v-slot="{ componentField }" :name="field.key">
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
type="number"
|
||||
:placeholder="field.placeholder || ''"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Date input -->
|
||||
<FormField v-else-if="field.type === 'date'" v-slot="{ componentField }" :name="field.key">
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
type="date"
|
||||
:placeholder="field.placeholder || ''"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Link/URL input -->
|
||||
<FormField v-else-if="field.type === 'link'" v-slot="{ componentField }" :name="field.key">
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
type="url"
|
||||
:placeholder="field.placeholder || 'https://'"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Checkbox input -->
|
||||
<FormField v-else-if="field.type === 'checkbox'" v-slot="{ componentField }" :name="field.key">
|
||||
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
:checked="componentField.modelValue"
|
||||
@update:checked="componentField.handleChange"
|
||||
/>
|
||||
</FormControl>
|
||||
<div class="space-y-1 leading-none">
|
||||
<!-- Text input -->
|
||||
<FormField v-if="field.type === 'text'" v-slot="{ componentField }" :name="field.key">
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
type="text"
|
||||
:placeholder="field.placeholder || ''"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- List/Select input -->
|
||||
<FormField v-else-if="field.type === 'list'" v-slot="{ componentField }" :name="field.key">
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger>
|
||||
<SelectValue :placeholder="field.placeholder || $t('globals.terms.select')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="option in getFieldOptions(field)"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<!-- Email input -->
|
||||
<FormField
|
||||
v-else-if="field.type === 'email'"
|
||||
v-slot="{ componentField }"
|
||||
:name="field.key"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
type="email"
|
||||
:placeholder="field.placeholder || ''"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Number input -->
|
||||
<FormField
|
||||
v-else-if="field.type === 'number'"
|
||||
v-slot="{ componentField }"
|
||||
:name="field.key"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
type="number"
|
||||
:placeholder="field.placeholder || ''"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Date input -->
|
||||
<FormField
|
||||
v-else-if="field.type === 'date'"
|
||||
v-slot="{ componentField }"
|
||||
:name="field.key"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
type="date"
|
||||
:placeholder="field.placeholder || ''"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Link/URL input -->
|
||||
<FormField
|
||||
v-else-if="field.type === 'link'"
|
||||
v-slot="{ componentField }"
|
||||
:name="field.key"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
type="url"
|
||||
:placeholder="field.placeholder || 'https://'"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Checkbox input -->
|
||||
<FormField
|
||||
v-else-if="field.type === 'checkbox'"
|
||||
v-slot="{ componentField, handleChange }"
|
||||
:name="field.key"
|
||||
>
|
||||
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox :checked="componentField.modelValue" @update:checked="handleChange" />
|
||||
</FormControl>
|
||||
<div class="space-y-1 leading-none">
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- List/Select input -->
|
||||
<FormField
|
||||
v-else-if="field.type === 'list'"
|
||||
v-slot="{ componentField }"
|
||||
:name="field.key"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm font-medium">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger>
|
||||
<SelectValue :placeholder="field.placeholder || $t('globals.terms.select')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="option in getFieldOptions(field)"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Submit button - fixed at bottom -->
|
||||
<div class="p-4 border-t">
|
||||
<Button
|
||||
@click="submitForm"
|
||||
class="w-full"
|
||||
:disabled="!meta.valid"
|
||||
>
|
||||
<Button @click="submitForm" class="w-full" :disabled="!requiredFieldsFilled || !meta.valid">
|
||||
{{ $t('globals.terms.continue') }}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -175,14 +194,20 @@ import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { Button } from '@shared-ui/components/ui/button'
|
||||
import { Input } from '@shared-ui/components/ui/input'
|
||||
import { Checkbox } from '@shared-ui/components/ui/checkbox'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@shared-ui/components/ui/select'
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@shared-ui/components/ui/form'
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@shared-ui/components/ui/form'
|
||||
import { useWidgetStore } from '../store/widget.js'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { createPreChatFormSchema } from './preChatFormSchema.js'
|
||||
@@ -205,27 +230,25 @@ const formFields = computed(() => config.value.fields || [])
|
||||
|
||||
// Sort and filter enabled fields, excluding default fields if user has session token
|
||||
const sortedFields = computed(() => {
|
||||
let fields = formFields.value.filter(field => field.enabled)
|
||||
|
||||
let fields = formFields.value.filter((field) => field.enabled)
|
||||
|
||||
// If user has session token, exclude default name and email fields
|
||||
if (props.excludeDefaultFields) {
|
||||
fields = fields.filter(field => !['name', 'email'].includes(field.key))
|
||||
fields = fields.filter((field) => !['name', 'email'].includes(field.key))
|
||||
}
|
||||
|
||||
|
||||
return fields.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
})
|
||||
|
||||
const showForm = computed(() => preChatFormEnabled.value && sortedFields.value.length > 0)
|
||||
|
||||
// Create form with dynamic schema based on fields
|
||||
const formSchema = computed(() =>
|
||||
toTypedSchema(createPreChatFormSchema(t, sortedFields.value))
|
||||
)
|
||||
const formSchema = computed(() => toTypedSchema(createPreChatFormSchema(t, sortedFields.value)))
|
||||
|
||||
// Generate initial values dynamically
|
||||
const initialValues = computed(() => {
|
||||
const values = {}
|
||||
sortedFields.value.forEach(field => {
|
||||
sortedFields.value.forEach((field) => {
|
||||
if (field.type === 'checkbox') {
|
||||
values[field.key] = false
|
||||
} else {
|
||||
@@ -235,21 +258,31 @@ const initialValues = computed(() => {
|
||||
return values
|
||||
})
|
||||
|
||||
const { handleSubmit, meta, resetForm } = useForm({
|
||||
const { handleSubmit, meta, values } = useForm({
|
||||
validationSchema: formSchema,
|
||||
initialValues
|
||||
})
|
||||
|
||||
const requiredFieldsFilled = computed(() => {
|
||||
return sortedFields.value
|
||||
.filter((field) => field.required)
|
||||
.every((field) => {
|
||||
const value = values[field.key]
|
||||
if (field.type === 'checkbox') return true
|
||||
return value && String(value).trim() !== ''
|
||||
})
|
||||
})
|
||||
|
||||
const submitForm = handleSubmit((values) => {
|
||||
// Filter out empty values (except for checkboxes)
|
||||
const filteredValues = {}
|
||||
Object.keys(values).forEach(key => {
|
||||
const field = sortedFields.value.find(f => f.key === key)
|
||||
Object.keys(values).forEach((key) => {
|
||||
const field = sortedFields.value.find((f) => f.key === key)
|
||||
if (field?.type === 'checkbox' || (values[key] && String(values[key]).trim())) {
|
||||
filteredValues[key] = values[key]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
emit('submit', filteredValues)
|
||||
})
|
||||
|
||||
@@ -258,7 +291,7 @@ const getFieldOptions = (field) => {
|
||||
if (field.type === 'list' && field.custom_attribute_id) {
|
||||
const customAttr = widgetStore.config?.custom_attributes?.[field.custom_attribute_id]
|
||||
if (customAttr?.values) {
|
||||
return customAttr.values.map(value => ({
|
||||
return customAttr.values.map((value) => ({
|
||||
value: value,
|
||||
label: value
|
||||
}))
|
||||
@@ -268,16 +301,13 @@ const getFieldOptions = (field) => {
|
||||
}
|
||||
|
||||
// Auto-submit for disabled mode
|
||||
watch(showForm, (newValue) => {
|
||||
if (!newValue) {
|
||||
emit('submit', {})
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Reset form when pre-chat form config changes
|
||||
watch([preChatFormEnabled, sortedFields], () => {
|
||||
resetForm({
|
||||
values: initialValues.value
|
||||
})
|
||||
}, { deep: true })
|
||||
</script>
|
||||
watch(
|
||||
showForm,
|
||||
(newValue) => {
|
||||
if (!newValue) {
|
||||
emit('submit', {})
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
@@ -26,13 +26,13 @@ export const createPreChatFormSchema = (t, fields = []) => {
|
||||
break
|
||||
|
||||
case 'date':
|
||||
fieldSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
||||
fieldSchema = z.string().regex(/^(\d{4}-\d{2}-\d{2}|)$/, {
|
||||
message: t('globals.messages.invalid', { name: field.label })
|
||||
})
|
||||
break
|
||||
|
||||
case 'link':
|
||||
fieldSchema = z.string().url({
|
||||
fieldSchema = z.string().refine((val) => val === '' || z.string().url().safeParse(val).success, {
|
||||
message: t('globals.messages.invalid', { name: t('globals.terms.url').toLowerCase() })
|
||||
})
|
||||
break
|
||||
|
@@ -23,6 +23,18 @@ const (
|
||||
MaxConnectionsPerUser = 10
|
||||
)
|
||||
|
||||
type PreChatFormField struct {
|
||||
Key string `json:"key"`
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
Placeholder string `json:"placeholder"`
|
||||
Required bool `json:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Order int `json:"order"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
CustomAttributeID int `json:"custom_attribute_id"`
|
||||
}
|
||||
|
||||
// Config holds the live chat inbox configuration.
|
||||
type Config struct {
|
||||
BrandName string `json:"brand_name"`
|
||||
@@ -71,19 +83,9 @@ type Config struct {
|
||||
ShowOfficeHoursAfterAssignment bool `json:"show_office_hours_after_assignment"`
|
||||
ChatReplyExpectationMessage string `json:"chat_reply_expectation_message"`
|
||||
PreChatForm struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Title string `json:"title"`
|
||||
Fields []struct {
|
||||
Key string `json:"key"`
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
Placeholder string `json:"placeholder"`
|
||||
Required bool `json:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Order int `json:"order"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
CustomAttributeID int `json:"custom_attribute_id,omitempty"`
|
||||
} `json:"fields"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Title string `json:"title"`
|
||||
Fields []PreChatFormField `json:"fields"`
|
||||
} `json:"prechat_form"`
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user