diff --git a/app/javascript/application.js b/app/javascript/application.js index 7dee5edf9..f32bf9968 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -35,6 +35,8 @@ import PromptPassword from './elements/prompt_password' import EmailsTextarea from './elements/emails_textarea' import ToggleSubmit from './elements/toggle_submit' import ToggleOnSubmit from './elements/toggle_on_submit' +import ToggleOnChecked from './elements/toggle_on_checked' +import AppendTemplate from './elements/append_template' import CheckOnClick from './elements/check_on_click' import PasswordInput from './elements/password_input' import SearchInput from './elements/search_input' @@ -159,6 +161,8 @@ safeRegisterElement('emails-textarea', EmailsTextarea) safeRegisterElement('toggle-cookies', ToggleCookies) safeRegisterElement('toggle-submit', ToggleSubmit) safeRegisterElement('toggle-on-submit', ToggleOnSubmit) +safeRegisterElement('toggle-on-checked', ToggleOnChecked) +safeRegisterElement('append-template', AppendTemplate) safeRegisterElement('password-input', PasswordInput) safeRegisterElement('search-input', SearchInput) safeRegisterElement('toggle-attribute', ToggleAttribute) diff --git a/app/javascript/elements/append_template.js b/app/javascript/elements/append_template.js new file mode 100644 index 000000000..18012bcf4 --- /dev/null +++ b/app/javascript/elements/append_template.js @@ -0,0 +1,29 @@ +export default class extends HTMLElement { + connectedCallback () { + this.addEventListener('click', this.onClick) + } + + disconnectedCallback () { + this.removeEventListener('click', this.onClick) + } + + onClick = (e) => { + e.preventDefault() + + const target = document.getElementById(this.dataset.targetId) + const item = this.querySelector('template').content.firstElementChild.cloneNode(true) + + item.querySelector('a').addEventListener('click', (e) => { + e.preventDefault() + + item.remove() + + target.classList.toggle('hidden', !target.childElementCount) + }) + + target.append(item) + target.classList.remove('hidden') + + item.querySelector('input').focus() + } +} diff --git a/app/javascript/elements/dynamic_list.js b/app/javascript/elements/dynamic_list.js index 57c9ccf18..a0ad26c0b 100644 --- a/app/javascript/elements/dynamic_list.js +++ b/app/javascript/elements/dynamic_list.js @@ -2,7 +2,7 @@ import { actionable } from '@github/catalyst/lib/actionable' import { targets, targetable } from '@github/catalyst/lib/targetable' export default actionable(targetable(class extends HTMLElement { - static [targets.static] = ['items'] + static [targets.static] = ['items', 'groups'] addItem (e) { e.preventDefault() @@ -24,11 +24,28 @@ export default actionable(targetable(class extends HTMLElement { duplicateItem.querySelectorAll('a.hidden').forEach((button) => button.classList.toggle('hidden')) originalItem.parentNode.append(duplicateItem) + + this.toggleMultipleClasses() + + duplicateItem.querySelector("input:not([type='hidden'])")?.focus() } removeItem (e) { e.preventDefault() this.items.find((item) => item.contains(e.target))?.remove() + + this.toggleMultipleClasses() + } + + toggleMultipleClasses () { + const isMultiple = this.items.length > 1 + const elements = [...this.items, ...this.groups] + + elements.forEach((element) => { + const classes = element.dataset.multipleClasses || this.dataset.multipleClasses + + classes?.split(' ').forEach((cls) => element.classList.toggle(cls, isMultiple)) + }) } })) diff --git a/app/javascript/elements/toggle_on_checked.js b/app/javascript/elements/toggle_on_checked.js new file mode 100644 index 000000000..85ba9b0df --- /dev/null +++ b/app/javascript/elements/toggle_on_checked.js @@ -0,0 +1,23 @@ +export default class extends HTMLElement { + connectedCallback () { + this.form.addEventListener('change', this.toggle) + + this.toggle() + } + + disconnectedCallback () { + this.form.removeEventListener('change', this.toggle) + } + + toggle = () => { + const selector = this.dataset.names.split(' ').map((name) => `[name="${name}"]:checked:enabled`).join(', ') + const isChecked = !!this.form.querySelector(selector) + + this.firstElementChild.classList.toggle('hidden', !isChecked) + this.lastElementChild.classList.toggle('hidden', isChecked) + } + + get form () { + return this.closest('form') + } +} diff --git a/app/javascript/native.js b/app/javascript/native.js index 7de1f76a1..5ddd44580 100644 --- a/app/javascript/native.js +++ b/app/javascript/native.js @@ -24,6 +24,9 @@ const ICONS = { user: 'person', users: 'person.2', users_plus: 'person.badge.plus', + user_plus: 'person.badge.plus', + eye_plus: 'eye', + mail_plus: 'envelope', photo: 'photo', settings: 'gearshape', logout: 'rectangle.portrait.and.arrow.right', diff --git a/app/views/icons/_eye_plus.html.erb b/app/views/icons/_eye_plus.html.erb new file mode 100644 index 000000000..ad4a2aef7 --- /dev/null +++ b/app/views/icons/_eye_plus.html.erb @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/views/icons/_mail_plus.html.erb b/app/views/icons/_mail_plus.html.erb new file mode 100644 index 000000000..5b4ecab38 --- /dev/null +++ b/app/views/icons/_mail_plus.html.erb @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/views/icons/_user_plus.html.erb b/app/views/icons/_user_plus.html.erb index 935d26d05..0a83ce86e 100644 --- a/app/views/icons/_user_plus.html.erb +++ b/app/views/icons/_user_plus.html.erb @@ -1,8 +1,7 @@ - - - + + diff --git a/app/views/submissions/_add_new.html.erb b/app/views/submissions/_add_new.html.erb new file mode 100644 index 000000000..3a283f7fb --- /dev/null +++ b/app/views/submissions/_add_new.html.erb @@ -0,0 +1,70 @@ +<% fields_id = "add_new_fields_#{SecureRandom.hex(3)}" %> + + + + + diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb index c948f568a..978323510 100644 --- a/app/views/submissions/_detailed_form.html.erb +++ b/app/views/submissions/_detailed_form.html.erb @@ -1,12 +1,12 @@ <% has_phone_field = false %> -<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> +<%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> - +
-
+
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? && local_assigns[:variables_form].blank? %> - - <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> - <%= t('add_new') %> - + <%= render 'add_new', template: %> <% end %> <%= local_assigns[:variables_form] %> -
+
<%= render('submitters_order', f:, template:) if can_send_emails %> <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %> <% if has_phone_field %> @@ -101,7 +98,12 @@ <% end %> <%= render 'extra_fields', f: %>
-
- <%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> +
+ <%= f.button class: 'base-button' do %> + + <%= button_title(title: t('send'), disabled_with: t('sending')) %> + + + <% end %>
<% end %> diff --git a/app/views/submissions/_email_form.html.erb b/app/views/submissions/_email_form.html.erb index 718cb905e..d11464583 100644 --- a/app/views/submissions/_email_form.html.erb +++ b/app/views/submissions/_email_form.html.erb @@ -1,4 +1,4 @@ -<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> +<%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> <% if submitters.size == 1 && local_assigns[:variables_form] %> <% item = submitters.first %> @@ -26,18 +26,18 @@ - <%= f.text_area :emails, required: true, class: 'base-textarea w-full !text-lg', placeholder: "#{t('type_emails_here')}...", rows: 2 %> + <%= f.text_area :emails, required: true, class: 'base-textarea block w-full !text-lg', placeholder: "#{t('type_emails_here')}...", rows: 2 %> <% else %> - +
-
+
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %> - - <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> - <%= t('add_new') %> - + <%= render 'add_new', template: %> <% end %> <% end %> <%= local_assigns[:variables_form] %> -
+
<%= render('submitters_order', f:, template:) if can_send_emails %> <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %> <%= render 'extra_fields', f: %>
-
+
<%= content_for(:submit_button) || capture do %> - <%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> + <%= f.button class: 'base-button' do %> + + <%= button_title(title: t('send'), disabled_with: t('sending')) %> + + + <% end %> <% end %>
<% end %> diff --git a/app/views/submissions/_phone_form.html.erb b/app/views/submissions/_phone_form.html.erb index a3d9b92d0..cf0f1748c 100644 --- a/app/views/submissions/_phone_form.html.erb +++ b/app/views/submissions/_phone_form.html.erb @@ -1,11 +1,11 @@ -<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> +<%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> - +
-
+
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %> - - <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> - <%= t('add_new') %> - + <%= render 'add_new', template: %> <% end %> <%= local_assigns[:variables_form] %> -
+
<%= render 'submitters_order', f:, template: %> <%= render 'send_sms', f:, checked: true %> <%= render 'extra_phone_fields', f: %>
-
- <%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> +
+ <%= f.button class: 'base-button' do %> + + <%= button_title(title: t('send'), disabled_with: t('sending')) %> + + + <% end %>
<% end %> diff --git a/app/views/submissions/_send_email_base.html.erb b/app/views/submissions/_send_email_base.html.erb index 24d25e737..2341d70c3 100644 --- a/app/views/submissions/_send_email_base.html.erb +++ b/app/views/submissions/_send_email_base.html.erb @@ -14,7 +14,7 @@ <% end %> diff --git a/app/views/submissions/_send_sms.html.erb b/app/views/submissions/_send_sms.html.erb index 92a8de542..598b3a887 100644 --- a/app/views/submissions/_send_sms.html.erb +++ b/app/views/submissions/_send_sms.html.erb @@ -1,5 +1,7 @@ <% unless turbo_native_ios? %> -
- <%= render 'sms_settings/placeholder' %> -
+ +
+ <%= render 'sms_settings/placeholder' %> +
+
<% end %> diff --git a/app/views/submissions/new.html.erb b/app/views/submissions/new.html.erb index 19fa41aa6..7821f1d2e 100644 --- a/app/views/submissions/new.html.erb +++ b/app/views/submissions/new.html.erb @@ -26,7 +26,7 @@
<% end %> -
+
<% unless only_detailed %>
<%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index b432b436a..55ae36830 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -1028,6 +1028,8 @@ en: &en failed_to_scan_the_document: Failed to scan the document document_scanning_is_not_available_on_this_device: Document scanning is not available on this device click_to_upload: Click to upload + cc_view_only: CC View-only + bcc_completed: BCC Completed devise: confirmations: confirmed: Your email address has been successfully confirmed. @@ -2176,6 +2178,8 @@ es: &es failed_to_save_the_scan: No se pudo guardar el escaneo failed_to_scan_the_document: No se pudo escanear el documento document_scanning_is_not_available_on_this_device: El escaneo de documentos no está disponible en este dispositivo + cc_view_only: CC Solo lectura + bcc_completed: CCO Completado devise: confirmations: confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente. @@ -3338,6 +3342,8 @@ it: &it failed_to_save_the_scan: Impossibile salvare la scansione failed_to_scan_the_document: Impossibile scansionare il documento document_scanning_is_not_available_on_this_device: La scansione dei documenti non è disponibile su questo dispositivo + cc_view_only: CC Sola lettura + bcc_completed: BCC Completato devise: confirmations: confirmed: Il tuo indirizzo email è stato confermato con successo. @@ -4483,6 +4489,8 @@ fr: &fr failed_to_save_the_scan: Impossible d’enregistrer la numérisation failed_to_scan_the_document: Impossible de numériser le document document_scanning_is_not_available_on_this_device: La numérisation de documents n’est pas disponible sur cet appareil + cc_view_only: Cc Lecture seule + bcc_completed: Cci Terminé devise: confirmations: confirmed: Votre adresse e-mail a été confirmée avec succès. @@ -5644,6 +5652,8 @@ pt: &pt failed_to_save_the_scan: Não foi possível salvar a digitalização failed_to_scan_the_document: Não foi possível digitalizar o documento document_scanning_is_not_available_on_this_device: A digitalização de documentos não está disponível neste dispositivo + cc_view_only: CC Somente leitura + bcc_completed: BCC Concluído devise: confirmations: confirmed: Seu endereço de e-mail foi confirmado com sucesso. @@ -6805,6 +6815,8 @@ de: &de failed_to_save_the_scan: Scan konnte nicht gespeichert werden failed_to_scan_the_document: Dokument konnte nicht gescannt werden document_scanning_is_not_available_on_this_device: Das Scannen von Dokumenten ist auf diesem Gerät nicht verfügbar + cc_view_only: CC Nur Ansicht + bcc_completed: BCC Abgeschlossen devise: confirmations: confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt. @@ -8490,6 +8502,8 @@ nl: &nl failed_to_save_the_scan: Scan kon niet worden opgeslagen failed_to_scan_the_document: Document kon niet worden gescand document_scanning_is_not_available_on_this_device: Documenten scannen is niet beschikbaar op dit apparaat + cc_view_only: CC Alleen bekijken + bcc_completed: BCC Voltooid devise: confirmations: confirmed: Uw e-mailadres is succesvol bevestigd. diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index 10117e33d..dd2780537 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -90,6 +90,10 @@ module Submissions maybe_add_invite_submitters(submission, template, attrs[:submitters]) + maybe_add_cc_submitters(submission, params[:cc_recipients], + submitters_order:, user:, params:, + preferences: preferences.merge(submission_preferences)) + assign_submitters_is_viewer(submission) submission.template = nil unless with_template @@ -211,6 +215,25 @@ module Submissions end end + def maybe_add_cc_submitters(submission, recipients, submitters_order:, user:, params:, preferences:) + order = submission.template_submitters.filter_map { |s| s['order'] }.max + + Array.wrap(recipients).each do |recipient| + attrs = recipient.is_a?(String) ? { email: recipient } : recipient.slice(:name, :email, :phone) + + next if attrs.compact_blank.blank? + + uuid = SecureRandom.uuid + + submission.template_submitters << + { 'name' => 'CC', 'uuid' => uuid, 'is_viewer' => true, 'order' => order }.compact + + build_submitter(submission:, attrs:, uuid:, user:, params:, + is_order_sent: submitters_order == 'random', + preferences: preferences.except('email_message_uuid')) + end + end + def submitter_message_preferences(uuid, params) return {} if params[:request_email_per_submitter] != '1' return {} if params[:is_custom_message] != '1' diff --git a/lib/submitters.rb b/lib/submitters.rb index 4dcd1ebd0..d191421d2 100644 --- a/lib/submitters.rb +++ b/lib/submitters.rb @@ -152,7 +152,7 @@ module Submitters preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms') preferences['require_phone_2fa'] = params['require_phone_2fa'].in?(TRUE_VALUES) if params.key?('require_phone_2fa') preferences['require_email_2fa'] = params['require_email_2fa'].in?(TRUE_VALUES) if params.key?('require_email_2fa') - preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed') + preferences['bcc_completed'] = normalize_bcc_completed(params['bcc_completed']) if params.key?('bcc_completed') preferences['reply_to'] = params['reply_to'] if params.key?('reply_to') preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last') preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url') @@ -160,6 +160,14 @@ module Submitters preferences end + def normalize_bcc_completed(value) + if value.is_a?(Array) + value.compact_blank.uniq.join(', ') + else + value + end + end + def fetch_values_for_delegate(submitter) fields = submitter.submission.template_fields || submitter.template.fields default_values = submitter.preferences['default_values'] || {}