Compare commits
35 commits
develop
...
macros-tes
Author | SHA1 | Date | |
---|---|---|---|
|
09b5a101d0 | ||
|
b21d3f5def | ||
|
0ba2969eae | ||
|
05b31f90cb | ||
|
3dbbb10481 | ||
|
4aa154f77d | ||
|
ae304bc88e | ||
|
30b7b2f135 | ||
|
cfaa44ea67 | ||
|
a600652867 | ||
|
f48c14104c | ||
|
4f3a31a5c9 | ||
|
687055b0c0 | ||
|
47d8c9335d | ||
|
56bb2a9121 | ||
|
2b40dfc98f | ||
|
a9a8d8f70a | ||
|
54076a35ec | ||
|
534eb909b7 | ||
|
16d736d43d | ||
|
9f1556f195 | ||
|
428dafaefe | ||
|
291dbd5a5a | ||
|
ad554a2341 | ||
|
76a878c0ef | ||
|
34700d508d | ||
|
95ed729705 | ||
|
6cbe52cfcd | ||
|
4640d698d0 | ||
|
ce134816b1 | ||
|
3685ca1091 | ||
|
a55e9eef86 | ||
|
c5d875cb08 | ||
|
6ce46ea4f5 | ||
|
611ecfe46e |
40 changed files with 1816 additions and 60 deletions
|
@ -14,6 +14,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
|
||||
|
||||
@macro.save!
|
||||
process_attachments
|
||||
@macro
|
||||
end
|
||||
|
||||
def show
|
||||
|
@ -25,10 +27,21 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||
head :ok
|
||||
end
|
||||
|
||||
def attach_file
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:attachment].tempfile,
|
||||
filename: params[:attachment].original_filename,
|
||||
content_type: params[:attachment].content_type
|
||||
)
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
@macro.update!(macros_with_user)
|
||||
@macro.set_visibility(current_user, permitted_params)
|
||||
process_attachments
|
||||
@macro.save!
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
|
@ -42,6 +55,17 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||
head :ok
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
||||
return if actions.blank?
|
||||
|
||||
actions.each do |action|
|
||||
blob_id = action['action_params']
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
@macro.files.attach(blob)
|
||||
end
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(
|
||||
:name, :account_id, :visibility,
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
class="filter"
|
||||
:class="{ error: v.action_params.$dirty && v.action_params.$error }"
|
||||
>
|
||||
<div class="filter" :class="actionInputStyles">
|
||||
<div class="filter-inputs">
|
||||
<select
|
||||
v-model="action_name"
|
||||
|
@ -60,6 +57,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
v-if="!isMacro"
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
|
@ -120,6 +118,10 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isMacro: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
action_name: {
|
||||
|
@ -146,6 +148,12 @@ export default {
|
|||
return this.actionTypes.find(action => action.key === this.action_name)
|
||||
.inputType;
|
||||
},
|
||||
actionInputStyles() {
|
||||
return {
|
||||
error: this.v.action_params.$dirty && this.v.action_params.$error,
|
||||
'is-a-macro': this.isMacro,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeAction() {
|
||||
|
@ -165,6 +173,18 @@ export default {
|
|||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium);
|
||||
margin-bottom: var(--space-small);
|
||||
|
||||
&.is-a-macro {
|
||||
margin-bottom: 0;
|
||||
background: var(--white);
|
||||
padding: var(--space-zero);
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.no-margin-bottom {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter.error {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
type="outline"
|
||||
class="error-icon"
|
||||
/>
|
||||
<p class="file-button">{{ label }}</p>
|
||||
<span class="file-button">{{ label }}</span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
|
71
app/javascript/dashboard/helper/specs/macrosFixtures.js
Normal file
71
app/javascript/dashboard/helper/specs/macrosFixtures.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
export const teams = [
|
||||
{
|
||||
id: 1,
|
||||
name: '⚙️ sales team',
|
||||
description: 'This is our internal sales team',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '🤷♂️ fayaz',
|
||||
description: 'Test',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '🇮🇳 apac sales',
|
||||
description: 'Sales team for France Territory',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const labels = [
|
||||
{
|
||||
id: 6,
|
||||
title: 'sales',
|
||||
description: 'sales team',
|
||||
color: '#8EA20F',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'billing',
|
||||
description: 'billing',
|
||||
color: '#4077DA',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'snoozed',
|
||||
description: 'Items marked for later',
|
||||
color: '#D12F42',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'mobile-app',
|
||||
description: 'tech team',
|
||||
color: '#2DB1CC',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
title: 'human-resources-department-with-long-title',
|
||||
description: 'Test',
|
||||
color: '#FF6E09',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
title: 'priority',
|
||||
description: 'For important sales leads',
|
||||
color: '#7E7CED',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
];
|
53
app/javascript/dashboard/helper/specs/macrosHelper.spec.js
Normal file
53
app/javascript/dashboard/helper/specs/macrosHelper.spec.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { MACRO_ACTION_TYPES } from '../../routes/dashboard/settings/macros/constants';
|
||||
|
||||
import { teams, labels } from './macrosFixtures';
|
||||
import {
|
||||
resolveActionName,
|
||||
emptyMacro,
|
||||
resolveLabels,
|
||||
resolveTeamIds,
|
||||
} from '../../routes/dashboard/settings/macros/macroHelper';
|
||||
|
||||
describe('#resolveActionName', () => {
|
||||
it('resolve action name from key and return the correct label', () => {
|
||||
expect(resolveActionName(MACRO_ACTION_TYPES[0].key)).toEqual(
|
||||
MACRO_ACTION_TYPES[0].label
|
||||
);
|
||||
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).toEqual(
|
||||
MACRO_ACTION_TYPES[1].label
|
||||
);
|
||||
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).not.toEqual(
|
||||
MACRO_ACTION_TYPES[0].label
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#emptyMacro', () => {
|
||||
const defaultMacro = {
|
||||
name: '',
|
||||
actions: [
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [],
|
||||
},
|
||||
],
|
||||
visibility: 'global',
|
||||
};
|
||||
it('returns the default macro', () => {
|
||||
expect(emptyMacro).toEqual(defaultMacro);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolveTeamIds', () => {
|
||||
it('resolves team names from ids, and returns a joined string', () => {
|
||||
const resolvedTeams = '⚙️ sales team, 🤷♂️ fayaz';
|
||||
expect(resolveTeamIds(teams, [1, 2])).toEqual(resolvedTeams);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolveLabels', () => {
|
||||
it('resolves labels names from ids and returns a joined string', () => {
|
||||
const resolvedLabels = 'sales, billing';
|
||||
expect(resolveLabels(labels, ['sales', 'billing'])).toEqual(resolvedLabels);
|
||||
});
|
||||
});
|
|
@ -208,7 +208,8 @@
|
|||
"CONVERSATION_LABELS": "Conversation Labels",
|
||||
"CONVERSATION_INFO": "Conversation Information",
|
||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||
"PREVIOUS_CONVERSATION": "Previous Conversations"
|
||||
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
|
|
@ -1,5 +1,106 @@
|
|||
{
|
||||
"MACROS": {
|
||||
"HEADER": "Macros"
|
||||
"HEADER": "Macros",
|
||||
"HEADER_BTN_TXT": "Add a new macro",
|
||||
"HEADER_BTN_TXT_SAVE": "Save macro",
|
||||
"LOADING": "Fetching macros",
|
||||
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
|
||||
"ERROR": "Something went wrong. Please try again",
|
||||
"ORDER_INFO": "Macros will run in the order you add yout actions. You can rearrange them by dragging them by the handle beside each action.",
|
||||
"ADD": {
|
||||
"TITLE": "Add new Macro",
|
||||
"SUBMIT": "Create",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Macro name",
|
||||
"PLACEHOLDER": "Enter a name for your macro",
|
||||
"ERROR": "Name is required for creating a macro"
|
||||
},
|
||||
"DESC": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Enter macro description",
|
||||
"ERROR": "Description is required"
|
||||
},
|
||||
"CONDITIONS": {
|
||||
"LABEL": "Conditions"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"LABEL": "Actions"
|
||||
}
|
||||
},
|
||||
"ACTION_BUTTON_LABEL": "Add Action",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Macro added successfully",
|
||||
"ERROR_MESSAGE": "Unable to create macro, Please try again later"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Created by",
|
||||
"Last updated by",
|
||||
"Visibility"
|
||||
],
|
||||
"404": "No macros found"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Macro",
|
||||
"SUBMIT": "Delete",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Macro deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not able to delete macro, Please try again later"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Macro",
|
||||
"SUBMIT": "Update",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Macro updated successfully",
|
||||
"ERROR_MESSAGE": "Could not update Macro, Please try again later"
|
||||
}
|
||||
},
|
||||
"FORM": {
|
||||
"EDIT": "Edit",
|
||||
"CREATE": "Create",
|
||||
"DELETE": "Delete",
|
||||
"CANCEL": "Cancel",
|
||||
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
|
||||
},
|
||||
"ACTION": {
|
||||
"DELETE_MESSAGE": "You need to have atleast one action to save",
|
||||
"TEAM_MESSAGE_INPUT_PLACEHOLDER": "Enter your message here",
|
||||
"TEAM_DROPDOWN_PLACEHOLDER": "Select teams"
|
||||
},
|
||||
"EDITOR": {
|
||||
"START_FLOW": "Start Flow",
|
||||
"END_FLOW": "End Flow",
|
||||
"ADD_BTN_TOOLTIP": "Add new action",
|
||||
"DELETE_BTN_TOOLTIP": "Delete Action",
|
||||
"VISIBILITY": {
|
||||
"LABEL": "Macro Visibility",
|
||||
"GLOBAL": {
|
||||
"LABEL": "Public",
|
||||
"DESCRIPTION": "This macro is available publicly for all agents in this account."
|
||||
},
|
||||
"PERSONAL": {
|
||||
"LABEL": "Private",
|
||||
"DESCRIPTION": "This macro will be private to you and not be available to others."
|
||||
}
|
||||
}
|
||||
},
|
||||
"EXECUTE": {
|
||||
"BUTTON_TOOLTIP": "Execute",
|
||||
"PREVIEW": "Preview Macro",
|
||||
"EXECUTED_SUCCESFULLY": "Macro executed successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ describe('uiSettingsMixin', () => {
|
|||
{ name: 'conversation_info' },
|
||||
{ name: 'contact_attributes' },
|
||||
{ name: 'previous_conversation' },
|
||||
{ name: 'macros' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = [
|
|||
{ name: 'conversation_info' },
|
||||
{ name: 'contact_attributes' },
|
||||
{ name: 'previous_conversation' },
|
||||
{ name: 'macros' },
|
||||
];
|
||||
export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = [
|
||||
{ name: 'contact_attributes' },
|
||||
|
@ -30,7 +31,18 @@ export default {
|
|||
...mapGetters({ uiSettings: 'getUISettings' }),
|
||||
conversationSidebarItemsOrder() {
|
||||
const { conversation_sidebar_items_order: itemsOrder } = this.uiSettings;
|
||||
return itemsOrder || DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
|
||||
// If the sidebar order is not set, use the default order.
|
||||
if (!itemsOrder) {
|
||||
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
|
||||
}
|
||||
|
||||
// If the sidebar order doesn't have the new elements, then add them to the list.
|
||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => {
|
||||
if (!itemsOrder.find(i => i.name === item.name)) {
|
||||
itemsOrder.push(item);
|
||||
}
|
||||
});
|
||||
return itemsOrder;
|
||||
},
|
||||
contactSidebarItemsOrder() {
|
||||
const { contact_sidebar_items_order: itemsOrder } = this.uiSettings;
|
||||
|
|
|
@ -93,6 +93,16 @@
|
|||
/>
|
||||
</accordion-item>
|
||||
</div>
|
||||
<div v-else-if="element.name === 'macros'">
|
||||
<accordion-item
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.MACROS')"
|
||||
:is-open="isContactSidebarItemOpen('is_macro_open')"
|
||||
compact
|
||||
@click="value => toggleSidebarUIState('is_macro_open', value)"
|
||||
>
|
||||
<macros-list :conversation-id="conversationId" />
|
||||
</accordion-item>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
|
@ -105,6 +115,7 @@ import alertMixin from 'shared/mixins/alertMixin';
|
|||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem';
|
||||
import ContactConversations from './ContactConversations.vue';
|
||||
import ConversationAction from './ConversationAction.vue';
|
||||
import MacrosList from './Macros/List';
|
||||
|
||||
import ContactInfo from './contact/ContactInfo';
|
||||
import ConversationInfo from './ConversationInfo';
|
||||
|
@ -123,6 +134,7 @@ export default {
|
|||
CustomAttributeSelector,
|
||||
ConversationAction,
|
||||
draggable,
|
||||
MacrosList,
|
||||
},
|
||||
mixins: [alertMixin, uiSettingsMixin],
|
||||
props: {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<div>
|
||||
<p
|
||||
v-if="!uiFlags.isFetching && !macros.length"
|
||||
class="no-items-error-message"
|
||||
>
|
||||
{{ $t('MACROS.LIST.404') }}
|
||||
</p>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('MACROS.LOADING')"
|
||||
/>
|
||||
<div v-if="!uiFlags.isFetching && macros.length" class="macros-list">
|
||||
<macro-item
|
||||
v-for="macro in macros"
|
||||
:key="macro.id"
|
||||
:macro="macro"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MacroItem from './MacroItem.vue';
|
||||
export default {
|
||||
components: {
|
||||
MacroItem,
|
||||
},
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
macros: ['macros/getMacros'],
|
||||
uiFlags: 'macros/getUIFlags',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('macros/get');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.macros-list {
|
||||
padding: var(--space-smaller);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<div class="macro">
|
||||
<span class="text-truncate">{{ macro.name }}</span>
|
||||
<div class="macros-actions">
|
||||
<woot-button
|
||||
v-tooltip.left-start="$t('MACROS.EXECUTE.PREVIEW')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="info"
|
||||
class="mr-2"
|
||||
@click="toggleMacroPreview(macro)"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-start="$t('MACROS.EXECUTE.BUTTON_TOOLTIP')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="play-circle"
|
||||
:is-loading="isExecuting"
|
||||
@click="executeMacro(macro)"
|
||||
/>
|
||||
</div>
|
||||
<transition name="menu-slide">
|
||||
<macro-preview
|
||||
v-if="showPreview"
|
||||
v-on-clickaway="closeMacroPreview"
|
||||
:macro="macro"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import MacroPreview from './MacroPreview';
|
||||
export default {
|
||||
components: {
|
||||
MacroPreview,
|
||||
},
|
||||
mixins: [alertMixin, clickaway],
|
||||
props: {
|
||||
macro: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExecuting: false,
|
||||
showPreview: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async executeMacro(macro) {
|
||||
try {
|
||||
this.isExecuting = true;
|
||||
await this.$store.dispatch('macros/execute', {
|
||||
macroId: macro.id,
|
||||
conversationIds: [this.conversationId],
|
||||
});
|
||||
this.showAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESFULLY'));
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('MACROS.ERROR'));
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
},
|
||||
toggleMacroPreview() {
|
||||
this.showPreview = !this.showPreview;
|
||||
},
|
||||
closeMacroPreview() {
|
||||
this.showPreview = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.macro {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
padding: var(--space-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: var(--border-radius-normal);
|
||||
color: var(--s-700);
|
||||
|
||||
&:hover {
|
||||
background: var(--s-25);
|
||||
color: var(--s-600);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--w-300);
|
||||
}
|
||||
|
||||
.macros-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.mr-2 {
|
||||
margin-right: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<div class="macro-preview">
|
||||
<p class="macro-title">{{ macro.name }}</p>
|
||||
<div v-for="(action, i) in resolvedMacro" :key="i" class="macro-block">
|
||||
<div v-if="i !== macro.actions.length - 1" class="macro-block-border" />
|
||||
<div class="macro-block-dot" />
|
||||
<p class="macro-action-name">{{ action.actionName }}</p>
|
||||
<p class="macro-action-params">{{ action.actionValue }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
resolveActionName,
|
||||
resolveTeamIds,
|
||||
resolveLabels,
|
||||
} from 'dashboard/routes/dashboard/settings/macros/macroHelper.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
macro: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
resolvedMacro() {
|
||||
return this.macro.actions.map(action => {
|
||||
return {
|
||||
actionName: resolveActionName(action.action_name),
|
||||
actionValue: this.getActionValue(
|
||||
action.action_name,
|
||||
action.action_params
|
||||
),
|
||||
};
|
||||
});
|
||||
},
|
||||
...mapGetters({
|
||||
labels: 'labels/getLabels',
|
||||
teams: 'teams/getTeams',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
getActionValue(key, params) {
|
||||
switch (key) {
|
||||
case 'assign_team':
|
||||
return resolveTeamIds(this.teams, params);
|
||||
case 'add_label':
|
||||
return resolveLabels(this.labels, params);
|
||||
case 'mute_conversation':
|
||||
case 'snooze_conversation':
|
||||
case 'resolve_conversation':
|
||||
return null;
|
||||
case 'send_webhook_event':
|
||||
case 'send_message':
|
||||
case 'send_email_transcript':
|
||||
return params[0];
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.macro-preview {
|
||||
position: absolute;
|
||||
max-height: calc(var(--space-giga) * 1.5);
|
||||
min-height: var(--space-jumbo);
|
||||
width: calc(var(--space-giga) + var(--space-large));
|
||||
border-radius: var(--border-radius-normal);
|
||||
background-color: var(--white);
|
||||
box-shadow: var(--shadow-dropdown-pane);
|
||||
bottom: calc(var(--space-three) + var(--space-half));
|
||||
right: calc(var(--space-three) + var(--space-half));
|
||||
overflow-y: auto;
|
||||
padding: var(--space-slab);
|
||||
|
||||
.macro-title {
|
||||
margin-bottom: var(--space-slab);
|
||||
color: var(--s-900);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.macro-block {
|
||||
position: relative;
|
||||
padding-left: var(--space-slab);
|
||||
&:not(:last-child) {
|
||||
padding-bottom: var(--space-slab);
|
||||
}
|
||||
|
||||
.macro-block-border {
|
||||
top: 0.625rem;
|
||||
position: absolute;
|
||||
bottom: var(--space-minus-half);
|
||||
left: 0;
|
||||
width: 1px;
|
||||
background-color: var(--s-100);
|
||||
}
|
||||
|
||||
.macro-block-dot {
|
||||
position: absolute;
|
||||
left: -0.35rem;
|
||||
height: var(--space-small);
|
||||
width: var(--space-small);
|
||||
border: 2px solid var(--s-100);
|
||||
background-color: var(--white);
|
||||
border-radius: var(--border-radius-full);
|
||||
top: 0.4375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.macro-action-name {
|
||||
font-size: var(--font-size-mini);
|
||||
color: var(--s-500);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,52 @@
|
|||
export const MACRO_ACTION_TYPES = [
|
||||
{
|
||||
key: 'assign_team',
|
||||
label: 'Assign a team',
|
||||
inputType: 'multi_select',
|
||||
},
|
||||
{
|
||||
key: 'add_label',
|
||||
label: 'Add a label',
|
||||
inputType: 'multi_select',
|
||||
},
|
||||
// {
|
||||
// key: 'send_email_to_team',
|
||||
// label: 'Send an email to team',
|
||||
// inputType: 'team_message',
|
||||
// },
|
||||
{
|
||||
key: 'send_email_transcript',
|
||||
label: 'Send an email transcript',
|
||||
inputType: 'email',
|
||||
},
|
||||
{
|
||||
key: 'mute_conversation',
|
||||
label: 'Mute conversation',
|
||||
inputType: null,
|
||||
},
|
||||
{
|
||||
key: 'snooze_conversation',
|
||||
label: 'Snooze conversation',
|
||||
inputType: null,
|
||||
},
|
||||
{
|
||||
key: 'resolve_conversation',
|
||||
label: 'Resolve conversation',
|
||||
inputType: null,
|
||||
},
|
||||
// {
|
||||
// key: 'send_webhook_event',
|
||||
// label: 'Send Webhook Event',
|
||||
// inputType: 'url',
|
||||
// },
|
||||
{
|
||||
key: 'send_attachment',
|
||||
label: 'Send Attachment',
|
||||
inputType: 'attachment',
|
||||
},
|
||||
{
|
||||
key: 'send_message',
|
||||
label: 'Send a message',
|
||||
inputType: 'textarea',
|
||||
},
|
||||
];
|
|
@ -225,7 +225,7 @@ export default {
|
|||
mode === 'EDIT'
|
||||
? this.$t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE')
|
||||
: this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE');
|
||||
await await this.$store.dispatch(action, payload);
|
||||
await this.$store.dispatch(action, payload);
|
||||
this.showAlert(this.$t(successMessage));
|
||||
this.hideAddPopup();
|
||||
this.hideEditPopup();
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div>
|
||||
<button
|
||||
v-tooltip="tooltip"
|
||||
class="macros__action-button"
|
||||
:class="type"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<fluent-icon :icon="icon" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'add',
|
||||
},
|
||||
tooltip: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.macros__action-button {
|
||||
height: var(--space-three);
|
||||
width: var(--space-three);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-default);
|
||||
border-radius: var(--border-radius-rounded);
|
||||
position: relative;
|
||||
margin-left: var(--space-one);
|
||||
|
||||
&.add {
|
||||
background-color: var(--g-100);
|
||||
color: var(--g-600);
|
||||
}
|
||||
&.delete {
|
||||
position: absolute;
|
||||
top: calc(var(--space-three) / -2);
|
||||
right: calc(var(--space-three) / -2);
|
||||
background-color: var(--r-100);
|
||||
color: var(--r-600);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,11 +1,123 @@
|
|||
<template>
|
||||
<div>
|
||||
Macros
|
||||
<div class="column content-box">
|
||||
<router-link
|
||||
:to="addAccountScoping('settings/macros/new')"
|
||||
class="button success button--fixed-right-top"
|
||||
>
|
||||
<fluent-icon icon="add-circle" />
|
||||
<span class="button__content">
|
||||
{{ $t('MACROS.HEADER_BTN_TXT') }}
|
||||
</span>
|
||||
</router-link>
|
||||
<div class="row">
|
||||
<div class="small-8 columns with-right-space">
|
||||
<p
|
||||
v-if="!uiFlags.isFetching && !records.length"
|
||||
class="no-items-error-message"
|
||||
>
|
||||
{{ $t('MACROS.LIST.404') }}
|
||||
</p>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('MACROS.LOADING')"
|
||||
/>
|
||||
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in $t('MACROS.LIST.TABLE_HEADER')"
|
||||
:key="thHeader"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<macros-table-row
|
||||
v-for="(macro, index) in records"
|
||||
:key="index"
|
||||
:macro="macro"
|
||||
@delete="openDeletePopup(macro, index)"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="small-4 columns">
|
||||
<span v-dompurify-html="$t('MACROS.SIDEBAR_TXT')" />
|
||||
</div>
|
||||
</div>
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
|
||||
:message="$t('MACROS.DELETE.CONFIRM.MESSAGE')"
|
||||
:message-value="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import accountMixin from 'dashboard/mixins/account.js';
|
||||
import MacrosTableRow from './MacrosTableRow';
|
||||
export default {
|
||||
components: {
|
||||
MacrosTableRow,
|
||||
},
|
||||
mixins: [alertMixin, accountMixin],
|
||||
data() {
|
||||
return {
|
||||
showDeleteConfirmationPopup: false,
|
||||
selectedResponse: {},
|
||||
loading: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
records: ['macros/getMacros'],
|
||||
uiFlags: 'macros/getUIFlags',
|
||||
}),
|
||||
deleteConfirmText() {
|
||||
return `${this.$t('MACROS.DELETE.CONFIRM.YES')} ${
|
||||
this.selectedResponse.name
|
||||
}`;
|
||||
},
|
||||
deleteRejectText() {
|
||||
return `${this.$t('MACROS.DELETE.CONFIRM.NO')} ${
|
||||
this.selectedResponse.name
|
||||
}`;
|
||||
},
|
||||
deleteMessage() {
|
||||
return ` ${this.selectedResponse.name}?`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('macros/get');
|
||||
},
|
||||
methods: {
|
||||
openDeletePopup(response) {
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
this.selectedResponse = response;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.loading[this.selectedResponse.id] = true;
|
||||
this.closeDeletePopup();
|
||||
this.deleteMacro(this.selectedResponse.id);
|
||||
},
|
||||
async deleteMacro(id) {
|
||||
try {
|
||||
await this.$store.dispatch('macros/delete', id);
|
||||
this.showAlert(this.$t('MACROS.DELETE.API.SUCCESS_MESSAGE'));
|
||||
this.loading[this.selectedResponse.id] = false;
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('MACROS.DELETE.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
|
@ -1,9 +1,144 @@
|
|||
<template>
|
||||
<div>MacrosEditor</div>
|
||||
<div class="column content-box">
|
||||
<p v-if="!uiFlags.isFetching && !macro" class="no-items-error-message">
|
||||
{{ $t('MACROS.LIST.404') }}
|
||||
</p>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('MACROS.LOADING')"
|
||||
/>
|
||||
<macro-form v-if="macro" :macro-data.sync="macro" @submit="saveMacro" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
import MacroForm from './MacroForm';
|
||||
import { MACRO_ACTION_TYPES } from './constants';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { emptyMacro } from './macroHelper';
|
||||
import actionQueryGenerator from 'dashboard/helper/actionQueryGenerator.js';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import macrosMixin from './macrosMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MacroForm,
|
||||
},
|
||||
mixins: [alertMixin, macrosMixin],
|
||||
provide() {
|
||||
return {
|
||||
macroActionTypes: this.macroActionTypes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
macro: {},
|
||||
mode: 'CREATE',
|
||||
macroActionTypes: MACRO_ACTION_TYPES,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'macros/getUIFlags',
|
||||
labels: 'labels/getLabels',
|
||||
teams: 'teams/getTeams',
|
||||
}),
|
||||
macroId() {
|
||||
return this.$route.params.macroId;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler() {
|
||||
if (this.$route.params.macroId) {
|
||||
this.fetchMacro();
|
||||
} else {
|
||||
this.initNewMacro();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchMacro() {
|
||||
this.mode = 'EDIT';
|
||||
this.$store.dispatch('agents/get');
|
||||
this.$store.dispatch('teams/get');
|
||||
this.$store.dispatch('labels/get');
|
||||
this.manifestMacro();
|
||||
},
|
||||
async manifestMacro() {
|
||||
const isMacroAvailable = this.$store.getters['macros/getMacro'](
|
||||
this.macroId
|
||||
);
|
||||
if (isMacroAvailable) this.macro = this.formatMacro(isMacroAvailable);
|
||||
else {
|
||||
const unserializedMacro = await this.getSingleMacro();
|
||||
this.macro = this.formatMacro(unserializedMacro);
|
||||
}
|
||||
},
|
||||
async getSingleMacro() {
|
||||
return this.$store.dispatch('macros/getSingleMacro', this.macroId);
|
||||
},
|
||||
formatMacro(macro) {
|
||||
const formattedActions = macro.actions.map(action => {
|
||||
let actionParams = [];
|
||||
if (action.action_params.length) {
|
||||
const inputType = this.macroActionTypes.find(
|
||||
item => item.key === action.action_name
|
||||
).inputType;
|
||||
if (inputType === 'multi_select') {
|
||||
actionParams = [
|
||||
...this.getDropdownValues(action.action_name, this.$store),
|
||||
].filter(item => [...action.action_params].includes(item.id));
|
||||
} else if (inputType === 'team_message') {
|
||||
actionParams = {
|
||||
team_ids: [
|
||||
...this.getDropdownValues(action.action_name, this.$store),
|
||||
].filter(item =>
|
||||
[...action.action_params[0].team_ids].includes(item.id)
|
||||
),
|
||||
message: action.action_params[0].message,
|
||||
};
|
||||
} else actionParams = [...action.action_params];
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
action_params: actionParams,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...macro,
|
||||
actions: formattedActions,
|
||||
};
|
||||
},
|
||||
initNewMacro() {
|
||||
this.mode = 'CREATE';
|
||||
this.macro = emptyMacro;
|
||||
},
|
||||
async saveMacro(macro) {
|
||||
try {
|
||||
const action = this.mode === 'EDIT' ? 'macros/update' : 'macros/create';
|
||||
const successMessage =
|
||||
this.mode === 'EDIT'
|
||||
? this.$t('MACROS.EDIT.API.SUCCESS_MESSAGE')
|
||||
: this.$t('MACROS.ADD.API.SUCCESS_MESSAGE');
|
||||
let serializeMacro = JSON.parse(JSON.stringify(macro));
|
||||
serializeMacro.actions = actionQueryGenerator(serializeMacro.actions);
|
||||
await this.$store.dispatch(action, serializeMacro);
|
||||
this.showAlert(this.$t(successMessage));
|
||||
this.$router.push({ name: 'macros_wrapper' });
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('MACROS.ERROR'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style scoped>
|
||||
.content-box {
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="small-8 columns with-right-space macros-canvas">
|
||||
<macro-nodes
|
||||
v-model="macro.actions"
|
||||
@addNewNode="appendNode"
|
||||
@deleteNode="deleteNode"
|
||||
@resetAction="resetNode"
|
||||
/>
|
||||
</div>
|
||||
<div class="small-4 columns">
|
||||
<macro-properties
|
||||
:macro-name="macro.name"
|
||||
:macro-visibility="macro.visibility"
|
||||
@update:name="updateName"
|
||||
@update:visibility="updateVisibility"
|
||||
@submit="submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MacroNodes from './MacroNodes';
|
||||
import MacroProperties from './MacroProperties';
|
||||
import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MacroNodes,
|
||||
MacroProperties,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
$v: this.$v,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
macroData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
macro: this.macroData,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
macroData: {
|
||||
handler() {
|
||||
this.macro = this.macroData;
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
macro: {
|
||||
name: {
|
||||
required,
|
||||
},
|
||||
visibility: {
|
||||
required,
|
||||
},
|
||||
actions: {
|
||||
required,
|
||||
$each: {
|
||||
action_params: {
|
||||
required: requiredIf(prop => {
|
||||
if (prop.action_name === 'send_email_to_team') return true;
|
||||
return !(
|
||||
prop.action_name === 'mute_conversation' ||
|
||||
prop.action_name === 'snooze_conversation' ||
|
||||
prop.action_name === 'resolve_conversation'
|
||||
);
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$v.$reset();
|
||||
},
|
||||
methods: {
|
||||
updateName(value) {
|
||||
this.macro.name = value;
|
||||
},
|
||||
updateVisibility(value) {
|
||||
this.macro.visibility = value;
|
||||
},
|
||||
appendNode() {
|
||||
this.macro.actions.push({
|
||||
action_name: 'assign_team',
|
||||
action_params: [],
|
||||
});
|
||||
},
|
||||
deleteNode(index) {
|
||||
this.macro.actions.splice(index, 1);
|
||||
},
|
||||
submit() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) return;
|
||||
this.$emit('submit', this.macro);
|
||||
},
|
||||
resetNode(index) {
|
||||
this.macro.actions[index].action_params = [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.row {
|
||||
height: 100%;
|
||||
}
|
||||
.macros-canvas {
|
||||
background-image: radial-gradient(var(--s-100) 1.2px, transparent 0);
|
||||
background-size: var(--space-normal) var(--space-normal);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
padding: var(--space-normal) var(--space-three);
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,149 @@
|
|||
<template>
|
||||
<div class="macro__node-action-container">
|
||||
<fluent-icon
|
||||
v-if="!singleNode"
|
||||
size="20"
|
||||
icon="navigation"
|
||||
class="macros__node-drag-handle"
|
||||
/>
|
||||
<div
|
||||
class="macro__node-action-item"
|
||||
:class="{
|
||||
'has-error': hasError($v.macro.actions.$each[index]),
|
||||
}"
|
||||
>
|
||||
<action-input
|
||||
v-model="actionData"
|
||||
:action-types="macroActionTypes"
|
||||
:dropdown-values="dropdownValues()"
|
||||
:show-action-input="showActionInput"
|
||||
:show-remove-button="false"
|
||||
:is-macro="true"
|
||||
:v="$v.macro.actions.$each[index]"
|
||||
@resetAction="$emit('resetAction')"
|
||||
/>
|
||||
<macro-action-button
|
||||
v-if="!singleNode"
|
||||
icon="dismiss-circle"
|
||||
class="macro__node macro__node-action-button-delete"
|
||||
type="delete"
|
||||
:tooltip="$t('MACROS.EDITOR.DELETE_BTN_TOOLTIP')"
|
||||
@click="$emit('deleteNode')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActionInput from 'dashboard/components/widgets/AutomationActionInput';
|
||||
import MacroActionButton from './ActionButton.vue';
|
||||
import macrosMixin from './macrosMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActionInput,
|
||||
MacroActionButton,
|
||||
},
|
||||
mixins: [macrosMixin],
|
||||
inject: ['macroActionTypes', '$v'],
|
||||
props: {
|
||||
singleNode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
labels: 'labels/getLabels',
|
||||
teams: 'teams/getTeams',
|
||||
}),
|
||||
actionData: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value);
|
||||
},
|
||||
},
|
||||
showActionInput() {
|
||||
if (
|
||||
this.actionData.action_name === 'send_email_to_team' ||
|
||||
this.actionData.action_name === 'send_message'
|
||||
)
|
||||
return false;
|
||||
const type = this.macroActionTypes.find(
|
||||
action => action.key === this.actionData.action_name
|
||||
).inputType;
|
||||
return !!type;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dropdownValues() {
|
||||
return this.getDropdownValues(this.value.action_name, this.$store);
|
||||
},
|
||||
hasError(v) {
|
||||
return !!(v.action_params.$dirty && v.action_params.$error);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.macro__node-action-container {
|
||||
position: relative;
|
||||
.macros__node-drag-handle {
|
||||
position: absolute;
|
||||
left: var(--space-minus-medium);
|
||||
top: var(--space-smaller);
|
||||
cursor: move;
|
||||
color: var(--s-400);
|
||||
}
|
||||
.macro__node-action-item {
|
||||
background-color: var(--white);
|
||||
padding: var(--space-slab);
|
||||
border-radius: var(--border-radius-medium);
|
||||
box-shadow: rgb(0 0 0 / 3%) 0px 6px 24px 0px,
|
||||
rgb(0 0 0 / 6%) 0px 0px 0px 1px;
|
||||
|
||||
.macro__node-action-button-delete {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
.macro__node-action-button-delete {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
&.has-error {
|
||||
animation: shake 0.3s ease-in-out 0s 2;
|
||||
background-color: var(--r-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(0.375rem);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-0.375rem);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(0.375rem);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<div class="macros__nodes">
|
||||
<macros-pill :label="$t('MACROS.EDITOR.START_FLOW')" class="macro__node" />
|
||||
<draggable
|
||||
:list="actionData"
|
||||
animation="200"
|
||||
ghost-class="ghost"
|
||||
tag="div"
|
||||
class="macros__nodes-draggable"
|
||||
handle=".macros__node-drag-handle"
|
||||
@start="dragging = true"
|
||||
@end="dragging = false"
|
||||
>
|
||||
<div v-for="(action, i) in actionData" :key="i" class="macro__node">
|
||||
<macro-node
|
||||
v-model="actionData[i]"
|
||||
class="macros__node-action"
|
||||
type="add"
|
||||
:index="i"
|
||||
:single-node="actionData.length === 1"
|
||||
@resetAction="$emit('resetAction', i)"
|
||||
@deleteNode="$emit('deleteNode', i)"
|
||||
/>
|
||||
</div>
|
||||
</draggable>
|
||||
<macro-action-button
|
||||
icon="add-circle"
|
||||
class="macro__node"
|
||||
:tooltip="$t('MACROS.EDITOR.ADD_BTN_TOOLTIP')"
|
||||
type="add"
|
||||
@click="$emit('addNewNode')"
|
||||
/>
|
||||
<macros-pill :label="$t('MACROS.EDITOR.END_FLOW')" class="macro__node" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MacrosPill from './Pill.vue';
|
||||
import Draggable from 'vuedraggable';
|
||||
import MacroNode from './MacroNode.vue';
|
||||
import MacroActionButton from './ActionButton.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Draggable,
|
||||
MacrosPill,
|
||||
MacroNode,
|
||||
MacroActionButton,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragging: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
actionData: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.macros__nodes {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.macro__node:not(:last-child) {
|
||||
position: relative;
|
||||
padding-bottom: var(--space-three);
|
||||
}
|
||||
|
||||
.macro__node:not(:last-child):not(.sortable-chosen):after,
|
||||
.macros__nodes-draggable:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: var(--space-three);
|
||||
width: var(--space-smaller);
|
||||
margin-left: var(--space-medium);
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='4' height='30' viewBox='0 0 4 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cline x1='1.50098' y1='0.579529' x2='1.50098' y2='30.5795' stroke='%2393afc8' stroke-width='2' stroke-dasharray='5 5'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
.macros__nodes-draggable {
|
||||
position: relative;
|
||||
padding-bottom: var(--space-three);
|
||||
}
|
||||
|
||||
.macros__node-action-container {
|
||||
position: relative;
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
left: var(--space-minus-medium);
|
||||
top: var(--space-smaller);
|
||||
cursor: move;
|
||||
color: var(--s-400);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,170 @@
|
|||
<template>
|
||||
<div class="macros__properties-panel">
|
||||
<div>
|
||||
<woot-input
|
||||
:value="macroName"
|
||||
:label="$t('MACROS.ADD.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('MACROS.ADD.FORM.NAME.PLACEHOLDER')"
|
||||
:error="$v.macro.name.$error ? $t('MACROS.ADD.FORM.NAME.ERROR') : null"
|
||||
:class="{ error: $v.macro.name.$error }"
|
||||
@input="onUpdateName($event)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="title">{{ $t('MACROS.EDITOR.VISIBILITY.LABEL') }}</p>
|
||||
<div class="macros__form-visibility">
|
||||
<button
|
||||
class="card"
|
||||
:class="isActive('global')"
|
||||
@click="onUpdateVisibility('global')"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="macroVisibility === 'global'"
|
||||
icon="checkmark-circle"
|
||||
type="solid"
|
||||
class="visibility-check"
|
||||
/>
|
||||
<p class="title">
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.GLOBAL.LABEL') }}
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.GLOBAL.DESCRIPTION') }}
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
class="card"
|
||||
:class="isActive('personal')"
|
||||
@click="onUpdateVisibility('personal')"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="macroVisibility === 'personal'"
|
||||
icon="checkmark-circle"
|
||||
type="solid"
|
||||
class="visibility-check"
|
||||
/>
|
||||
<p class="title">
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.PERSONAL.LABEL') }}
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.PERSONAL.DESCRIPTION') }}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
<div class="macros__info-panel">
|
||||
<fluent-icon icon="info" size="20" />
|
||||
<p>
|
||||
{{ $t('MACROS.ORDER_INFO') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="macros__submit-button">
|
||||
<woot-button
|
||||
size="expanded"
|
||||
color-scheme="success"
|
||||
@click="$emit('submit')"
|
||||
>
|
||||
{{ $t('MACROS.HEADER_BTN_TXT_SAVE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['$v'],
|
||||
props: {
|
||||
macroName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
macroVisibility: {
|
||||
type: String,
|
||||
default: 'global',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isActive(key) {
|
||||
return { active: this.macroVisibility === key };
|
||||
},
|
||||
onUpdateName(value) {
|
||||
this.$emit('update:name', value);
|
||||
},
|
||||
onUpdateVisibility(value) {
|
||||
this.$emit('update:visibility', value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.macros__properties-panel {
|
||||
padding: var(--space-slab);
|
||||
background-color: var(--white);
|
||||
// full screen height subtracted by the height of the header
|
||||
height: calc(100vh - 5.6rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid var(--s-50);
|
||||
}
|
||||
|
||||
.macros__submit-button {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.macros__form-visibility {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-slab);
|
||||
|
||||
.card {
|
||||
padding: var(--space-small);
|
||||
border-radius: var(--border-radius-normal);
|
||||
border: 1px solid var(--s-200);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
background-color: var(--w-25);
|
||||
border: 1px solid var(--w-300);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: var(--font-size-mini);
|
||||
color: var(--s-500);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.8;
|
||||
color: var(--color-body);
|
||||
}
|
||||
|
||||
.visibility-check {
|
||||
position: absolute;
|
||||
color: var(--w-500);
|
||||
top: var(--space-small);
|
||||
right: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.macros__info-panel {
|
||||
margin-top: var(--space-small);
|
||||
display: flex;
|
||||
background-color: var(--s-50);
|
||||
padding: var(--space-small);
|
||||
border-radius: var(--border-radius-normal);
|
||||
align-items: flex-start;
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
p {
|
||||
margin-left: var(--space-small);
|
||||
color: var(--s-600);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<tr>
|
||||
<td>{{ macro.name }}</td>
|
||||
<td>
|
||||
<div class="avatar-container">
|
||||
<thumbnail :username="macro.created_by.name" size="24px" />
|
||||
<span class="ml-2">{{ macro.created_by.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="avatar-container">
|
||||
<thumbnail :username="macro.updated_by.name" size="24px" />
|
||||
<span class="ml-2">{{ macro.updated_by.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ visibilityLabel }}</td>
|
||||
<td class="button-wrapper">
|
||||
<router-link :to="addAccountScoping(`settings/macros/${macro.id}/edit`)">
|
||||
<woot-button
|
||||
v-tooltip.top="$t('MACROS.FORM.EDIT')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="grey-btn"
|
||||
icon="edit"
|
||||
/>
|
||||
</router-link>
|
||||
<woot-button
|
||||
v-tooltip.top="$t('MACROS.FORM.DELETE')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
class-names="grey-btn"
|
||||
@click="$emit('delete')"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||
import accountMixin from 'dashboard/mixins/account.js';
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [accountMixin],
|
||||
props: {
|
||||
macro: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
visibilityLabel() {
|
||||
return this.macro.visibility === 'global'
|
||||
? this.$t('MACROS.EDITOR.VISIBILITY.GLOBAL.LABEL')
|
||||
: this.$t('MACROS.EDITOR.VISIBILITY.PERSONAL.LABEL');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin-left: var(--space-one);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="macros-item macros-pill">
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.macros-pill {
|
||||
padding: var(--space-slab);
|
||||
background-color: var(--w-500);
|
||||
max-width: max-content;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-small);
|
||||
border-radius: var(--border-radius-full);
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,42 @@
|
|||
export const MACRO_ACTION_TYPES = [
|
||||
{
|
||||
key: 'assign_team',
|
||||
label: 'Assign a team',
|
||||
inputType: 'multi_select',
|
||||
},
|
||||
{
|
||||
key: 'add_label',
|
||||
label: 'Add a label',
|
||||
inputType: 'multi_select',
|
||||
},
|
||||
{
|
||||
key: 'send_email_transcript',
|
||||
label: 'Send an email transcript',
|
||||
inputType: 'email',
|
||||
},
|
||||
{
|
||||
key: 'mute_conversation',
|
||||
label: 'Mute conversation',
|
||||
inputType: null,
|
||||
},
|
||||
{
|
||||
key: 'snooze_conversation',
|
||||
label: 'Snooze conversation',
|
||||
inputType: null,
|
||||
},
|
||||
{
|
||||
key: 'resolve_conversation',
|
||||
label: 'Resolve conversation',
|
||||
inputType: null,
|
||||
},
|
||||
{
|
||||
key: 'send_attachment',
|
||||
label: 'Send Attachment',
|
||||
inputType: 'attachment',
|
||||
},
|
||||
{
|
||||
key: 'send_message',
|
||||
label: 'Send a message',
|
||||
inputType: 'textarea',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,34 @@
|
|||
import { MACRO_ACTION_TYPES as macroActionTypes } from './constants';
|
||||
|
||||
export const resolveActionName = key => {
|
||||
return macroActionTypes.find(i => i.key === key).label;
|
||||
};
|
||||
|
||||
export const resolveTeamIds = (teams, ids) => {
|
||||
return ids
|
||||
.map(id => {
|
||||
const team = teams.find(i => i.id === id);
|
||||
return team ? team.name : '';
|
||||
})
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
export const resolveLabels = (labels, ids) => {
|
||||
return ids
|
||||
.map(id => {
|
||||
const label = labels.find(i => i.title === id);
|
||||
return label ? label.title : '';
|
||||
})
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
export const emptyMacro = {
|
||||
name: '',
|
||||
actions: [
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [],
|
||||
},
|
||||
],
|
||||
visibility: 'global',
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
export default {
|
||||
methods: {
|
||||
getDropdownValues(type) {
|
||||
switch (type) {
|
||||
case 'assign_team':
|
||||
case 'send_email_to_team':
|
||||
return this.teams;
|
||||
case 'add_label':
|
||||
return this.labels.map(i => {
|
||||
return {
|
||||
id: i.title,
|
||||
name: i.title,
|
||||
};
|
||||
});
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
|
@ -14,8 +14,8 @@ import labels from './labels/labels.routes';
|
|||
import macros from './macros/macros.routes';
|
||||
import profile from './profile/profile.routes';
|
||||
import reports from './reports/reports.routes';
|
||||
import store from '../../../store';
|
||||
import teams from './teams/teams.routes';
|
||||
import store from '../../../store';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
|
@ -46,5 +46,6 @@ export default {
|
|||
...profile.routes,
|
||||
...reports.routes,
|
||||
...teams.routes,
|
||||
...macros.routes,
|
||||
],
|
||||
};
|
||||
|
|
|
@ -2,14 +2,16 @@ import Vue from 'vue';
|
|||
import Vuex from 'vuex';
|
||||
|
||||
import accounts from './modules/accounts';
|
||||
import agents from './modules/agents';
|
||||
import agentBots from './modules/agentBots';
|
||||
import agents from './modules/agents';
|
||||
import articles from './modules/helpCenterArticles';
|
||||
import attributes from './modules/attributes';
|
||||
import auth from './modules/auth';
|
||||
import automations from './modules/automations';
|
||||
import bulkActions from './modules/bulkActions';
|
||||
import campaigns from './modules/campaigns';
|
||||
import cannedResponse from './modules/cannedResponse';
|
||||
import categories from './modules/helpCenterCategories';
|
||||
import contactConversations from './modules/contactConversations';
|
||||
import contactLabels from './modules/contactLabels';
|
||||
import contactNotes from './modules/contactNotes';
|
||||
|
@ -30,28 +32,29 @@ import inboxes from './modules/inboxes';
|
|||
import inboxMembers from './modules/inboxMembers';
|
||||
import integrations from './modules/integrations';
|
||||
import labels from './modules/labels';
|
||||
import macros from './modules/macros';
|
||||
import notifications from './modules/notifications';
|
||||
import portals from './modules/helpCenterPortals';
|
||||
import reports from './modules/reports';
|
||||
import teamMembers from './modules/teamMembers';
|
||||
import teams from './modules/teams';
|
||||
import userNotificationSettings from './modules/userNotificationSettings';
|
||||
import webhooks from './modules/webhooks';
|
||||
import articles from './modules/helpCenterArticles';
|
||||
import portals from './modules/helpCenterPortals';
|
||||
import categories from './modules/helpCenterCategories';
|
||||
|
||||
Vue.use(Vuex);
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
accounts,
|
||||
agents,
|
||||
agentBots,
|
||||
agents,
|
||||
articles,
|
||||
attributes,
|
||||
auth,
|
||||
automations,
|
||||
bulkActions,
|
||||
campaigns,
|
||||
cannedResponse,
|
||||
categories,
|
||||
contactConversations,
|
||||
contactLabels,
|
||||
contactNotes,
|
||||
|
@ -72,14 +75,13 @@ export default new Vuex.Store({
|
|||
inboxMembers,
|
||||
integrations,
|
||||
labels,
|
||||
macros,
|
||||
notifications,
|
||||
portals,
|
||||
reports,
|
||||
teamMembers,
|
||||
teams,
|
||||
userNotificationSettings,
|
||||
webhooks,
|
||||
articles,
|
||||
portals,
|
||||
categories,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
:root {
|
||||
// border-radius
|
||||
--border-radius-small: 0.3rem;
|
||||
--border-radius-normal: 0.5rem;
|
||||
--border-radius-medium: 0.7rem;
|
||||
--border-radius-large: 0.9rem;
|
||||
--border-radius-full: 10rem;
|
||||
--border-radius-rounded: 50%;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
--space-minus-micro: -0.2rem;
|
||||
--space-minus-smaller: -0.4rem;
|
||||
--space-minus-half: -0.5rem;
|
||||
--space-minus-small: -0.8rem;
|
||||
--space-minus-one: -1rem;
|
||||
--space-minus-slab: -1.2rem;
|
||||
|
|
|
@ -79,6 +79,7 @@
|
|||
"error-circle-outline": "M12 2c5.523 0 10 4.478 10 10s-4.477 10-10 10S2 17.522 2 12 6.477 2 12 2Zm0 1.667c-4.595 0-8.333 3.738-8.333 8.333 0 4.595 3.738 8.333 8.333 8.333 4.595 0 8.333-3.738 8.333-8.333 0-4.595-3.738-8.333-8.333-8.333Zm-.001 10.835a.999.999 0 1 1 0 1.998.999.999 0 0 1 0-1.998ZM11.994 7a.75.75 0 0 1 .744.648l.007.101.004 4.502a.75.75 0 0 1-1.493.103l-.007-.102-.004-4.501a.75.75 0 0 1 .75-.751Z",
|
||||
"filter-outline": "M13.5 16a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h3Zm3-5a.75.75 0 0 1 0 1.5h-9a.75.75 0 0 1 0-1.5h9Zm3-5a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1 0-1.5h15Z",
|
||||
"file-upload-outline": "M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6Zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7ZM5.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9Zm2.354-4.854a.5.5 0 1 1-.708.708L6 13.707V16.5a.5.5 0 0 1-1 0v-2.793l-1.146 1.147a.5.5 0 1 1-.708-.707l2-2A.5.5 0 0 1 5.497 12h.006a.498.498 0 0 1 .348.144l.003.003l2 2Z",
|
||||
"flash-settings-outline": "M6.19 2.77c.13-.455.547-.77 1.02-.77h5.25c.724 0 1.236.71 1.007 1.398l-.002.008L12.204 7h2.564c.946 0 1.407 1.144.766 1.811l-.003.004l-.237.242a5.545 5.545 0 0 0-1.374-.027l.894-.912a.056.056 0 0 0 .017-.032a.084.084 0 0 0-.007-.044a.079.079 0 0 0-.025-.034c-.005-.004-.013-.008-.031-.008h-3.27a.5.5 0 0 1-.471-.666L12.52 3.08a.062.062 0 0 0-.06-.08H7.211a.062.062 0 0 0-.06.045l-2.25 7.874c-.01.04.019.08.06.08H6.87a.5.5 0 0 1 .485.62l-1.325 5.3a.086.086 0 0 0-.003.03a.02.02 0 0 0 .003.011a.08.08 0 0 0 .072.04a.03.03 0 0 0 .01-.004a.087.087 0 0 0 .024-.018l.003-.004l2.882-2.94a5.573 5.573 0 0 0 .054 1.372l-2.22 2.267c-.754.782-2.059.06-1.795-.996l1.17-4.679H4.96a1.062 1.062 0 0 1-1.021-1.354l2.25-7.873Zm5.877 8.673a2 2 0 0 1-1.431 2.478l-.461.118a4.702 4.702 0 0 0 .01 1.016l.35.083a2 2 0 0 1 1.456 2.519l-.127.422c.257.204.537.378.835.518l.325-.344a2 2 0 0 1 2.91.002l.337.358c.292-.135.568-.302.822-.498l-.157-.556a2 2 0 0 1 1.431-2.479l.46-.117a4.702 4.702 0 0 0-.01-1.017l-.348-.082a2 2 0 0 1-1.456-2.52l.126-.421a4.32 4.32 0 0 0-.835-.519l-.325.344a2 2 0 0 1-2.91-.001l-.337-.358a4.316 4.316 0 0 0-.821.497l.156.557ZM14.5 15.5a1 1 0 1 1 0-2a1 1 0 0 1 0 2Z",
|
||||
"flash-on-outline": "m8.294 14-1.767 7.068c-.187.746.736 1.256 1.269.701L19.79 9.27A.75.75 0 0 0 19.25 8h-4.46l1.672-5.013A.75.75 0 0 0 15.75 2h-7a.75.75 0 0 0-.721.544l-3 10.5A.75.75 0 0 0 5.75 14h2.544Zm4.745-5.487a.75.75 0 0 0 .711.987h3.74l-8.824 9.196 1.316-5.264a.75.75 0 0 0-.727-.932h-2.51l2.57-9h5.394l-1.67 5.013Z",
|
||||
"flash-settings-outline": "M6.19 2.77c.13-.455.547-.77 1.02-.77h5.25c.724 0 1.236.71 1.007 1.398l-.002.008L12.204 7h2.564c.946 0 1.407 1.144.766 1.811l-.003.004l-.237.242a5.545 5.545 0 0 0-1.374-.027l.894-.912a.056.056 0 0 0 .017-.032a.084.084 0 0 0-.007-.044a.079.079 0 0 0-.025-.034c-.005-.004-.013-.008-.031-.008h-3.27a.5.5 0 0 1-.471-.666L12.52 3.08a.062.062 0 0 0-.06-.08H7.211a.062.062 0 0 0-.06.045l-2.25 7.874c-.01.04.019.08.06.08H6.87a.5.5 0 0 1 .485.62l-1.325 5.3a.086.086 0 0 0-.003.03a.02.02 0 0 0 .003.011a.08.08 0 0 0 .072.04a.03.03 0 0 0 .01-.004a.087.087 0 0 0 .024-.018l.003-.004l2.882-2.94a5.573 5.573 0 0 0 .054 1.372l-2.22 2.267c-.754.782-2.059.06-1.795-.996l1.17-4.679H4.96a1.062 1.062 0 0 1-1.021-1.354l2.25-7.873Zm5.877 8.673a2 2 0 0 1-1.431 2.478l-.461.118a4.702 4.702 0 0 0 .01 1.016l.35.083a2 2 0 0 1 1.456 2.519l-.127.422c.257.204.537.378.835.518l.325-.344a2 2 0 0 1 2.91.002l.337.358c.292-.135.568-.302.822-.498l-.157-.556a2 2 0 0 1 1.431-2.479l.46-.117a4.702 4.702 0 0 0-.01-1.017l-.348-.082a2 2 0 0 1-1.456-2.52l.126-.421a4.32 4.32 0 0 0-.835-.519l-.325.344a2 2 0 0 1-2.91-.001l-.337-.358a4.316 4.316 0 0 0-.821.497l.156.557ZM14.5 15.5a1 1 0 1 1 0-2a1 1 0 0 1 0 2Z",
|
||||
"folder-outline": "M8.207 4c.46 0 .908.141 1.284.402l.156.12L12.022 6.5h7.728a2.25 2.25 0 0 1 2.229 1.938l.016.158.005.154v9a2.25 2.25 0 0 1-2.096 2.245L19.75 20H4.25a2.25 2.25 0 0 1-2.245-2.096L2 17.75V6.25a2.25 2.25 0 0 1 2.096-2.245L4.25 4h3.957Zm1.44 5.979a2.25 2.25 0 0 1-1.244.512l-.196.009-4.707-.001v7.251c0 .38.282.694.648.743l.102.007h15.5a.75.75 0 0 0 .743-.648l.007-.102v-9a.75.75 0 0 0-.648-.743L19.75 8h-7.729L9.647 9.979ZM8.207 5.5H4.25a.75.75 0 0 0-.743.648L3.5 6.25v2.749L8.207 9a.75.75 0 0 0 .395-.113l.085-.06 1.891-1.578-1.89-1.575a.75.75 0 0 0-.377-.167L8.207 5.5Z",
|
||||
|
@ -106,6 +107,7 @@
|
|||
"microphone-stop-outline": "M18,18H6V6H18V18Z",
|
||||
"microphone-pause-outline": "M14,19H18V5H14M6,19H10V5H6V19Z",
|
||||
"microphone-play-outline": "M8,5.14V19.14L19,12.14L8,5.14Z",
|
||||
"navigation-outline": "M3 17h18a1 1 0 0 1 .117 1.993L21 19H3a1 1 0 0 1-.117-1.993L3 17h18H3Zm0-6l18-.002a1 1 0 0 1 .117 1.993l-.117.007L3 13a1 1 0 0 1-.117-1.993L3 11l18-.002L3 11Zm0-6h18a1 1 0 0 1 .117 1.993L21 7H3a1 1 0 0 1-.117-1.993L3 5h18H3Z",
|
||||
"number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z",
|
||||
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
||||
"panel-sidebar-outline": "M4.75 4A2.75 2.75 0 0 0 2 6.75v10.5A2.75 2.75 0 0 0 4.75 20h14.5A2.75 2.75 0 0 0 22 17.25V6.75A2.75 2.75 0 0 0 19.25 4H4.75ZM9 18.5v-13h10.25c.69 0 1.25.56 1.25 1.25v10.5c0 .69-.56 1.25-1.25 1.25H9ZM5.5 3.5h4M5.5 5.5h4M5.5 7.5h4M5.5 9.5h4",
|
||||
|
@ -117,6 +119,7 @@
|
|||
"person-add-outline": "M17.5 12a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11zm-5.477 2a6.47 6.47 0 0 0-.709 1.5H4.253a.749.749 0 0 0-.75.75v.577c0 .535.192 1.053.54 1.46c1.253 1.469 3.22 2.214 5.957 2.214c.597 0 1.157-.035 1.68-.106c.246.495.553.954.912 1.367c-.795.16-1.66.24-2.592.24c-3.146 0-5.532-.906-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.578A2.249 2.249 0 0 1 4.253 14h7.77zm5.477 0l-.09.008a.5.5 0 0 0-.402.402L17 14.5V17h-2.496l-.09.008a.5.5 0 0 0-.402.402l-.008.09l.008.09a.5.5 0 0 0 .402.402l.09.008H17L17 20.5l.008.09a.5.5 0 0 0 .402.402l.09.008l.09-.008a.5.5 0 0 0 .402-.402L18 20.5V18h2.504l.09-.008a.5.5 0 0 0 .402-.402l.008-.09l-.008-.09a.5.5 0 0 0-.402-.402l-.09-.008H18L18 14.5l-.008-.09a.5.5 0 0 0-.402-.402L17.5 14zM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z",
|
||||
"person-assign-outline": "M11.313 15.5a6.471 6.471 0 0 1 .709-1.5h-7.77a2.249 2.249 0 0 0-2.249 2.25v.577c0 .892.319 1.756.899 2.435c1.566 1.834 3.952 2.74 7.098 2.74c.931 0 1.796-.08 2.592-.24a6.51 6.51 0 0 1-.913-1.366c-.524.07-1.083.105-1.68.105c-2.737 0-4.703-.745-5.957-2.213a2.25 2.25 0 0 1-.539-1.461v-.578a.75.75 0 0 1 .75-.749h7.06ZM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7ZM23 17.5a5.5 5.5 0 1 1-11 0a5.5 5.5 0 0 1 11 0Zm-4.647-2.853a.5.5 0 0 0-.707.707L19.293 17H15a.5.5 0 1 0 0 1h4.293l-1.647 1.647a.5.5 0 0 0 .707.707l2.5-2.5a.497.497 0 0 0 .147-.345V17.5a.498.498 0 0 0-.15-.357l-2.497-2.496Z",
|
||||
"person-outline": "M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438-1.57 1.834-3.957 2.739-7.102 2.739-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501Zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461 1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75ZM12 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z",
|
||||
"play-circle-outline": "M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12Zm8.856-3.845A1.25 1.25 0 0 0 9 9.248v5.504a1.25 1.25 0 0 0 1.856 1.093l5.757-3.189a.75.75 0 0 0 0-1.312l-5.757-3.189Z",
|
||||
"power-outline": "M8.204 4.82a.75.75 0 0 1 .634 1.36A7.51 7.51 0 0 0 4.5 12.991c0 4.148 3.358 7.51 7.499 7.51s7.499-3.362 7.499-7.51a7.51 7.51 0 0 0-4.323-6.804.75.75 0 1 1 .637-1.358 9.01 9.01 0 0 1 5.186 8.162c0 4.976-4.029 9.01-9 9.01C7.029 22 3 17.966 3 12.99a9.01 9.01 0 0 1 5.204-8.17ZM12 2.496a.75.75 0 0 1 .743.648l.007.102v7.5a.75.75 0 0 1-1.493.102l-.007-.102v-7.5a.75.75 0 0 1 .75-.75Z",
|
||||
"quote-outline": "M7.5 6a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.555-1.24 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.64-1.737 2.66-3.674 3.077-5.859A2.5 2.5 0 1 1 7.5 6Zm9 0a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.56-1.238 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.643-1.738 2.662-3.672 3.078-5.859A2.5 2.5 0 1 1 16.5 6Zm-9 1.5a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Zm9 0a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Z",
|
||||
"resize-large-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5A3.25 3.25 0 0 1 6.25 3h1.5a.75.75 0 0 1 0 1.5h-1.5ZM19.5 6.25a1.75 1.75 0 0 0-1.75-1.75h-1.5a.75.75 0 0 1 0-1.5h1.5A3.25 3.25 0 0 1 21 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5ZM19.5 17.75a1.75 1.75 0 0 1-1.75 1.75h-1.5a.75.75 0 0 0 0 1.5h1.5A3.25 3.25 0 0 0 21 17.75v-1.5a.75.75 0 0 0-1.5 0v1.5ZM4.5 17.75c0 .966.784 1.75 1.75 1.75h1.5a.75.75 0 0 1 0 1.5h-1.5A3.25 3.25 0 0 1 3 17.75v-1.5a.75.75 0 0 1 1.5 0v1.5ZM8.25 6A2.25 2.25 0 0 0 6 8.25v7.5A2.25 2.25 0 0 0 8.25 18h7.5A2.25 2.25 0 0 0 18 15.75v-7.5A2.25 2.25 0 0 0 15.75 6h-7.5ZM7.5 8.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-.75.75h-7.5a.75.75 0 0 1-.75-.75v-7.5Z",
|
||||
|
|
|
@ -19,17 +19,21 @@
|
|||
# index_macros_on_updated_by_id (updated_by_id)
|
||||
#
|
||||
class Macro < ApplicationRecord
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :created_by,
|
||||
class_name: :User
|
||||
belongs_to :updated_by,
|
||||
class_name: :User
|
||||
has_many_attached :files
|
||||
|
||||
enum visibility: { personal: 0, global: 1 }
|
||||
|
||||
validate :json_actions_format
|
||||
|
||||
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agent send_webhook_event mute_conversation change_status
|
||||
resolve_conversation snooze_conversation].freeze
|
||||
ACTIONS_ATTRS = %w[send_message add_label assign_team assign_best_agent mute_conversation change_status
|
||||
resolve_conversation snooze_conversation send_email_transcript send_attachment].freeze
|
||||
|
||||
def set_visibility(user, params)
|
||||
self.visibility = params[:visibility]
|
||||
|
@ -47,6 +51,20 @@ class Macro < ApplicationRecord
|
|||
params[:page] || 1
|
||||
end
|
||||
|
||||
def file_base_data
|
||||
files.map do |file|
|
||||
{
|
||||
id: file.id,
|
||||
macro_id: id,
|
||||
file_type: file.content_type,
|
||||
account_id: account_id,
|
||||
file_url: url_for(file),
|
||||
blob_id: file.blob_id,
|
||||
filename: file.filename.to_s
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def json_actions_format
|
||||
|
|
|
@ -22,4 +22,8 @@ class MacroPolicy < ApplicationPolicy
|
|||
def execute?
|
||||
true
|
||||
end
|
||||
|
||||
def attach_file?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -39,6 +39,12 @@ class ActionService
|
|||
@conversation.update!(team_id: team_ids[0])
|
||||
end
|
||||
|
||||
def send_email_transcript(emails)
|
||||
emails.each do |email|
|
||||
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, email)&.deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def agent_belongs_to_account?(agent_ids)
|
||||
|
|
|
@ -26,21 +26,15 @@ class AutomationRules::ActionService < ActionService
|
|||
|
||||
return unless @rule.files.attached?
|
||||
|
||||
blob = ActiveStorage::Blob.find(blob_ids)
|
||||
blobs = ActiveStorage::Blob.where(id: blob_ids)
|
||||
|
||||
return if blob.blank?
|
||||
return if blobs.blank?
|
||||
|
||||
params = { content: nil, private: false, attachments: blob }
|
||||
params = { content: nil, private: false, attachments: blobs }
|
||||
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
||||
mb.perform
|
||||
end
|
||||
|
||||
def send_email_transcript(emails)
|
||||
emails.each do |email|
|
||||
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, email)&.deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
def send_webhook_event(webhook_url)
|
||||
payload = @conversation.webhook_data.merge(event: "automation_event.#{@rule.event_name}")
|
||||
WebhookJob.perform_later(webhook_url[0], payload)
|
||||
|
|
|
@ -21,18 +21,25 @@ class Macros::ExecutionService < ActionService
|
|||
|
||||
private
|
||||
|
||||
def send_webhook_event(webhook_url)
|
||||
payload = @conversation.webhook_data.merge(event: "macro_event.#{@macro.name}")
|
||||
WebhookJob.perform_later(webhook_url[0], payload)
|
||||
end
|
||||
|
||||
def send_message(message)
|
||||
return if conversation_a_tweet?
|
||||
|
||||
params = { content: message[0], private: false, content_attributes: { macro_id: @macro.id } }
|
||||
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
||||
params = { content: message[0], private: false }
|
||||
mb = Messages::MessageBuilder.new(nil, @conversation.reload, params)
|
||||
mb.perform
|
||||
end
|
||||
|
||||
def send_email_to_team(_params); end
|
||||
def send_attachment(blob_ids)
|
||||
return if conversation_a_tweet?
|
||||
|
||||
return unless @macro.files.attached?
|
||||
|
||||
blobs = ActiveStorage::Blob.where(id: blob_ids)
|
||||
|
||||
return if blobs.blank?
|
||||
|
||||
params = { content: nil, private: false, attachments: blobs }
|
||||
mb = Messages::MessageBuilder.new(nil, @conversation.reload, params)
|
||||
mb.perform
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,3 +16,4 @@ end
|
|||
|
||||
json.account_id macro.account_id
|
||||
json.actions macro.actions
|
||||
json.files macro.file_base_data if macro.files.any?
|
||||
|
|
|
@ -58,9 +58,8 @@ Rails.application.routes.draw do
|
|||
post :attach_file, on: :collection
|
||||
end
|
||||
resources :macros, only: [:index, :create, :show, :update, :destroy] do
|
||||
member do
|
||||
post :execute
|
||||
end
|
||||
post :execute, on: :member
|
||||
post :attach_file, on: :collection
|
||||
end
|
||||
resources :campaigns, only: [:index, :create, :show, :update, :destroy]
|
||||
resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy]
|
||||
|
|
|
@ -117,6 +117,40 @@ RSpec.describe 'Api::V1::Accounts::MacrosController', type: :request do
|
|||
expect(json_response['payload']['visibility']).to eql('personal')
|
||||
expect(json_response['payload']['created_by']['id']).to eql(agent.id)
|
||||
end
|
||||
|
||||
it 'Saves file in the macros actions to send an attachments' do
|
||||
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/macros/attach_file",
|
||||
headers: administrator.create_new_auth_token,
|
||||
params: { attachment: file }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
blob = JSON.parse(response.body)
|
||||
|
||||
expect(blob['blob_key']).to be_present
|
||||
expect(blob['blob_id']).to be_present
|
||||
|
||||
params[:actions] = [
|
||||
{
|
||||
'action_name': :send_message,
|
||||
'action_params': ['Welcome to the chatwoot platform.']
|
||||
},
|
||||
{
|
||||
'action_name': :send_attachment,
|
||||
'action_params': [blob['blob_id']]
|
||||
}
|
||||
]
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/macros",
|
||||
headers: administrator.create_new_auth_token,
|
||||
params: params
|
||||
|
||||
macro = account.macros.last
|
||||
expect(macro.files.presence).to be_truthy
|
||||
expect(macro.files.count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -196,12 +230,6 @@ RSpec.describe 'Api::V1::Accounts::MacrosController', type: :request do
|
|||
create(:account_user, user: user_1, account: account)
|
||||
macro.update!(actions:
|
||||
[
|
||||
{
|
||||
'action_name' => 'send_email_to_team', 'action_params' => [{
|
||||
'message' => 'Please pay attention to this conversation, its from high priority customer',
|
||||
'team_ids' => [team.id]
|
||||
}]
|
||||
},
|
||||
{ 'action_name' => 'assign_team', 'action_params' => [team.id] },
|
||||
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
|
||||
{ 'action_name' => 'snooze_conversation' },
|
||||
|
|
Loading…
Reference in a new issue