1
0

Implement email search

This commit is contained in:
Tim Kächele 2020-12-11 20:00:57 +01:00
parent 7bd547141b
commit c644ddbe8d
7 changed files with 172 additions and 53 deletions

View File

@ -1,6 +1,12 @@
class EmailsController < ApplicationController class EmailsController < ApplicationController
before_action :set_email, only: [:show, :mark_as_read, :body_preview] before_action :set_email, only: [:show, :mark_as_read, :body_preview]
def index
@emails = Email.where(mailbox_id: params[:mailbox_id])
@emails = @emails.search(params[:q]) if params[:q].present?
render(layout: false)
end
def show def show
@selected = params[:selected] @selected = params[:selected]
@email.mark_as_read! unless @email.read? @email.mark_as_read! unless @email.read?

View File

@ -0,0 +1,47 @@
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "query", "output" ]
static values = { url: String }
connect() {
this.requests = [];
}
search() {
this.resetTimeout();
this.cancelPreviousRequests();
this.searchTimeout = setTimeout(() => {
let abortController = new AbortController();
this.requests.push(abortController)
let signal = abortController.signal;
let url = this.urlValue + "&q=" + this.queryTarget.value;
fetch(url, { signal })
.then((response) => {
return response.text();
})
.then((html) => {
this.outputTarget.innerHTML = html;
})
.catch(e => {
console.log(e)
})
}, 75)
}
resetTimeout() {
if(this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
}
cancelPreviousRequests() {
this.requests.forEach(request => {
request.abort();
});
this.requests = [];
}
}

View File

@ -9,6 +9,10 @@ class Email < ApplicationRecord
validates :rendered_html, presence: true validates :rendered_html, presence: true
validates :rendered_plain_text, presence: true validates :rendered_plain_text, presence: true
def self.search(query)
Search::EmailSearch.new.search(self, query)
end
def self.unread def self.unread
where(read_at: nil) where(read_at: nil)
end end

View File

@ -0,0 +1,32 @@
module Search
class EmailSearch
def call(collection, search_query)
collection.where(where_statement,
search_query_parts: search_query_parts(search_query))
end
alias search call
private
def search_query_parts(search_query)
return [] if search_query.blank?
search_query.split(/\s+/)
.map { |query| "%#{query}%" }
end
def where_statement
<<~SQL
request_payload->'personalizations'->>personalization_id
ILIKE
ANY(ARRAY[:search_query_parts])
OR
subject ILIKE ANY(ARRAY[:search_query_parts])
OR
template_id ILIKE ANY(ARRAY[:search_query_parts])
OR
rendered_plain_text ILIKE ANY(ARRAY[:search_query_parts])
SQL
end
end
end

View File

@ -0,0 +1,18 @@
<% if @emails.any? %>
<div class="email email--no-chrome">
<b class="email__from">
From
</b>
<b class="email__to">To</b>
<b class="email__subject">Subject</b>
<b class="email__created-at">Received At</b>
</div>
<% @emails.each do |email| %>
<%= render(EmailListEntryComponent.new(email: email)) %>
<% end %>
<% else %>
<div class="text-center">
<p>¯\_(ツ)_/¯</p>
<p>We couldn't find any emails.</p>
</div>
<% end %>

View File

@ -1,56 +1,68 @@
<section> <div data-controller="search" data-search-url-value="<%= emails_path(mailbox_id: @mailbox.id) %>">
<header class="header header--with-border"> <section>
<div class="header--justified-header"> <header class="header header--with-border">
<h1><%= @mailbox.name %> (<%= @mailbox.unread_count %>)</h1> <div class="header--justified-header">
<div data-controller="remote-content"> <h1><%= @mailbox.name %> (<%= @mailbox.unread_count %>)</h1>
<p> <div data-controller="remote-content">
<%= link_to '&#128712;'.html_safe, <p>
details_mailbox_path(@mailbox), <%= link_to '&#128712;'.html_safe,
class: "button", details_mailbox_path(@mailbox),
data: { action: 'click->remote-content#load'} %> class: "button",
data: { action: 'click->remote-content#load'} %>
</p>
<div data-remote-content-target="outlet"></div>
</div>
</div>
<% if @emails.any? %>
<p class="button-collection">
<%= link_to 'Mark all as read',
mark_all_as_read_mailbox_path(@mailbox),
class: 'button button--primary',
method: :post,
'data-confirm': 'Are you sure you want to mark all emails as read?' %>
<%= link_to 'Clear mailbox',
clear_mailbox_mailbox_path(@mailbox),
class: 'button button--danger',
method: :post,
'data-confirm': 'Are you sure you want to delete all emails?' %>
</p> </p>
<div data-remote-content-target="outlet"></div> <% end %>
</div> </header>
</div> </section>
<div class="form-group">
<input class="form-control"
data-search-target="query"
data-action="keyup->search#search"
placeholder="Search..."
type="text"
class="form-control"
/>
</div>
<div data-search-target="output">
<% if @emails.any? %> <% if @emails.any? %>
<p class="button-collection"> <div class="email email--no-chrome">
<%= link_to 'Mark all as read', <b class="email__from">
mark_all_as_read_mailbox_path(@mailbox), From
class: 'button button--primary', </b>
method: :post, <b class="email__to">To</b>
'data-confirm': 'Are you sure you want to mark all emails as read?' %> <b class="email__subject">Subject</b>
<%= link_to 'Clear mailbox', <b class="email__created-at">Received At</b>
clear_mailbox_mailbox_path(@mailbox),
class: 'button button--danger',
method: :post,
'data-confirm': 'Are you sure you want to delete all emails?' %>
</p>
<% end %>
</header>
</section>
<% if @emails.any? %>
<div class="email email--no-chrome">
<b class="email__from">
From
</b>
<b class="email__to">To</b>
<b class="email__subject">Subject</b>
<b class="email__created-at">Received At</b>
</div>
<% @emails.each do |email| %>
<%= render(EmailListEntryComponent.new(email: email)) %>
<% end %>
<% else %>
<div class="text-center">
<p>¯\_(ツ)_/¯</p>
<p>You don't have any emails yet.</p>
<p>Use this api token to send emails with sendgrid</p>
<div class="width-50 centered input-group" data-controller="clipboard">
<input data-clipboard-target="source" type="text" class="input-group__input form-control" readonly value="<%= @mailbox.sendgrid_mock_api_token %>"/>
<div class="input-group__append">
<a data-action="click->clipboard#copy" href="#">Copy</a>
</div> </div>
</div> <% @emails.each do |email| %>
<%= render(EmailListEntryComponent.new(email: email)) %>
<% end %>
<% else %>
<div class="text-center">
<p>¯\_(ツ)_/¯</p>
<p>You don't have any emails yet.</p>
<p>Use this api token to send emails with sendgrid</p>
<div class="width-50 centered input-group" data-controller="clipboard">
<input data-clipboard-target="source" type="text" class="input-group__input form-control" readonly value="<%= @mailbox.sendgrid_mock_api_token %>"/>
<div class="input-group__append">
<a data-action="click->clipboard#copy" href="#">Copy</a>
</div>
</div>
</div>
<% end %>
</div> </div>
<% end %> </div>

View File

@ -7,7 +7,7 @@ Rails.application.routes.draw do
post 'clear_mailbox', to: 'mailboxes#clear_mailbox' post 'clear_mailbox', to: 'mailboxes#clear_mailbox'
end end
end end
resources :emails, only: [:show] do resources :emails, only: [:index, :show] do
member do member do
get '/preview', to: 'emails#body_preview' get '/preview', to: 'emails#body_preview'
end end