Implement email search
This commit is contained in:
parent
7bd547141b
commit
c644ddbe8d
@ -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?
|
||||||
|
47
app/javascript/controllers/search_controller.js
Normal file
47
app/javascript/controllers/search_controller.js
Normal 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 = [];
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
32
app/services/search/email_search.rb
Normal file
32
app/services/search/email_search.rb
Normal 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
|
18
app/views/emails/index.html.erb
Normal file
18
app/views/emails/index.html.erb
Normal 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 %>
|
@ -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 '🛈'.html_safe,
|
<p>
|
||||||
details_mailbox_path(@mailbox),
|
<%= link_to '🛈'.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>
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user