diff --git a/app/controllers/api/submitters_autocomplete_controller.rb b/app/controllers/api/submitters_autocomplete_controller.rb new file mode 100644 index 00000000..ff7dc2e6 --- /dev/null +++ b/app/controllers/api/submitters_autocomplete_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Api + class SubmittersAutocompleteController < ApiBaseController + load_and_authorize_resource :submitter, parent: false + + SELECT_COLUMNS = %w[email phone name].freeze + LIMIT = 100 + + def index + submitters = search_submitters(@submitters) + + values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', ')) + + attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h } + attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present? + + render json: attrs + end + + private + + def search_submitters(submitters) + if SELECT_COLUMNS.include?(params[:field]) + column = Submitter.arel_table[params[:field].to_sym] + + term = "%#{params[:q].downcase}%" + + submitters.where(column.lower.matches(term)) + else + Submitters.search(submitters, params[:q]) + end + end + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index 8860eb7f..f9d1e043 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -15,6 +15,7 @@ import DownloadButton from './elements/download_button' import SetOriginUrl from './elements/set_origin_url' import SetTimezone from './elements/set_timezone' import AutoresizeTextarea from './elements/autoresize_textarea' +import SubmittersAutocomplete from './elements/submitter_autocomplete' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -43,6 +44,7 @@ window.customElements.define('download-button', DownloadButton) window.customElements.define('set-origin-url', SetOriginUrl) window.customElements.define('set-timezone', SetTimezone) window.customElements.define('autoresize-textarea', AutoresizeTextarea) +window.customElements.define('submitters-autocomplete', SubmittersAutocomplete) document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody) document.addEventListener('turbo:submit-end', async (event) => { diff --git a/app/javascript/application.scss b/app/javascript/application.scss index e731dfc5..3adfab12 100644 --- a/app/javascript/application.scss +++ b/app/javascript/application.scss @@ -91,3 +91,30 @@ button[disabled] .enabled { right: auto; bottom: auto; } + +.autocomplete { + background: white; + z-index: 1000; + font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + overflow: auto; + box-sizing: border-box; + @apply border border-base-300; +} + +.autocomplete * { + font: inherit; +} + +.autocomplete > div { + padding: 0 4px; +} + +.autocomplete .group { + background: #eee; +} + +.autocomplete > div:hover:not(.group), +.autocomplete > div.selected { + @apply bg-base-300; + cursor: pointer; +} diff --git a/app/javascript/elements/submitter_autocomplete.js b/app/javascript/elements/submitter_autocomplete.js new file mode 100644 index 00000000..d6fcf707 --- /dev/null +++ b/app/javascript/elements/submitter_autocomplete.js @@ -0,0 +1,56 @@ +import autocomplete from 'autocompleter' + +export default class extends HTMLElement { + connectedCallback () { + autocomplete({ + input: this.input, + preventSubmit: 1, + minLength: 1, + showOnFocus: true, + onSelect: this.onSelect, + render: this.render, + fetch: this.fetch + }) + } + + onSelect = (item) => { + const fields = ['email', 'name', 'phone'] + const submitterItemEl = this.closest('submitter-item') + + fields.forEach((field) => { + const input = submitterItemEl.querySelector(`submitters-autocomplete[data-field="${field}"] input`) + + if (input && item[field]) { + input.value = item[field] + } + }) + } + + fetch = (text, resolve) => { + if (text) { + const queryParams = new URLSearchParams({ q: text, field: this.dataset.field }) + + fetch('/api/submitters_autocomplete?' + queryParams).then(async (resp) => { + const items = await resp.json() + + resolve(items) + }).catch(() => { + resolve([]) + }) + } else { + resolve([]) + } + } + + render = (item) => { + const div = document.createElement('div') + + div.textContent = item[this.dataset.field] + + return div + } + + get input () { + return this.querySelector('input') + } +} diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb index 1b54ac0a..ae2a59cd 100644 --- a/app/views/submissions/_detailed_form.html.erb +++ b/app/views/submissions/_detailed_form.html.erb @@ -10,19 +10,25 @@
<% template.submitters.each do |item| %> -
+ <% if template.submitters.size > 1 %> <% end %> - + + +
- - + + + + + +
-
+ <% end %>
diff --git a/app/views/submissions/_email_form.html.erb b/app/views/submissions/_email_form.html.erb index cfb82077..422e7544 100644 --- a/app/views/submissions/_email_form.html.erb +++ b/app/views/submissions/_email_form.html.erb @@ -17,13 +17,15 @@
<% template.submitters.each do |item| %> -
+ - -
+ + + + <% end %>
diff --git a/app/views/submissions/_phone_form.html.erb b/app/views/submissions/_phone_form.html.erb index 9183e06e..8ad65ec3 100644 --- a/app/views/submissions/_phone_form.html.erb +++ b/app/views/submissions/_phone_form.html.erb @@ -8,25 +8,33 @@ <%= svg_icon('trash', class: 'w-4 h-4') %> -
+
<% template.submitters.each do |item| %> -
- <% if template.submitters.size > 1 %> - - <% end %> - - - <% if template.submitters.size > 1 %> - - <% end %> -
- <% if template.submitters.size == 1 %> -
- + +
+ <% if template.submitters.size > 1 %> + + <% end %> + + + + + <% if template.submitters.size > 1 %> + + + + <% end %>
- <% end %> + <% if template.submitters.size == 1 %> +
+ + + +
+ <% end %> +
<% end %>
diff --git a/config/routes.rb b/config/routes.rb index 51eca223..921366b8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -31,6 +31,7 @@ Rails.application.routes.draw do namespace :api, defaults: { format: :json } do resources :attachments, only: %i[create] + resources :submitters_autocomplete, only: %i[index] resources :submitter_email_clicks, only: %i[create] resources :submitter_form_views, only: %i[create] resources :submissions, only: %i[create] diff --git a/lib/submitters.rb b/lib/submitters.rb index 9b04e7fe..6a92df59 100644 --- a/lib/submitters.rb +++ b/lib/submitters.rb @@ -3,6 +3,20 @@ module Submitters module_function + def search(submitters, keyword) + return submitters if keyword.blank? + + term = "%#{keyword.downcase}%" + + arel_table = Submitter.arel_table + + arel = arel_table[:email].lower.matches(term) + .or(arel_table[:phone].matches(term)) + .or(arel_table[:name].lower.matches(term)) + + submitters.where(arel) + end + def select_attachments_for_download(submitter) original_documents = submitter.submission.template.documents.preload(:blob) is_more_than_two_images = original_documents.count(&:image?) > 1 diff --git a/package.json b/package.json index ca4cfd12..36706394 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@hotwired/turbo-rails": "^7.3.0", "@rails/activestorage": "^7.0.0", "@tabler/icons-vue": "^2.20.0", + "autocompleter": "^9.1.0", "autoprefixer": "^10.4.14", "babel-loader": "9.1.2", "babel-plugin-dynamic-import-node": "^2.3.3", diff --git a/yarn.lock b/yarn.lock index f5e013a4..66f2a241 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1737,6 +1737,11 @@ array.prototype.flatmap@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" +autocompleter@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/autocompleter/-/autocompleter-9.1.0.tgz#c7248a8cc0c58376d0969734c40e29626d950f04" + integrity sha512-dwAYJTaLHj1MpzCZXFg8WLmk+tgQ85OEDFfBegGnA+uVUZyzW/PZAdjSXR3fOt0+q8ZeEfMDiHDqw60uoF1NDg== + autoprefixer@^10.4.14: version "10.4.14" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d"