Module: CommunicationsHelper

Defined in:
app/helpers/communications_helper.rb

Overview

View helper: communications.

Instance Method Summary collapse

Instance Method Details

#attachment_name(upload) ⇒ String

Display name for an upload attached to a communication.

Parameters:

  • upload (Upload)

    the uploaded attachment

Returns:

  • (String)

    the attachment's display name



19
20
21
# File 'app/helpers/communications_helper.rb', line 19

def attachment_name(upload)
  upload.attachment_name
end

#clean_received_body(body) ⇒ String

Strips quoted/forwarded content from a plain-text received email body for display.
Handles two common patterns:

  1. "On , wrote:" reply blocks (via EmailReplyParser)
  2. "From: / To: / Date: / Subject:" forwarded message header blocks (via regex)

Parameters:

  • body (String, nil)

    the plain-text received email body

Returns:

  • (String)

    the body with quoted/forwarded content stripped



228
229
230
231
232
# File 'app/helpers/communications_helper.rb', line 228

def clean_received_body(body)
  text = EmailReplyParser.parse_reply(body.to_s)
  # Strip forwarded-message header blocks: "From:\nTo: ..." and everything after
  text.sub(/\n+\s*From:[^\n]*\n[\t ]*(?:To|Sent|Date|Cc|Subject):.*\z/m, '').strip
end

#combo_category_select_options(category = nil) ⇒ Array<Array(String, String)>

Options for the recipient category combo select: email TO/CC/BCC and fax,
narrowed to a single category when one is given.

Parameters:

  • category (String, nil) (defaults to: nil)

    'email' or 'fax' to restrict the options,
    nil for all

Returns:

  • (Array<Array(String, String)>)

    [label, value] option pairs



125
126
127
128
129
130
131
132
133
134
135
136
# File 'app/helpers/communications_helper.rb', line 125

def combo_category_select_options(category = nil)
  opts = []
  if category.nil? || (category == 'email')
    opts += [
      ['Email TO', 'email_to'],
      ['Email CC', 'email_cc'],
      ['Email BCC', 'email_bcc']
    ]
  end
  opts += [%w[Fax fax]] if category.nil? || (category == 'fax')
  opts
end

#communication_actions(c) ⇒ Array<String>

Action links shown on a communication's detail page, gated by the current
user's abilities and the communication's state machine events.

Parameters:

Returns:

  • (Array<String>)

    the state label followed by HTML action links



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'app/helpers/communications_helper.rb', line 93

def communication_actions(c)
  [].tap do |cmds|
    cmds << c.human_state_name.titleize
    cmds << link_to(fa_icon('pen-to-square', text: 'Edit'), edit_communication_path(@communication)) if (c.draft? || c.exception?) && can?(:update, c)
    if can?(:destroy, c) && !c.sent?
      cmds << link_to(fa_icon('trash', text: 'Delete'), communication_path(@communication),
                                                                                            data: { turbo_confirm: 'Delete for sure?', turbo_method: :delete })
    end
    cmds << link_to(fa_icon('repeat', text: 'Resend this email'), resend_email_communication_path(@communication)) if can?(:create, Communication) && !@communication.draft? && @communication.direction_outbound?
    cmds << link_to(fa_icon('copy', text: 'Copy into new blank communication'), copy_communication_path(@communication)) if can?(:create, Communication) && !@communication.draft?
    if @communication.can_queue?
      cmds << link_to(fa_icon('envelope', text: 'Queue for delivery'), communication_path(@communication, communication: { state_event: :queue }),
                      data: { turbo_method: :put })
    end
    cmds << link_to(fa_icon('arrow-rotate-left', text: 'Back to Draft'), communication_path(@communication, communication: { state_event: :unqueue }), data: { turbo_method: :put }) if @communication.can_unqueue?
    cmds << link_to(fa_icon('envelope', text: 'Send Now'), send_now_communication_path(@communication), data: { turbo_method: :put }) if @communication.draft? || @communication.queued?
    if @communication.can_resend?
      cmds << link_to(fa_icon('envelope', text: 'Queue to Resend'), communication_path(@communication, communication: { state_event: :resend }),
                      data: { turbo_method: :put })
    end
    cmds << link_to(fa_icon('reply', text: 'Reply'), reply_communication_path(@communication))
    cmds << link_to(fa_icon('reply-all', text: 'Reply All'), reply_all_communication_path(@communication)) if @communication.communication_recipients.many?
    cmds << link_to(fa_icon('bell', text: 'Events'), events_communication_path(@communication))
  end
end

#communication_recipient_delete_button(f) ⇒ String

Renders the delete affordance for a nested communication-recipient field:
a _destroy checkbox set for persisted rows, a JS remove link for new ones.

Parameters:

  • f (ActionView::Helpers::FormBuilder)

    the nested recipient form builder

Returns:

  • (String)

    HTML for the delete control, or nil when neither branch applies



64
65
66
67
68
69
70
71
72
73
74
# File 'app/helpers/communications_helper.rb', line 64

def communication_recipient_delete_button(f)
  if f.object.persisted?
    (:div, class: 'btn btn-outline-warning') do
      concat f.hidden_field '_destroy'
      concat f.label '_destroy', fa_icon('trash'), class: 'm-0 p-0'
      concat f.check_box '_destroy', class: 'destroy'
    end
  else
    link_to(fa_icon('trash'), '#', class: 'btn btn-outline-warning trash-remove', data: { 'class-to-remove': '.communication_recipient' })
  end
end

#communication_recipient_state_label(communication_recipient) ⇒ ActiveSupport::SafeBuffer

Bootstrap badge for a recipient's delivery state (delivered, bounced, …).

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    the state badge HTML



166
167
168
169
170
171
# File 'app/helpers/communications_helper.rb', line 166

def communication_recipient_state_label(communication_recipient)
  label_class = { processed: 'secondary', dropped: 'danger', delivered: 'success',
                  deferred: 'warning', bounced: 'danger', opened: 'success', ok: 'success',
                  clicked: 'success', spammed: 'danger', unsubscribed: 'warning' }[communication_recipient.state.to_sym] || 'default'
   :span, communication_recipient.human_state_name.upcase, class: "badge bg-#{label_class}"
end

#communication_state_details(cr) ⇒ ActiveSupport::SafeBuffer

Human-readable summary of when a recipient's delivery state last changed,
plus any gateway response message recorded with it.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    HTML-safe details string (may be empty)



81
82
83
84
85
86
# File 'app/helpers/communications_helper.rb', line 81

def communication_state_details(cr)
  msg = []
  msg << "State triggered at #{cr.state_updated_at.to_fs(:crm_default)}" if cr.state_updated_at
  msg << "Response: #{cr.state_response}" if cr.state_response.present?
  raw(msg.join('. '))
end

#communication_state_label(communication) ⇒ ActiveSupport::SafeBuffer

Bootstrap badge for a communication's state; appends "(Scheduled)" when a
queued communication has a future transmit time.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    the state badge HTML



155
156
157
158
159
160
# File 'app/helpers/communications_helper.rb', line 155

def communication_state_label(communication)
  label_class = { exception: 'danger', suppressed: 'danger', queued: 'warning', sent: 'success', received: 'info', draft: 'secondary' }[communication.state.to_sym] || 'default'
  comm_label = communication.human_state_name.upcase
  comm_label << ' (Scheduled)' if communication.transmit_at && communication.queued?
   :span, comm_label, class: "badge bg-#{label_class}"
end

Rewrites external (http/https) links in a displayed email body so they open
in a new browser tab instead of "in place". Display-only — never apply this to
the stored body, since Communication#compliant_body is the literal payload
handed to CommunicationMailer and preview affordances must not leak into sent
mail.

Outbound/template bodies render inline inside the Activities <turbo-frame>,
where Turbo otherwise intercepts a bare <a> click and loads the destination
into the frame. An explicit target="_blank" sidesteps that (Turbo leaves
links with an explicit target alone) and rel="noopener noreferrer" is the
standard safety companion. Received HTML emails are handled separately via a
<base target> in their srcdoc iframe (see _preview_body.html.erb).

Returns the body unchanged when it has no external anchors, and parses
fragments as fragments so a plain-text or partial body is never wrapped in a
synthetic <html>/<body>.

Parameters:

  • html (String, nil)

    the email body HTML

Returns:

  • (String)

    the body with external anchors marked target="_blank"



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'app/helpers/communications_helper.rb', line 202

def email_links_in_new_tab(html)
  source = html.to_s
  return source unless source.match?(/<a[\s>]/i)

  full_document = source.match?(/<html[\s>]|<!doctype\s+html/i)
  doc = full_document ? Nokogiri::HTML(source) : Nokogiri::HTML::DocumentFragment.parse(source)

  rewrote = false
  doc.css('a[href]').each do |node|
    next unless node['href'].to_s.match?(%r{\Ahttps?://}i)

    node['target'] = '_blank'
    node['rel'] = (node['rel'].to_s.split + %w[noopener noreferrer]).uniq.join(' ')
    rewrote = true
  end

  rewrote ? doc.to_html : source
end

#email_template_option(template) ⇒ Array(String, Integer)

Builds a single [label, id] option pair for an email template select.
Legacy Redactor 3 templates are flagged (v3) in the label.

Parameters:

  • template (EmailTemplate)

    the template to render as an option

Returns:

  • (Array(String, Integer))

    the option label and the template id



49
50
51
52
53
54
55
56
57
# File 'app/helpers/communications_helper.rb', line 49

def email_template_option(template)
  # All templates are selectable, but mark v3 templates in the label
  label = if template.redactor_4_ready?
            "#{template.description} [#{template.category}]"
          else
            "#{template.description} [#{template.category}] (v3)"
          end
  [label, template.id]
end

#email_templates_for_user(include_email_template_id) ⇒ Hash{String => Array<Array(String, Integer)>}

Builds the grouped option list of email templates available to the current
context user: templates private to that employee plus global templates.

Parameters:

  • include_email_template_id (Integer, nil)

    template id to include even
    when it is not owned by the context user (e.g. the template already
    selected on the record being edited)

Returns:

  • (Hash{String => Array<Array(String, Integer)>})

    optgroup label to
    [label, id] option pairs, for grouped_options_for_select



31
32
33
34
35
36
37
38
39
40
41
42
# File 'app/helpers/communications_helper.rb', line 31

def email_templates_for_user(include_email_template_id)
  options = {}
  # Show all templates - Redactor 3 templates are shown but disabled (only v4 can be selected)
  base_query = EmailTemplate.non_campaign.active.order(:description)
  person_filter_sql = "resource_type = 'Employee' and resource_id = :employee_id"
  person_filter_sql = "(#{person_filter_sql}) OR email_templates.id = :email_template_id" if include_email_template_id
  tpl_pers = base_query.where(person_filter_sql, employee_id: @context_user.id, email_template_id: include_email_template_id)
  tpl_global = base_query.where(resource_id: nil)
  options['Private Templates'] = tpl_pers.map { |t| email_template_option(t) } if tpl_pers.present?
  options['Global Templates'] = tpl_global.map { |t| email_template_option(t) } if tpl_global.present?
  options
end

#preview_communication_body(communication) ⇒ String?

Renders the body preview partial for a communication.

Parameters:

  • communication (Communication, nil)

    the communication to preview

Returns:

  • (String, nil)

    the rendered preview partial, nil without a communication



177
178
179
180
181
# File 'app/helpers/communications_helper.rb', line 177

def preview_communication_body(communication)
  return unless communication

  render partial: '/communications/preview_body', locals: { communication: communication }
end

#setup_communication(communication) ⇒ Communication

Ensures the communication has at least one email recipient row so the
recipient sub-form renders an editable entry.

Parameters:

Returns:

  • (Communication)

    the same communication, for form-builder chaining



9
10
11
12
13
# File 'app/helpers/communications_helper.rb', line 9

def setup_communication(communication)
  communication.tap do |c|
    c.communication_recipients.build(category: ContactPoint::EMAIL, email_method: 'to') unless c.communication_recipients.find { |cr| cr.category == ContactPoint::EMAIL }
  end
end

#translate_cp_category_to_combo(category) ⇒ String?

Maps a contact-point category to the matching combo-select value.

Parameters:

  • category (String)

    the contact-point category ('email' or 'fax')

Returns:

  • (String, nil)

    the combo value ('email_to' or 'fax'), nil if unmapped

See Also:



143
144
145
146
147
148
# File 'app/helpers/communications_helper.rb', line 143

def translate_cp_category_to_combo(category)
  {
    'email' => 'email_to',
    'fax' => 'fax'
  }[category]
end