rails-contact 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/app/controllers/rails/contact/contacts_controller.rb +27 -1
- data/app/jobs/rails/contact/google_sync_unsynced_job.rb +13 -0
- data/app/views/rails/contact/_index_pagination.html.erb +25 -0
- data/app/views/rails/contact/index.html.erb +17 -0
- data/config/routes.rb +1 -0
- data/lib/rails/contact/google/sync_service.rb +16 -0
- data/lib/rails/contact/search/backends/database.rb +13 -8
- data/lib/rails/contact/search/backends/elasticsearch.rb +19 -7
- data/lib/rails/contact/search/query.rb +12 -2
- data/lib/rails/contact/search/result.rb +13 -0
- data/lib/rails/contact/version.rb +1 -1
- data/lib/rails/contact.rb +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11aea6cb5a1f51337bf1c48e9cb0e3d34ada33d16af8fc5a8476a687d78eb5ba
|
|
4
|
+
data.tar.gz: 63ce98be235f82c0dfe65a22abe308566d44a72e423e0a2351d8e8d04a5ef0f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1dc45c5cbcb8d9db9bfa9e8374408e8746c2d0e03fd6bd7bea77acf767df515c74ce7b0334761e31f94ac9fdabb86a19af3d5a8ce4c3c6876b81184336e25704
|
|
7
|
+
data.tar.gz: 4345e73338be0e56ad00d29d9796fa3ea08e7136f319eb6df79939823f17fe1b898b8ef53d8047f9863142c3ab0751f60b4f00f40bc3b11e177dc6c9c3db95b4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.8
|
|
4
|
+
|
|
5
|
+
- Contacts index: paginated search (total count, previous/next, page indicator) with Elasticsearch and database backends.
|
|
6
|
+
- `POST /google_sync_unsynced` and **Sync … not yet in Google** button when sync is enabled; `GoogleSyncUnsyncedJob` runs `SyncService#sync_unsynced!` for contacts with no `google_resource_name`.
|
|
7
|
+
|
|
3
8
|
## 0.1.7
|
|
4
9
|
|
|
5
10
|
- Google People API: `updateContact` sends required `updatePersonFields`; update payloads include `resourceName` and `etag`; sync preloads emails, phones, and addresses.
|
|
@@ -10,10 +10,30 @@ module Rails
|
|
|
10
10
|
|
|
11
11
|
def index
|
|
12
12
|
@query = params[:q].to_s.strip
|
|
13
|
-
|
|
13
|
+
result = Search::Query.new(
|
|
14
|
+
@query,
|
|
15
|
+
filters: normalized_filters(filter_params),
|
|
16
|
+
page: params[:page]
|
|
17
|
+
).call
|
|
18
|
+
@contacts = result.records
|
|
19
|
+
@total_count = result.total_count
|
|
20
|
+
@page = result.page
|
|
21
|
+
@per_page = result.per_page
|
|
22
|
+
@total_pages = result.total_pages
|
|
23
|
+
@google_contacts_pending_sync = google_pending_sync_count
|
|
14
24
|
render "rails/contact/index"
|
|
15
25
|
end
|
|
16
26
|
|
|
27
|
+
def google_sync_unsynced
|
|
28
|
+
unless Rails::Contact.configuration.google_sync_enabled
|
|
29
|
+
redirect_to contacts_path, alert: "Google Contacts sync is disabled."
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
GoogleSyncUnsyncedJob.perform_later
|
|
34
|
+
redirect_to contacts_path, notice: "Google sync has been queued for contacts not yet linked."
|
|
35
|
+
end
|
|
36
|
+
|
|
17
37
|
def show
|
|
18
38
|
render "rails/contact/show"
|
|
19
39
|
end
|
|
@@ -99,6 +119,12 @@ module Rails
|
|
|
99
119
|
params.permit(:city, :region, :sync_eligible, :starred)
|
|
100
120
|
end
|
|
101
121
|
|
|
122
|
+
def google_pending_sync_count
|
|
123
|
+
return 0 unless Rails::Contact.configuration.google_sync_enabled
|
|
124
|
+
|
|
125
|
+
Contact.where("google_resource_name IS NULL OR google_resource_name = ?", "").count
|
|
126
|
+
end
|
|
127
|
+
|
|
102
128
|
def enqueue_index(contact_id)
|
|
103
129
|
IndexContactJob.perform_later(contact_id)
|
|
104
130
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<% if defined?(@total_count) && @total_count.to_i > 0 %>
|
|
2
|
+
<div class="flex flex-col gap-3 border-t border-gray-200 bg-gray-50 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
3
|
+
<p class="text-sm text-gray-600">
|
|
4
|
+
<% start_i = (@page - 1) * @per_page + 1 %>
|
|
5
|
+
<% end_i = [ @page * @per_page, @total_count ].min %>
|
|
6
|
+
Showing <span class="font-medium text-gray-900"><%= start_i %>–<%= end_i %></span>
|
|
7
|
+
of <span class="font-medium text-gray-900"><%= number_with_delimiter(@total_count) %></span>
|
|
8
|
+
<% if @per_page.present? && @per_page > 0 %>
|
|
9
|
+
<span class="text-gray-500">(<%= @per_page %> per page)</span>
|
|
10
|
+
<% end %>
|
|
11
|
+
</p>
|
|
12
|
+
<% if defined?(@total_pages) && @total_pages > 1 %>
|
|
13
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
14
|
+
<% qp = request.query_parameters.except(:page, :action, :controller) %>
|
|
15
|
+
<% if @page > 1 %>
|
|
16
|
+
<%= link_to "Previous", url_for(qp.merge(page: @page - 1)), class: "inline-flex rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50" %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<span class="text-sm text-gray-600">Page <%= @page %> / <%= @total_pages %></span>
|
|
19
|
+
<% if @page < @total_pages %>
|
|
20
|
+
<%= link_to "Next", url_for(qp.merge(page: @page + 1)), class: "inline-flex rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50" %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
@@ -26,6 +26,22 @@
|
|
|
26
26
|
<% end %>
|
|
27
27
|
</div>
|
|
28
28
|
|
|
29
|
+
<% if Rails::Contact.configuration.google_sync_enabled %>
|
|
30
|
+
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
|
31
|
+
<h2 class="mb-2 text-sm font-semibold text-gray-900">Google Contacts</h2>
|
|
32
|
+
<% if @google_contacts_pending_sync.to_i.positive? %>
|
|
33
|
+
<%= form_with url: google_sync_unsynced_contacts_path, method: :post, local: true, class: "flex flex-wrap items-center gap-3" do %>
|
|
34
|
+
<%= submit_tag "Sync #{@google_contacts_pending_sync} contact#{'s' if @google_contacts_pending_sync != 1} not yet in Google",
|
|
35
|
+
class: "inline-flex rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700",
|
|
36
|
+
data: { confirm: "Queue Google sync for #{@google_contacts_pending_sync} contact(s)? This runs in the background." } %>
|
|
37
|
+
<% end %>
|
|
38
|
+
<p class="mt-2 text-xs text-gray-500">Creates or updates People entries for contacts with no Google link yet. Already-linked contacts are unchanged.</p>
|
|
39
|
+
<% else %>
|
|
40
|
+
<p class="text-sm text-gray-600">Every contact in the database is already linked to Google (or there are no contacts).</p>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
29
45
|
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
|
30
46
|
<%= form_with url: bulk_destroy_contacts_path, method: :post, local: true, class: "inline-flex items-center gap-2" do |f| %>
|
|
31
47
|
<%= hidden_field_tag :ids, "", id: "bulk-ids-field" %>
|
|
@@ -76,6 +92,7 @@
|
|
|
76
92
|
<% end %>
|
|
77
93
|
</tbody>
|
|
78
94
|
</table>
|
|
95
|
+
<%= render "rails/contact/index_pagination" %>
|
|
79
96
|
</div>
|
|
80
97
|
</div>
|
|
81
98
|
|
data/config/routes.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Rails::Contact::Engine.routes.draw do
|
|
2
2
|
post "/bulk_destroy", to: "contacts#bulk_destroy", as: :bulk_destroy_contacts
|
|
3
3
|
post "/merge", to: "contacts#merge", as: :merge_contacts
|
|
4
|
+
post "/google_sync_unsynced", to: "contacts#google_sync_unsynced", as: :google_sync_unsynced_contacts
|
|
4
5
|
get "/", to: "contacts#index", as: :contacts
|
|
5
6
|
get "/new", to: "contacts#new", as: :new_contact
|
|
6
7
|
post "/", to: "contacts#create"
|
|
@@ -13,6 +13,22 @@ module Rails
|
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
def unsynced_contacts_scope
|
|
17
|
+
Rails::Contact::Contact.where("google_resource_name IS NULL OR google_resource_name = ?", "")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def sync_unsynced!
|
|
21
|
+
unsynced_contacts_scope.in_batches(of: 100) do |batch|
|
|
22
|
+
batch.preload(:emails, :phones, :addresses).each do |contact|
|
|
23
|
+
sync_contact(contact)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
Rails.logger.error(
|
|
26
|
+
"[Rails::Contact] Google sync_unsynced failed for contact #{contact.id}: #{e.class}: #{e.message}"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
16
32
|
def sync_contact(contact)
|
|
17
33
|
payload = PayloadMapper.new(contact).to_people_payload
|
|
18
34
|
response = if contact.google_resource_name.present?
|
|
@@ -3,13 +3,19 @@ module Rails
|
|
|
3
3
|
module Search
|
|
4
4
|
module Backends
|
|
5
5
|
class Database
|
|
6
|
-
def search(query, filters)
|
|
6
|
+
def search(query, filters, page:, per_page:)
|
|
7
|
+
offset = (page - 1) * per_page
|
|
7
8
|
scope = Contact.includes(:emails, :phones, :labels).recent_first
|
|
8
9
|
scope = apply_filters(scope, filters)
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
if query.blank?
|
|
12
|
+
total = scope.count
|
|
13
|
+
records = scope.offset(offset).limit(per_page).to_a
|
|
14
|
+
return Search::Result.new(records: records, total_count: total, page: page, per_page: per_page)
|
|
15
|
+
end
|
|
10
16
|
|
|
11
17
|
wildcard = "%#{query.downcase}%"
|
|
12
|
-
scope.left_joins(:emails, :phones, :labels).where(
|
|
18
|
+
filtered = scope.left_joins(:emails, :phones, :labels).where(
|
|
13
19
|
"LOWER(rails_contact_contacts.given_name) LIKE :q OR "\
|
|
14
20
|
"LOWER(rails_contact_contacts.family_name) LIKE :q OR "\
|
|
15
21
|
"LOWER(COALESCE(rails_contact_contacts.metadata->>'company', '')) LIKE :q OR "\
|
|
@@ -19,7 +25,10 @@ module Rails
|
|
|
19
25
|
"LOWER(rails_contact_labels.name) LIKE :q",
|
|
20
26
|
q: wildcard,
|
|
21
27
|
raw: "%#{query}%"
|
|
22
|
-
).distinct
|
|
28
|
+
).distinct
|
|
29
|
+
total = filtered.count(:id)
|
|
30
|
+
records = filtered.offset(offset).limit(per_page).to_a
|
|
31
|
+
Search::Result.new(records: records, total_count: total, page: page, per_page: per_page)
|
|
23
32
|
end
|
|
24
33
|
|
|
25
34
|
private
|
|
@@ -34,10 +43,6 @@ module Rails
|
|
|
34
43
|
end
|
|
35
44
|
scoped
|
|
36
45
|
end
|
|
37
|
-
|
|
38
|
-
def limit
|
|
39
|
-
Rails::Contact.configuration.default_per_page
|
|
40
|
-
end
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
end
|
|
@@ -23,13 +23,16 @@ module Rails
|
|
|
23
23
|
@client = client || self.class.default_client
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def search(query, filters)
|
|
27
|
-
response = @client.search(index: INDEX, body: search_body(query, filters))
|
|
28
|
-
|
|
26
|
+
def search(query, filters, page:, per_page:)
|
|
27
|
+
response = @client.search(index: INDEX, body: search_body(query, filters, page: page, per_page: per_page))
|
|
28
|
+
hits = response.fetch("hits", {})
|
|
29
|
+
ids = hits.fetch("hits", []).map { |doc| doc["_id"] }
|
|
30
|
+
total_count = extract_total_hits(hits["total"])
|
|
29
31
|
records_by_id = Contact.where(id: ids).index_by { |record| record.id.to_s }
|
|
30
|
-
ids.filter_map { |id| records_by_id[id.to_s] }
|
|
32
|
+
records = ids.filter_map { |id| records_by_id[id.to_s] }
|
|
33
|
+
Search::Result.new(records: records, total_count: total_count, page: page, per_page: per_page)
|
|
31
34
|
rescue StandardError
|
|
32
|
-
Search::Backends::Database.new.search(query, filters)
|
|
35
|
+
Search::Backends::Database.new.search(query, filters, page: page, per_page: per_page)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def upsert(contact)
|
|
@@ -81,9 +84,12 @@ module Rails
|
|
|
81
84
|
}
|
|
82
85
|
end
|
|
83
86
|
|
|
84
|
-
def search_body(query, filters)
|
|
87
|
+
def search_body(query, filters, page:, per_page:)
|
|
88
|
+
from = (page - 1) * per_page
|
|
85
89
|
{
|
|
86
|
-
|
|
90
|
+
from: from,
|
|
91
|
+
size: per_page,
|
|
92
|
+
track_total_hits: true,
|
|
87
93
|
sort: [ { updated_at: { order: "desc" } }, { id: { order: "desc" } } ],
|
|
88
94
|
query: {
|
|
89
95
|
bool: {
|
|
@@ -94,6 +100,12 @@ module Rails
|
|
|
94
100
|
}
|
|
95
101
|
end
|
|
96
102
|
|
|
103
|
+
def extract_total_hits(raw)
|
|
104
|
+
return 0 if raw.nil?
|
|
105
|
+
|
|
106
|
+
raw.is_a?(Hash) ? raw.fetch("value", 0).to_i : raw.to_i
|
|
107
|
+
end
|
|
108
|
+
|
|
97
109
|
def search_clause(query)
|
|
98
110
|
return [ { match_all: {} } ] if query.blank?
|
|
99
111
|
|
|
@@ -2,13 +2,17 @@ module Rails
|
|
|
2
2
|
module Contact
|
|
3
3
|
module Search
|
|
4
4
|
class Query
|
|
5
|
-
|
|
5
|
+
MAX_PER_PAGE = 100
|
|
6
|
+
|
|
7
|
+
def initialize(query, filters: {}, page: nil, per_page: nil)
|
|
6
8
|
@query = query
|
|
7
9
|
@filters = filters.to_h.compact_blank
|
|
10
|
+
@page = page.to_i < 1 ? 1 : page.to_i
|
|
11
|
+
@per_page = resolve_per_page(per_page)
|
|
8
12
|
end
|
|
9
13
|
|
|
10
14
|
def call
|
|
11
|
-
backend.search(@query, @filters)
|
|
15
|
+
backend.search(@query, @filters, page: @page, per_page: @per_page)
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
private
|
|
@@ -21,6 +25,12 @@ module Rails
|
|
|
21
25
|
Search::Backends::Database.new
|
|
22
26
|
end
|
|
23
27
|
end
|
|
28
|
+
|
|
29
|
+
def resolve_per_page(per_page)
|
|
30
|
+
p = per_page.to_i
|
|
31
|
+
p = Rails::Contact.configuration.default_per_page if p <= 0
|
|
32
|
+
[ p, MAX_PER_PAGE ].min
|
|
33
|
+
end
|
|
24
34
|
end
|
|
25
35
|
end
|
|
26
36
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Contact
|
|
3
|
+
module Search
|
|
4
|
+
Result = Struct.new(:records, :total_count, :page, :per_page, keyword_init: true) do
|
|
5
|
+
def total_pages
|
|
6
|
+
return 0 if total_count.zero? || per_page.zero?
|
|
7
|
+
|
|
8
|
+
(total_count.to_f / per_page).ceil
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/rails/contact.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "rails/contact/engine"
|
|
|
4
4
|
require "rails/contact/configuration"
|
|
5
5
|
require "rails/contact/orm"
|
|
6
6
|
require "rails/contact/routing"
|
|
7
|
+
require "rails/contact/search/result"
|
|
7
8
|
require "rails/contact/search/query"
|
|
8
9
|
require "rails/contact/search/backends/database"
|
|
9
10
|
require "rails/contact/search/backends/elasticsearch"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-contact
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kshitiz Sinha
|
|
@@ -119,6 +119,7 @@ files:
|
|
|
119
119
|
- app/helpers/rails/contact/application_helper.rb
|
|
120
120
|
- app/jobs/rails/contact/application_job.rb
|
|
121
121
|
- app/jobs/rails/contact/google_sync_job.rb
|
|
122
|
+
- app/jobs/rails/contact/google_sync_unsynced_job.rb
|
|
122
123
|
- app/jobs/rails/contact/index_contact_job.rb
|
|
123
124
|
- app/mailers/rails/contact/application_mailer.rb
|
|
124
125
|
- app/mailers/rails/contact/contact_mailer.rb
|
|
@@ -137,6 +138,7 @@ files:
|
|
|
137
138
|
- app/views/rails/contact/_email_fields.html.erb
|
|
138
139
|
- app/views/rails/contact/_event_fields.html.erb
|
|
139
140
|
- app/views/rails/contact/_form.html.erb
|
|
141
|
+
- app/views/rails/contact/_index_pagination.html.erb
|
|
140
142
|
- app/views/rails/contact/_nested_fields_script.html.erb
|
|
141
143
|
- app/views/rails/contact/_phone_fields.html.erb
|
|
142
144
|
- app/views/rails/contact/_stylesheet.html.erb
|
|
@@ -181,6 +183,7 @@ files:
|
|
|
181
183
|
- lib/rails/contact/search/backends/database.rb
|
|
182
184
|
- lib/rails/contact/search/backends/elasticsearch.rb
|
|
183
185
|
- lib/rails/contact/search/query.rb
|
|
186
|
+
- lib/rails/contact/search/result.rb
|
|
184
187
|
- lib/rails/contact/version.rb
|
|
185
188
|
- lib/tasks/rails/contact_tasks.rake
|
|
186
189
|
- spec/concerns/filterable_spec.rb
|