fix: Add Attachment endpoint to save file against automation rule (#4480)

Co-authored-by: fayazara <fayazara@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Tejaswini Chile 2022-04-24 12:02:40 +05:30 committed by GitHub
parent 2acb48bbe0
commit 1b3011b27b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 291 additions and 57 deletions

View file

@ -17,6 +17,16 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
@automation_rule
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 show; end
def update
@ -25,6 +35,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
@automation_rule.actions = params[:actions] if params[:actions]
@automation_rule.save!
process_attachments
rescue StandardError => e
Rails.logger.error e
render json: { error: @automation_rule.errors.messages }.to_json, status: :unprocessable_entity
@ -43,17 +54,19 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
@automation_rule = new_rule
end
private
def process_attachments
return if params[:attachments].blank?
actions = @automation_rule.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
return if actions.blank?
params[:attachments].each do |uploaded_attachment|
@automation_rule.files.attach(uploaded_attachment)
actions.each do |action|
blob_id = action['action_params']
blob = ActiveStorage::Blob.find_by(id: blob_id)
@automation_rule.files.attach(blob)
end
@automation_rule
end
private
def automation_rules_permit
params.permit(
:name, :description, :event_name, :account_id, :active,

View file

@ -9,6 +9,14 @@ class AutomationsAPI extends ApiClient {
clone(automationId) {
return axios.post(`${this.url}/${automationId}/clone`);
}
attachment(file) {
return axios.post(`${this.url}/attach_file`, file, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
}
export default new AutomationsAPI();

View file

@ -52,6 +52,11 @@
class="answer--text-input"
placeholder="Enter url"
/>
<automation-action-file-input
v-if="inputType === 'attachment'"
v-model="action_params"
:initial-file-name="initialFileName"
/>
</div>
</div>
<woot-button
@ -84,9 +89,11 @@
<script>
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
import AutomationActionFileInput from './AutomationFileInput.vue';
export default {
components: {
AutomationActionTeamMessageInput,
AutomationActionFileInput,
},
props: {
value: {
@ -109,6 +116,10 @@ export default {
type: Boolean,
default: true,
},
initialFileName: {
type: String,
default: '',
},
},
computed: {
action_name: {
@ -187,6 +198,7 @@ export default {
.filter__answer--wrap {
margin-right: var(--space-smaller);
flex-grow: 1;
max-width: 50%;
input {
margin-bottom: 0;

View file

@ -0,0 +1,117 @@
<template>
<label class="input-wrapper" :class="uploadState">
<input
v-if="uploadState !== 'processing'"
type="file"
name="attachment"
:class="uploadState === 'processing' ? 'disabled' : ''"
@change="onChangeFile"
/>
<spinner v-if="uploadState === 'processing'" />
<fluent-icon v-if="uploadState === 'idle'" icon="file-upload" />
<fluent-icon
v-if="uploadState === 'uploaded'"
icon="checkmark-circle"
type="outline"
class="success-icon"
/>
<fluent-icon
v-if="uploadState === 'failed'"
icon="dismiss-circle"
type="outline"
class="error-icon"
/>
<p class="file-button">{{ label }}</p>
</label>
</template>
<script>
import Spinner from 'shared/components/Spinner';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
Spinner,
},
mixins: [alertMixin],
props: {
value: {
type: Array,
default: () => [],
},
initialFileName: {
type: String,
default: '',
},
},
data() {
return {
uploadState: 'idle',
label: this.$t('AUTOMATION.ATTACHMENT.LABEL_IDLE'),
};
},
mounted() {
if (this.initialFileName) {
this.label = this.initialFileName;
this.uploadState = 'uploaded';
}
},
methods: {
async onChangeFile(event) {
this.uploadState = 'processing';
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
try {
const file = event.target.files[0];
const formData = new FormData();
formData.append('attachment', file, file.name);
const id = await this.$store.dispatch(
'automations/uploadAttachment',
formData
);
this.$emit('input', [id]);
this.uploadState = 'uploaded';
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADED');
} catch (error) {
this.uploadState = 'failed';
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED');
this.showAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
}
},
},
};
</script>
<style scoped>
input[type='file'] {
display: none;
}
.input-wrapper {
display: flex;
height: 39px;
background-color: var(--white);
border-radius: var(--border-radius-small);
border: 1px dashed var(--w-100);
padding: var(--space-smaller) var(--space-small);
align-items: center;
font-size: var(--font-size-mini);
cursor: pointer;
}
.success-icon {
margin-right: var(--space-small);
color: var(--g-500);
}
.error-icon {
margin-right: var(--space-small);
color: var(--r-500);
}
.processing {
cursor: not-allowed;
opacity: 0.9;
}
.file-button {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
</style>

View file

@ -1,7 +1,15 @@
const allElementsString = arr => {
return arr.every(elem => typeof elem === 'string');
};
const allElementsNumbers = arr => {
return arr.every(elem => typeof elem === 'number');
};
const formatArray = params => {
if (params.length <= 0) {
params = [];
} else if (params.every(elem => typeof elem === 'string')) {
} else if (allElementsString(params) || allElementsNumbers(params)) {
params = [...params];
} else {
params = params.map(val => val.id);

View file

@ -104,6 +104,13 @@
"DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later",
"CONFIRMATION_LABEL": "Yes",
"CANCEL_LABEL": "No"
},
"ATTACHMENT": {
"UPLOAD_ERROR": "Could not upload attachment, Please try again",
"LABEL_IDLE": "Upload Attachment",
"LABEL_UPLOADING": "Uploading...",
"LABEL_UPLOADED": "Succesfully Uploaded",
"LABEL_UPLOAD_FAILED": "Upload Failed"
}
}
}

View file

@ -101,6 +101,12 @@
showActionInput(automation.actions[i].action_name)
"
:v="$v.automation.actions.$each[i]"
:initial-file-name="
getFileName(
automation.actions[i].action_params[0],
automation.actions[i].action_name
)
"
@removeAction="removeAction(i)"
/>
<div class="filter-actions">
@ -507,6 +513,15 @@ export default {
if (type === null) return false;
return true;
},
getFileName(id, actionType) {
if (!id) return '';
if (actionType === 'send_attachment') {
const file = this.automation.files.find(item => item.blob_id === id);
// replace `blob_id.toString()` with file name once api is fixed.
if (file) return file.filename.toString();
}
return '';
},
},
};
</script>

View file

@ -119,11 +119,11 @@ export const AUTOMATIONS = {
name: 'Send Webhook Event',
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
},
// {
// key: 'send_attachment',
// name: 'Send Attachment',
// attributeI18nKey: 'SEND_ATTACHMENT',
// },
{
key: 'send_attachment',
name: 'Send Attachment',
attributeI18nKey: 'SEND_ATTACHMENT',
},
],
},
conversation_created: {
@ -210,11 +210,11 @@ export const AUTOMATIONS = {
name: 'Send Webhook Event',
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
},
// {
// key: 'send_attachment',
// name: 'Send Attachment',
// attributeI18nKey: 'SEND_ATTACHMENT',
// },
{
key: 'send_attachment',
name: 'Send Attachment',
attributeI18nKey: 'SEND_ATTACHMENT',
},
],
},
conversation_updated: {
@ -315,11 +315,11 @@ export const AUTOMATIONS = {
name: 'Send Webhook Event',
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
},
// {
// key: 'send_attachment',
// name: 'Send Attachment',
// attributeI18nKey: 'SEND_ATTACHMENT',
// },
{
key: 'send_attachment',
name: 'Send Attachment',
attributeI18nKey: 'SEND_ATTACHMENT',
},
],
},
};
@ -380,11 +380,11 @@ export const AUTOMATION_ACTION_TYPES = [
label: 'Send Webhook Event',
inputType: 'url',
},
// {
// key: 'send_attachment',
// label: 'Send Attachment',
// inputType: 'file',
// },
{
key: 'send_attachment',
label: 'Send Attachment',
inputType: 'attachment',
},
{
key: 'send_message',
label: 'Send a message',

View file

@ -76,6 +76,14 @@ export const actions = {
commit(types.SET_AUTOMATION_UI_FLAG, { isCloning: false });
}
},
uploadAttachment: async (_, file) => {
try {
const { data } = await AutomationAPI.attachment(file);
return data.blob_id;
} catch (error) {
throw new Error(error);
}
},
};
export const mutations = {

View file

@ -68,6 +68,7 @@
"emoji-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5ZM8.462 14.784A4.491 4.491 0 0 0 12 16.502a4.492 4.492 0 0 0 3.535-1.714.75.75 0 1 1 1.177.93A5.991 5.991 0 0 1 12 18.002a5.991 5.991 0 0 1-4.716-2.29.75.75 0 0 1 1.178-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z",
"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-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",
"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",
"globe-desktop-outline": "M22.002 12C22.002 6.477 17.524 2 12 2 6.476 1.999 2 6.477 2 12.001c0 5.186 3.947 9.45 9.001 9.952V20.11c-.778-.612-1.478-1.905-1.939-3.61h1.94V15H8.737a18.969 18.969 0 0 1-.135-5h6.794c.068.64.105 1.31.105 2h1.5c0-.684-.033-1.353-.095-2h3.358c.154.64.237 1.31.237 2h1.5ZM4.786 16.5h2.722l.102.396c.317 1.17.748 2.195 1.27 3.015a8.532 8.532 0 0 1-4.094-3.41ZM3.736 10h3.358a20.847 20.847 0 0 0-.095 2c0 1.043.075 2.051.217 3H4.043a8.483 8.483 0 0 1-.544-3c0-.682.08-1.347.232-1.983L3.736 10Zm5.122-5.902.023-.008C8.16 5.222 7.611 6.748 7.298 8.5H4.25c.905-2 2.56-3.587 4.608-4.402Zm3.026-.594L12 3.5l.126.006c1.262.126 2.48 2.125 3.045 4.995H8.83c.568-2.878 1.79-4.88 3.055-4.996Zm3.343.76-.107-.174.291.121a8.533 8.533 0 0 1 4.339 4.29h-3.048c-.298-1.665-.806-3.125-1.475-4.237Z M12 19a1 1 0 0 0 1 1h3v2h-.5a.5.5 0 1 0 0 1h4a.5.5 0 0 0 0-1H19v-2h3a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1v5Z",

View file

@ -28,7 +28,6 @@ class Attachment < ApplicationRecord
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
application/vnd.openxmlformats-officedocument.wordprocessingml.document
].freeze
belongs_to :account
belongs_to :message
has_one_attached :file

View file

@ -18,6 +18,8 @@
# index_automation_rules_on_account_id (account_id)
#
class AutomationRule < ApplicationRecord
include Rails.application.routes.url_helpers
belongs_to :account
has_many_attached :files
@ -28,7 +30,21 @@ class AutomationRule < ApplicationRecord
scope :active, -> { where(active: true) }
CONDITIONS_ATTRS = %w[content email country_code status message_type browser_language assignee_id team_id referer city company inbox_id].freeze
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents send_attachments].freeze
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents send_attachment].freeze
def file_base_data
files.map do |file|
{
id: file.id,
automation_rule_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

View file

@ -7,6 +7,10 @@ class AutomationRulePolicy < ApplicationPolicy
@account_user.administrator?
end
def attach_file?
@account_user.administrator?
end
def show?
@account_user.administrator?
end

View file

@ -21,9 +21,14 @@ class AutomationRules::ActionService
private
def send_attachments(_file_params)
blobs = @rule.files.map { |file, _| file.blob }
params = { content: nil, private: false, attachments: blobs }
def send_attachment(blob_ids)
return unless @rule.files.attached?
blob = ActiveStorage::Blob.find(blob_ids)
return if blob.blank?
params = { content: nil, private: false, attachments: blob }
mb = Messages::MessageBuilder.new(nil, @conversation, params)
mb.perform
end
@ -51,7 +56,7 @@ class AutomationRules::ActionService
end
def send_webhook_event(webhook_url)
payload = @conversation.webhook_data.merge(event: "automation_event: #{@rule.event_name}")
payload = @conversation.webhook_data.merge(event: "automation_event.#{@rule.event_name}")
WebhookJob.perform_later(webhook_url[0], payload)
end

View file

@ -7,3 +7,4 @@ json.conditions automation_rule.conditions
json.actions automation_rule.actions
json.created_on automation_rule.created_at.to_i
json.active automation_rule.active?
json.files automation_rule.file_base_data if automation_rule.files.any?

View file

@ -55,6 +55,7 @@ Rails.application.routes.draw do
resources :canned_responses, only: [:index, :create, :update, :destroy]
resources :automation_rules, only: [:index, :create, :show, :update, :destroy] do
post :clone
post :attach_file, on: :collection
end
resources :campaigns, only: [:index, :create, :show, :update, :destroy]

View file

@ -133,29 +133,35 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do
it 'Saves file in the automation actions to send an attachments' do
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
params[:attachments] = [file]
expect(account.automation_rules.count).to eq(0)
post "/api/v1/accounts/#{account.id}/automation_rules/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: :update_additional_attributes,
action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }]
},
{
action_name: :send_attachments
action_name: :send_attachment,
action_params: [blob['blob_id']]
}
]
expect(account.automation_rules.count).to eq(0)
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
expect(account.automation_rules.count).to eq(1)
automation_rule = account.automation_rules.first
expect(automation_rule.files.presence).to be_truthy
expect(automation_rule.files.count).to eq(1)
@ -163,32 +169,36 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do
it 'Saves files in the automation actions to send multiple attachments' do
file_1 = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
file_2 = fixture_file_upload(Rails.root.join('spec/assets/sample.pdf'), 'image/png')
params[:attachments] = [file_1, file_2]
file_2 = fixture_file_upload(Rails.root.join('spec/assets/sample.png'), 'image/png')
post "/api/v1/accounts/#{account.id}/automation_rules/attach_file",
headers: administrator.create_new_auth_token,
params: { attachment: file_1 }
blob_1 = JSON.parse(response.body)
post "/api/v1/accounts/#{account.id}/automation_rules/attach_file",
headers: administrator.create_new_auth_token,
params: { attachment: file_2 }
blob_2 = JSON.parse(response.body)
params[:actions] = [
{
action_name: :send_message,
action_params: ['Welcome to the chatwoot platform.']
action_name: :send_attachment,
action_params: [blob_1['blob_id']]
},
{
action_name: :update_additional_attributes,
action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }]
},
{
action_name: :send_attachments
action_name: :send_attachment,
action_params: [blob_2['blob_id']]
}
]
expect(account.automation_rules.count).to eq(0)
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
expect(account.automation_rules.count).to eq(1)
automation_rule = account.automation_rules.first
expect(automation_rule.files.presence).to be_truthy
expect(automation_rule.files.count).to eq(2)
end
end

View file

@ -128,6 +128,15 @@ describe AutomationRuleListener do
expect(conversation.messages.first.content).to eq('Send this message.')
end
it 'triggers automation rule to mute conversation' do
expect(conversation).not_to be_muted
listener.conversation_updated(event)
conversation.reload
expect(conversation).to be_muted
end
it 'triggers automation_rule with contact standard attributes' do
automation_rule.update!(
conditions: [