-
{{ $t('form.field.subject') }}
+
{{ $t('globals.terms.subject') }}
{{ conversation.subject }}
diff --git a/frontend/src/features/conversation/sidebar/ConversationSideBar.vue b/frontend/src/features/conversation/sidebar/ConversationSideBar.vue
index 034f708a..6baea76e 100644
--- a/frontend/src/features/conversation/sidebar/ConversationSideBar.vue
+++ b/frontend/src/features/conversation/sidebar/ConversationSideBar.vue
@@ -10,107 +10,31 @@
-
-
-
-
-
-
- {{ item.value === 'none' ? 'N' : item.label.slice(0, 2).toUpperCase() }}
-
-
-
{{ item.label }}
-
-
-
-
-
-
-
-
- {{
- selected?.value === 'none' ? 'N' : selected?.label?.slice(0, 2)?.toUpperCase()
- }}
-
-
-
{{ selected?.label || t('form.field.assignAgent') }}
-
-
-
+ type="user"
+ />
-
-
-
-
-
{{ item.emoji }}
-
-
-
-
-
{{ item.label }}
-
-
-
-
-
-
- {{ selected?.emoji }}
-
-
{{ selected?.label || t('form.field.assignTeam') }}
-
-
-
+ type="team"
+ />
-
-
-
-
-
-
-
{{ item.label }}
-
-
-
-
-
-
-
-
-
{{ selected?.label || t('form.field.selectPriority') }}
-
-
-
+ type="priority"
+ />
String(conversationStore.current?.assigned_user_id))
-const assignedTeamID = computed(() => String(conversationStore.current?.assigned_team_id))
-const priorityID = computed(() => String(conversationStore.current?.priority_id))
const priorityOptions = computed(() => conversationStore.priorityOptions)
const fetchTags = async () => {
@@ -288,7 +207,6 @@ const selectAgent = (agent) => {
handleRemoveAssignee('user')
return
}
- if (conversationStore.current.assigned_user_id == agent.value) return
conversationStore.current.assigned_user_id = agent.value
handleAssignedUserChange(agent.value)
}
@@ -298,31 +216,15 @@ const selectTeam = (team) => {
handleRemoveAssignee('team')
return
}
- if (conversationStore.current.assigned_team_id == team.value) return
- conversationStore.current.assigned_team_id = team.value
handleAssignedTeamChange(team.value)
}
const selectPriority = (priority) => {
- if (conversationStore.current.priority_id == priority.value) return
conversationStore.current.priority = priority.label
conversationStore.current.priority_id = priority.value
handlePriorityChange(priority.label)
}
-const getPriorityIcon = (value) => {
- switch (value) {
- case '1':
- return SignalLow
- case '2':
- return SignalMedium
- case '3':
- return SignalHigh
- default:
- return CircleAlert
- }
-}
-
const updateContactCustomAttributes = async (attributes) => {
let previousAttributes = conversationStore.current.contact.custom_attributes
try {
diff --git a/frontend/src/features/conversation/sidebar/ConversationSideBarWrapper.vue b/frontend/src/features/conversation/sidebar/ConversationSideBarWrapper.vue
index e0c35623..c8ab89de 100644
--- a/frontend/src/features/conversation/sidebar/ConversationSideBarWrapper.vue
+++ b/frontend/src/features/conversation/sidebar/ConversationSideBarWrapper.vue
@@ -14,9 +14,9 @@
diff --git a/frontend/src/features/conversation/sidebar/CustomAttributes.vue b/frontend/src/features/conversation/sidebar/CustomAttributes.vue
index d90df525..79864d34 100644
--- a/frontend/src/features/conversation/sidebar/CustomAttributes.vue
+++ b/frontend/src/features/conversation/sidebar/CustomAttributes.vue
@@ -196,7 +196,7 @@ const getValidationSchema = (attribute) => {
z
.number({
invalid_type_error: t('globals.messages.invalid', {
- name: t('form.field.value').toLowerCase()
+ name: t('globals.terms.value').toLowerCase()
})
})
.nullable()
@@ -209,7 +209,7 @@ const getValidationSchema = (attribute) => {
.refine(
(val) => !isNaN(Date.parse(val)),
t('globals.messages.invalid', {
- name: t('form.field.value').toLowerCase()
+ name: t('globals.terms.value').toLowerCase()
})
)
.nullable()
@@ -227,7 +227,7 @@ const getValidationSchema = (attribute) => {
.string()
.refine((val) => attribute.values.includes(val), {
message: t('globals.messages.invalid', {
- name: t('form.field.value').toLowerCase()
+ name: t('globals.terms.value').toLowerCase()
})
})
.nullable()
diff --git a/frontend/src/stores/conversation.js b/frontend/src/stores/conversation.js
index 5a7891a9..2bfa46d3 100644
--- a/frontend/src/stores/conversation.js
+++ b/frontend/src/stores/conversation.js
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
-import { computed, reactive, ref, nextTick, watchEffect } from 'vue'
+import { computed, reactive, ref, watchEffect } from 'vue'
import { CONVERSATION_LIST_TYPE, CONVERSATION_DEFAULT_STATUSES } from '@/constants/conversation'
import { handleHTTPError } from '@/utils/http'
import { computeRecipientsFromMessage } from '@/utils/email-recipients'
@@ -16,6 +16,7 @@ export const useConversationStore = defineStore('conversation', () => {
const currentTo = ref([])
const currentBCC = ref([])
const currentCC = ref([])
+ const macros = ref({})
// Options for select fields
const priorityOptions = computed(() => {
@@ -99,8 +100,6 @@ export const useConversationStore = defineStore('conversation', () => {
const conversation = reactive({
data: null,
participants: {},
- mediaFiles: [],
- macro: {},
loading: false,
errorMessage: ''
})
@@ -118,25 +117,6 @@ export const useConversationStore = defineStore('conversation', () => {
const incrementMessageVersion = () => setTimeout(() => messages.version++, 0)
- async function setMacro (macro) {
- // Clear existing macro.
- conversation.macro = {}
- await nextTick()
- conversation.macro = macro
- }
-
- function removeMacroAction (action) {
- conversation.macro.actions = conversation.macro.actions.filter(a => a.type !== action.type)
- }
-
- function resetMacro () {
- conversation.macro = {}
- }
-
- function resetMediaFiles () {
- conversation.mediaFiles = []
- }
-
function setListStatus (status, fetch = true) {
conversations.status = status
if (fetch) {
@@ -646,7 +626,6 @@ export const useConversationStore = defineStore('conversation', () => {
Object.assign(conversation, {
data: null,
participants: {},
- mediaFiles: [],
macro: {},
loading: false,
errorMessage: ''
@@ -660,6 +639,24 @@ export const useConversationStore = defineStore('conversation', () => {
}
+ /** Macros for new conversation or open conversation **/
+ async function setMacro (macro, context) {
+ macros.value[context] = macro
+ }
+
+ function getMacro (context) {
+ return macros.value[context] || {}
+ }
+
+ function removeMacroAction (action, context) {
+ if (!macros.value[context]) return
+ macros.value[context].actions = macros.value[context].actions.filter(a => a.type !== action.type)
+ }
+
+ function resetMacro (context) {
+ macros.value = { ...macros.value, [context]: {} }
+ }
+
return {
conversations,
conversation,
@@ -699,9 +696,9 @@ export const useConversationStore = defineStore('conversation', () => {
setListSortField,
setListStatus,
removeMacroAction,
+ getMacro,
setMacro,
resetMacro,
- resetMediaFiles,
removeAssignee,
getListSortField,
getListStatus,
diff --git a/frontend/src/stores/macro.js b/frontend/src/stores/macro.js
index f19d6250..a8962326 100644
--- a/frontend/src/stores/macro.js
+++ b/frontend/src/stores/macro.js
@@ -7,11 +7,11 @@ import { useUserStore } from './user'
import api from '@/api'
import { permissions as perms } from '@/constants/permissions.js'
-
export const useMacroStore = defineStore('macroStore', () => {
const macroList = ref([])
const emitter = useEmitter()
const userStore = useUserStore()
+ const currentView = ref('')
// actionPermissions is a map of action names to their corresponding permissions that a user must have to perform the action.
const actionPermissions = {
@@ -34,6 +34,14 @@ export const useMacroStore = defineStore('macroStore', () => {
userTeams.includes(macro.team_id) ||
String(macro.user_id) === String(userStore.userID)
)
+
+ // Filter by visible_when if currentView is set.
+ if (currentView.value) {
+ filtered = filtered.filter(macro =>
+ !macro.visible_when?.length || macro.visible_when.includes(currentView.value)
+ )
+ }
+
// Filter macros based on permissions.
filtered.forEach(macro => {
macro.actions = macro.actions.filter(action => {
@@ -42,14 +50,17 @@ export const useMacroStore = defineStore('macroStore', () => {
return userStore.can(permission)
})
})
+
// Skip macros that do not have any actions left AND the macro field `message_content` is empty.
filtered = filtered.filter(macro => !(macro.actions.length === 0 && macro.message_content === ""))
+
return filtered.map(macro => ({
...macro,
label: macro.name,
value: String(macro.id),
}))
})
+
const loadMacros = async () => {
if (macroList.value.length) return
try {
@@ -62,9 +73,15 @@ export const useMacroStore = defineStore('macroStore', () => {
})
}
}
+
+ const setCurrentView = (view) => {
+ currentView.value = view
+ }
+
return {
macroList,
macroOptions,
loadMacros,
+ setCurrentView
}
})
\ No newline at end of file
diff --git a/i18n/en.json b/i18n/en.json
index 0ac44475..30f2c2c1 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -93,8 +93,15 @@
"globals.terms.security": "Security | Security",
"globals.terms.myInbox": "My Inbox | My Inboxes",
"globals.terms.teamInbox": "Team Inbox | Team Inboxes",
+ "globals.terms.optional": "Optional | Optionals",
+ "globals.terms.visibility": "Visibility | Visibilities",
+ "globals.terms.privateNote": "Private note | Private notes",
+ "globals.terms.automationRule": "Automation Rule | Automation Rules",
+ "globals.terms.subject": "Subject | Subjects",
+ "globals.messages.replying": "Replying",
"globals.messages.golangDurationHoursMinutes": "Duration in hours or minutes. Example: 1h, 30m, 1h30m",
"globals.messages.badRequest": "Bad request",
+ "globals.messages.visibleWhen": "Visible when",
"globals.messages.adjustFilters": "Try adjusting filters",
"globals.messages.errorUpdating": "Error updating {name}",
"globals.messages.errorCreating": "Error creating {name}",
@@ -132,21 +139,21 @@
"globals.messages.create": "Create {name}",
"globals.messages.new": "New {name}",
"globals.messages.add": "Add {name}",
+ "globals.messages.adding": "Adding {name}",
+ "globals.messages.starting": "Starting {name}",
"globals.messages.all": "All {name}",
"globals.messages.denied": "{name} denied",
"globals.messages.noResults": "No {name} found",
"globals.messages.enter": "Enter {name}",
"globals.messages.yes": "Yes",
- "globals.messages.no": "No",
+ "globals.messages.no": "No {name}",
+ "globals.messages.type": "{name} type",
"globals.messages.typeOf": "Type of {name}",
"globals.messages.invalidEmailAddress": "Invalid email address",
"globals.messages.pleaseSelectAtLeastOne": "Please select at least one {name}",
"globals.messages.strongPassword": "Password must be between {min} and {max} characters long, should contain at least one uppercase letter, one lowercase letter, one number, and one special character.",
- "globals.messages.couldNotReload": "Could not reload {name}. Please restart the app",
- "globals.messages.invalid": "Invalid {name}",
- "globals.messages.disabled": "{name} is disabled",
- "globals.messages.fieldRequired": "{name} required",
- "globals.messages.required": "Required",
+ "globals.messages.couldNotReload": "Could not reload {name}",
+ "globals.messages.required": "{name} Required",
"globals.messages.invalidPortNumber": "Invalid port number",
"globals.messages.mustBeNumber": "Must be a number",
"globals.messages.fileTypeisNotAnImage": "File type is not an image",
@@ -170,6 +177,7 @@
"globals.messages.snooze": "Snooze",
"globals.messages.resolve": "Resolve",
"globals.messages.applyMacro": "Apply macro",
+ "globals.messages.deletionConfirmation": "This action cannot be undone. This will permanently delete this {name}.",
"globals.messages.atleastOneRecipient": "At least one recipient is required",
"globals.messages.startTypingToSearch": "Start typing to search...",
"globals.messages.goHourMinuteDuration": "Invalid duration format. Should be a number followed by h (hours), m (minutes).",
@@ -256,7 +264,6 @@
"form.field.clientID": "Client ID",
"form.field.clientSecret": "Client Secret",
"form.field.callbackURL": "Callback URL",
- "form.field.subject": "Subject",
"form.field.referenceNumber": "Reference Number",
"form.field.initiatedAt": "Initiated at",
"form.field.firstReplyAt": "First Reply At",
@@ -311,15 +318,12 @@
"form.field.addNewAction": "Add new action",
"form.field.selectType": "Select type",
"form.field.selectInbox": "Select an inbox",
- "form.field.assignTeamOptional": "Assign team (optional)",
- "form.field.assignAgentOptional": "Assign agent (optional)",
"form.field.assignAgent": "Assign agent",
"form.field.assignTeam": "Assign team",
"form.field.message": "Message",
"form.field.setValue": "Set value",
"form.field.selectEvents": "Select events",
"form.field.selectOperator": "Select operator",
- "form.field.value": "Value",
"form.error.min": "Must be at least {min} characters",
"form.error.max": "Must be at most {max} characters",
"form.error.minmax": "Must be between {min} and {max} characters",
@@ -387,13 +391,8 @@
"admin.conversationTags.deleteConfirmation": "This action cannot be undone. This will permanently delete this tag, and remove it from all conversations.",
"admin.macro.messageContent": "Response to be sent when macro is used (optional)",
"admin.macro.actions": "Actions (optional)",
- "admin.macro.visibility": "Visibility",
- "admin.macro.visibility.all": "All users",
"admin.macro.messageOrActionRequired": "Either message content or actions are required",
- "admin.macro.actionTypeRequired": "Action type is required",
- "admin.macro.actionValueRequired": "Action value is required",
- "admin.macro.teamOrUserRequired": "team is required when visibility is `team` & a user is required when visibility is `user`",
- "admin.macro.deleteConfirmation": "This action cannot be undone. This will permanently delete this macro.",
+ "admin.macro.actionInvalid": "Each action must have a type and a value",
"admin.conversationStatus.name.description": "Set status name. Click save when you're done.",
"admin.conversationStatus.deleteConfirmation": "This action cannot be undone. This will permanently delete this status.",
"admin.inbox.name.description": "Name for your inbox.",
@@ -590,14 +589,14 @@
"conversation.sidebar.previousConvo": "Previous conversations",
"conversation.sidebar.noPreviousConvo": "No previous conversations",
"conversation.sidebar.notAvailable": "Not available",
- "editor.placeholder": "Shift + Enter to add a new line",
+ "editor.newLine": "Shift + Enter to add a new line. ",
+ "editor.send": " Cmd + Enter to send. ",
+ "editor.cmdK": "Cmd + K to open command bar. ",
"ai.apiKeyNotSet": "{provider} API Key is not set. Please ask administrator to set it up",
"ai.enterOpenAIAPIKey": "Enter OpenAI API Key",
"ai.apiKey.description": "{provider} API Key is not set or invalid. Please enter a valid API key to use AI features.",
"replyBox.reply": "Reply",
- "replyBox.privateNote": "Private note",
"replyBox.emailAddresess": "Email addresses separated by comma",
- "replyBox.editor.placeholder": "Shift + Enter to add a new line. Cmd + Enter to send. Cmd + K to open command bar.",
"replyBox.invalidEmailsIn": "Invalid email(s) in",
"replyBox.correctEmailErrors": "Please correct the email errors before sending.",
"contact.blockConfirm": "Are you sure you want to block this contact? They will no longer be able to interact with you.",
diff --git a/internal/macro/macro.go b/internal/macro/macro.go
index 035944f3..6ad925e8 100644
--- a/internal/macro/macro.go
+++ b/internal/macro/macro.go
@@ -11,6 +11,7 @@ import (
"github.com/abhinavxd/libredesk/internal/macro/models"
"github.com/jmoiron/sqlx"
"github.com/knadh/go-i18n"
+ "github.com/lib/pq"
"github.com/zerodha/logf"
)
@@ -67,8 +68,8 @@ func (m *Manager) Get(id int) (models.Macro, error) {
}
// Create adds a new macro.
-func (m *Manager) Create(name, messageContent string, userID, teamID *int, visibility string, actions json.RawMessage) error {
- _, err := m.q.Create.Exec(name, messageContent, userID, teamID, visibility, actions)
+func (m *Manager) Create(name, messageContent string, userID, teamID *int, visibility string, visibleWhen []string, actions json.RawMessage) error {
+ _, err := m.q.Create.Exec(name, messageContent, userID, teamID, visibility, pq.StringArray(visibleWhen), actions)
if err != nil {
m.lo.Error("error creating macro", "error", err)
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.macro}"), nil)
@@ -77,8 +78,8 @@ func (m *Manager) Create(name, messageContent string, userID, teamID *int, visib
}
// Update modifies an existing macro.
-func (m *Manager) Update(id int, name, messageContent string, userID, teamID *int, visibility string, actions json.RawMessage) error {
- result, err := m.q.Update.Exec(id, name, messageContent, userID, teamID, visibility, actions)
+func (m *Manager) Update(id int, name, messageContent string, userID, teamID *int, visibility string, visibleWhen []string, actions json.RawMessage) error {
+ result, err := m.q.Update.Exec(id, name, messageContent, userID, teamID, visibility, pq.StringArray(visibleWhen), actions)
if err != nil {
m.lo.Error("error updating macro", "error", err)
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.macro}"), nil)
diff --git a/internal/macro/models/models.go b/internal/macro/models/models.go
index 1ed30aad..3cfa8401 100644
--- a/internal/macro/models/models.go
+++ b/internal/macro/models/models.go
@@ -3,6 +3,8 @@ package models
import (
"encoding/json"
"time"
+
+ "github.com/lib/pq"
)
type Macro struct {
@@ -11,7 +13,8 @@ type Macro struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
MessageContent string `db:"message_content" json:"message_content"`
- Visibility string `db:"visibility" json:"visibility"`
+ VisibleWhen pq.StringArray `db:"visible_when" json:"visible_when"`
+ Visibility string `db:"visibility" json:"visibility"`
UserID *int `db:"user_id" json:"user_id,string"`
TeamID *int `db:"team_id" json:"team_id,string"`
UsageCount int `db:"usage_count" json:"usage_count"`
diff --git a/internal/macro/queries.sql b/internal/macro/queries.sql
index 80dd1f31..44ac3230 100644
--- a/internal/macro/queries.sql
+++ b/internal/macro/queries.sql
@@ -9,6 +9,7 @@ SELECT
user_id,
team_id,
actions,
+ visible_when,
usage_count
FROM
macros
@@ -26,6 +27,7 @@ SELECT
user_id,
team_id,
actions,
+ visible_when,
usage_count
FROM
macros
@@ -34,9 +36,9 @@ ORDER BY
-- name: create
INSERT INTO
- macros (name, message_content, user_id, team_id, visibility, actions)
+ macros (name, message_content, user_id, team_id, visibility, visible_when, actions)
VALUES
- ($1, $2, $3, $4, $5, $6);
+ ($1, $2, $3, $4, $5, $6, $7);
-- name: update
UPDATE
@@ -47,7 +49,8 @@ SET
user_id = $4,
team_id = $5,
visibility = $6,
- actions = $7,
+ visible_when = $7,
+ actions = $8,
updated_at = NOW()
WHERE
id = $1;
@@ -62,6 +65,7 @@ WHERE
UPDATE
macros
SET
- usage_count = usage_count + 1
+ usage_count = usage_count + 1,
+ updated_at = NOW()
WHERE
id = $1;
\ No newline at end of file
diff --git a/internal/migrations/v0.6.0.go b/internal/migrations/v0.6.0.go
index fc501567..94bc85b2 100644
--- a/internal/migrations/v0.6.0.go
+++ b/internal/migrations/v0.6.0.go
@@ -287,5 +287,29 @@ func V0_6_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
return err
}
+ // Add macro macro_visible_when enum type if it doesn't exist
+ _, err = db.Exec(`
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM pg_type WHERE typname = 'macro_visible_when'
+ ) THEN
+ CREATE TYPE macro_visible_when AS ENUM ('replying', 'starting_conversation', 'adding_private_note');
+ END IF;
+ END
+ $$;
+ `)
+ if err != nil {
+ return err
+ }
+
+ // Add visible_when column to macros table if it doesn't exist
+ _, err = db.Exec(`
+ ALTER TABLE macros
+ ADD COLUMN IF NOT EXISTS visible_when macro_visible_when[] NOT NULL DEFAULT ARRAY['replying', 'starting_conversation', 'adding_private_note']::macro_visible_when[];
+ `)
+ if err != nil {
+ return err
+ }
return nil
}
diff --git a/schema.sql b/schema.sql
index 248a052a..043179c6 100644
--- a/schema.sql
+++ b/schema.sql
@@ -19,6 +19,7 @@ DROP TYPE IF EXISTS "sla_event_status" CASCADE; CREATE TYPE "sla_event_status" A
DROP TYPE IF EXISTS "sla_metric" CASCADE; CREATE TYPE "sla_metric" AS ENUM ('first_response', 'resolution', 'next_response');
DROP TYPE IF EXISTS "sla_notification_type" CASCADE; CREATE TYPE "sla_notification_type" AS ENUM ('warning', 'breach');
DROP TYPE IF EXISTS "activity_log_type" CASCADE; CREATE TYPE "activity_log_type" AS ENUM ('agent_login', 'agent_logout', 'agent_away', 'agent_away_reassigned', 'agent_online');
+DROP TYPE IF EXISTS "macro_visible_when" CASCADE; CREATE TYPE "macro_visible_when" AS ENUM ('replying', 'starting_conversation', 'adding_private_note');
-- Sequence to generate reference number for conversations.
DROP SEQUENCE IF EXISTS conversation_reference_number_sequence; CREATE SEQUENCE conversation_reference_number_sequence START 100;
@@ -291,6 +292,7 @@ CREATE TABLE macros (
name TEXT NOT NULL,
actions JSONB DEFAULT '{}'::jsonb NOT NULL,
visibility macro_visibility NOT NULL,
+ visible_when macro_visible_when[] NOT NULL DEFAULT ARRAY['replying', 'starting_conversation', 'adding_private_note']::macro_visible_when[],
message_content TEXT NOT NULL,
-- Cascade deletes when user is deleted.
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,