Add prompt-to-tag-on-reply inbox setting

This commit is contained in:
Abhinav Raut
2026-04-21 20:24:19 +05:30
parent bcd93514ef
commit 06b36b74fd
14 changed files with 108 additions and 17 deletions
+7 -6
View File
@@ -326,12 +326,13 @@ func handleOAuthCallback(r *fastglue.Request) error {
// Create inbox
newInbox := imodels.Inbox{
Name: fmt.Sprintf("%s Inbox", userEmail),
From: userEmail,
Channel: inbox.ChannelEmail,
Enabled: true,
CSATEnabled: false,
Config: json.RawMessage(configJSON),
Name: fmt.Sprintf("%s Inbox", userEmail),
From: userEmail,
Channel: inbox.ChannelEmail,
Enabled: true,
CSATEnabled: false,
PromptTagsOnReply: false,
Config: json.RawMessage(configJSON),
}
createdInbox, err := app.inbox.Create(newInbox)
+1
View File
@@ -41,6 +41,7 @@ var migList = []migFunc{
{"v0.10.0", migrations.V0_10_0},
{"v1.0.1", migrations.V1_0_1},
{"v2.0.0", migrations.V2_0_0},
{"v2.1.0", migrations.V2_1_0},
}
// upgrade upgrades the database to the current version by running SQL migration files
@@ -70,6 +70,21 @@
</FormItem>
</FormField>
<FormField
v-if="showFormFields"
v-slot="{ componentField, handleChange }"
name="prompt_tags_on_reply"
>
<FormItem>
<SwitchField
:title="$t('admin.inbox.promptTagsOnReply')"
:description="$t('admin.inbox.promptTagsOnReply.description')"
:checked="componentField.modelValue"
@update:checked="handleChange"
/>
</FormItem>
</FormField>
<FormField v-if="setupMethod" v-slot="{ componentField }" name="auth_type">
<FormItem>
<FormControl>
@@ -750,6 +765,7 @@ const form = useForm({
from: '',
enabled: true,
csat_enabled: false,
prompt_tags_on_reply: false,
enable_plus_addressing: true,
auth_type: AUTH_TYPE_PASSWORD,
imap: {
@@ -45,6 +45,17 @@
</p>
</FormField>
<FormField v-slot="{ componentField, handleChange }" name="prompt_tags_on_reply">
<FormItem>
<SwitchField
:title="$t('admin.inbox.promptTagsOnReply')"
:description="$t('admin.inbox.promptTagsOnReply.description')"
:checked="componentField.modelValue"
@update:checked="handleChange"
/>
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>{{ $t('globals.terms.name') }}</FormLabel>
@@ -1112,6 +1123,7 @@ const form = useForm({
enabled: true,
secret: '',
csat_enabled: false,
prompt_tags_on_reply: false,
linked_email_inbox_id: null,
config: {
brand_name: '',
@@ -7,6 +7,7 @@ export const createFormSchema = (t) => z.object({
from: z.string().min(1, t('globals.messages.required')),
enabled: z.boolean().optional(),
csat_enabled: z.boolean().optional(),
prompt_tags_on_reply: z.boolean().optional(),
enable_plus_addressing: z.boolean().optional(),
auth_type: z.enum([AUTH_TYPE_PASSWORD, AUTH_TYPE_OAUTH2]),
oauth: z.object({
@@ -14,6 +14,7 @@ export const createFormSchema = (t) => z.object({
name: z.string().min(1, { message: t('globals.messages.required') }),
enabled: z.boolean(),
csat_enabled: z.boolean(),
prompt_tags_on_reply: z.boolean(),
secret: z.string().nullable().optional(),
linked_email_inbox_id: z.number().nullable().optional(),
config: z.object({
@@ -13,7 +13,24 @@
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ $t('globals.messages.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="processSend(true)">{{
<AlertDialogAction @click="processSend(true, true)">{{
$t('replyBox.sendAnyway')
}}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog :open="showMissingTagsWarning" @update:open="showMissingTagsWarning = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ $t('replyBox.missingTagsTitle') }}</AlertDialogTitle>
<AlertDialogDescription>
{{ $t('replyBox.missingTagsDescription') }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ $t('globals.messages.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="processSend(false, true)">{{
$t('replyBox.sendAnyway')
}}</AlertDialogAction>
</AlertDialogFooter>
@@ -138,6 +155,7 @@ import { useDraftManager } from '@main/composables/useDraftManager'
import api from '@main/api'
import { useI18n } from 'vue-i18n'
import { useConversationStore } from '@main/stores/conversation'
import { useInboxStore } from '@main/stores/inbox'
import {
AlertDialog,
AlertDialogAction,
@@ -181,6 +199,7 @@ const formSchema = toTypedSchema(
const { t } = useI18n()
const conversationStore = useConversationStore()
const inboxStore = useInboxStore()
const emitter = useEmitter()
const userStore = useUserStore()
@@ -221,6 +240,7 @@ const emailErrors = ref([])
const aiPrompts = ref([])
const replyBoxContentRef = ref(null)
const showContactEmailWarning = ref(false)
const showMissingTagsWarning = ref(false)
const mentions = ref([])
/**
@@ -297,7 +317,7 @@ const hasTextContent = computed(() => {
/**
* Processes the send action.
*/
const processSend = async (skipContactEmailCheck = false) => {
const processSend = async (skipContactEmailCheck = false, skipMissingTagsCheck = false) => {
let hasMessageSendingErrored = false
isEditorFullscreen.value = false
@@ -305,6 +325,19 @@ const processSend = async (skipContactEmailCheck = false) => {
const convUUID = conversationStore.current.uuid
const isPrivate = messageType.value === 'private_note'
const currentInbox = inboxStore.inboxes.find(
(i) => i.id === conversationStore.current.inbox_id
)
if (
!isPrivate &&
!skipMissingTagsCheck &&
currentInbox?.prompt_tags_on_reply &&
!(conversationStore.current.tags?.length > 0)
) {
showMissingTagsWarning.value = true
return
}
if (!isPrivate && conversationStore.current.inbox_channel === 'email') {
// Require at least one recipient in `to`.
if (!to.value.trim()) {
@@ -134,6 +134,7 @@ const submitLiveChatForm = (values) => {
channel: 'livechat',
enabled: values.enabled ?? true,
csat_enabled: values.csat_enabled ?? false,
prompt_tags_on_reply: values.prompt_tags_on_reply ?? false,
config: values.config
}
createInbox(payload)
+4
View File
@@ -128,6 +128,8 @@
"admin.inbox.csatSurveys.description_3": "CSAT surveys are only sent once per conversation.",
"admin.inbox.enablePlusAddressing": "Enable plus addressing",
"admin.inbox.enablePlusAddressing.description": "Improves conversation threading but requires provider support (e.g., Gmail, Microsoft 365).",
"admin.inbox.promptTagsOnReply": "Prompt to tag before replying",
"admin.inbox.promptTagsOnReply.description": "Warn agents before they send a reply on a conversation that has no tags. Agents can still send without tagging by confirming the prompt.",
"admin.inbox.enabled.description": "Toggle scanning inbox and sending out messages.",
"admin.inbox.fromEmailAddress.description": "From email address for your inbox. e.g. My inbox <support{'@'}example.com>",
"admin.inbox.fromEmailAddress.placeholder": "My inbox <support{'@'}example.com>",
@@ -905,6 +907,8 @@
"replyBox.contactEmailMissingDescription": "The contact's email ({email}) is not included in to, cc, or bcc. The contact won't receive this reply.",
"replyBox.emailAddresess": "Email addresses separated by comma",
"replyBox.invalidEmailsIn": "Invalid email(s) in",
"replyBox.missingTagsDescription": "This conversation has no tags. Tagging helps with reporting and triage. You can still send without tagging.",
"replyBox.missingTagsTitle": "No tags on this conversation",
"replyBox.removeBCC": "Remove BCC",
"replyBox.sendAnyway": "Send anyway",
"replyBox.toRequired": "At least one recipient is required in the To field.",
+2 -2
View File
@@ -250,7 +250,7 @@ func (m *Manager) Create(inbox imodels.Inbox) (imodels.Inbox, error) {
}
var createdInbox imodels.Inbox
if err := m.queries.InsertInbox.Get(&createdInbox, inbox.Channel, encryptedConfig, inbox.Name, inbox.From, inbox.CSATEnabled, inbox.Secret, inbox.LinkedEmailInboxID); err != nil {
if err := m.queries.InsertInbox.Get(&createdInbox, inbox.Channel, encryptedConfig, inbox.Name, inbox.From, inbox.CSATEnabled, inbox.PromptTagsOnReply, inbox.Secret, inbox.LinkedEmailInboxID); err != nil {
m.lo.Error("error creating inbox", "error", err)
return imodels.Inbox{}, envelope.NewError(envelope.GeneralError, m.i18n.T("globals.messages.somethingWentWrong"), nil)
}
@@ -424,7 +424,7 @@ func (m *Manager) Update(id int, inbox imodels.Inbox) (imodels.Inbox, error) {
// Update the inbox in the DB.
var updatedInbox imodels.Inbox
if err := m.queries.Update.Get(&updatedInbox, id, inbox.Channel, encryptedConfig, inbox.Name, inbox.From, inbox.CSATEnabled, inbox.Enabled, inbox.Secret, inbox.LinkedEmailInboxID); err != nil {
if err := m.queries.Update.Get(&updatedInbox, id, inbox.Channel, encryptedConfig, inbox.Name, inbox.From, inbox.CSATEnabled, inbox.PromptTagsOnReply, inbox.Enabled, inbox.Secret, inbox.LinkedEmailInboxID); err != nil {
m.lo.Error("error updating inbox", "error", err)
return imodels.Inbox{}, envelope.NewError(envelope.GeneralError, m.i18n.T("globals.messages.somethingWentWrong"), nil)
}
+1
View File
@@ -27,6 +27,7 @@ type Inbox struct {
Channel string `db:"channel" json:"channel"`
Enabled bool `db:"enabled" json:"enabled"`
CSATEnabled bool `db:"csat_enabled" json:"csat_enabled"`
PromptTagsOnReply bool `db:"prompt_tags_on_reply" json:"prompt_tags_on_reply"`
From string `db:"from" json:"from"`
Config json.RawMessage `db:"config" json:"config"`
Secret null.String `db:"secret" json:"secret"`
+7 -7
View File
@@ -1,24 +1,24 @@
-- name: get-active-inboxes
SELECT id, uuid, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, config, "from", linked_email_inbox_id FROM inboxes where enabled is TRUE and deleted_at is NULL;
SELECT id, uuid, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, prompt_tags_on_reply, config, "from", linked_email_inbox_id FROM inboxes where enabled is TRUE and deleted_at is NULL;
-- name: get-all-inboxes
SELECT id, uuid, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, config, "from", linked_email_inbox_id FROM inboxes where deleted_at is NULL;
SELECT id, uuid, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, prompt_tags_on_reply, config, "from", linked_email_inbox_id FROM inboxes where deleted_at is NULL;
-- name: insert-inbox
INSERT INTO inboxes
(channel, config, "name", "from", csat_enabled, secret, linked_email_inbox_id)
VALUES($1, $2, $3, $4, $5, $6, $7)
(channel, config, "name", "from", csat_enabled, prompt_tags_on_reply, secret, linked_email_inbox_id)
VALUES($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
-- name: get-inbox
SELECT id, uuid, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, config, "from", secret, linked_email_inbox_id FROM inboxes where id = $1 and deleted_at is NULL;
SELECT id, uuid, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, prompt_tags_on_reply, config, "from", secret, linked_email_inbox_id FROM inboxes where id = $1 and deleted_at is NULL;
-- name: get-inbox-by-uuid
SELECT id, uuid, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, config, "from", secret, linked_email_inbox_id FROM inboxes where uuid = $1 and deleted_at is NULL;
SELECT id, uuid, created_at, updated_at, "name", deleted_at, channel, enabled, csat_enabled, prompt_tags_on_reply, config, "from", secret, linked_email_inbox_id FROM inboxes where uuid = $1 and deleted_at is NULL;
-- name: update
UPDATE inboxes
set channel = $2, config = $3, "name" = $4, "from" = $5, csat_enabled = $6, enabled = $7, secret = $8, linked_email_inbox_id = $9, updated_at = now()
set channel = $2, config = $3, "name" = $4, "from" = $5, csat_enabled = $6, prompt_tags_on_reply = $7, enabled = $8, secret = $9, linked_email_inbox_id = $10, updated_at = now()
where id = $1 and deleted_at is NULL
RETURNING *;
+19
View File
@@ -0,0 +1,19 @@
package migrations
import (
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)
// V2_1_0 updates the database schema to v2.1.0.
func V2_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
_, err := db.Exec(`
ALTER TABLE inboxes ADD COLUMN IF NOT EXISTS prompt_tags_on_reply bool DEFAULT false NOT NULL;
`)
if err != nil {
return err
}
return nil
}
+1
View File
@@ -84,6 +84,7 @@ CREATE TABLE inboxes (
channel channels NOT NULL,
enabled bool DEFAULT TRUE NOT NULL,
csat_enabled bool DEFAULT false NOT NULL,
prompt_tags_on_reply bool DEFAULT false NOT NULL,
config jsonb DEFAULT '{}'::jsonb NOT NULL,
"from" TEXT NULL,
secret TEXT NULL,