Implement live points count update for imports

This commit is contained in:
Eugene Burmakin 2024-11-04 13:06:04 +01:00
parent 9cc9632b6d
commit 4a3f7d5e65
11 changed files with 140 additions and 13 deletions

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class ImportsChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end

View file

@ -0,0 +1,15 @@
import consumer from "./consumer"
consumer.subscriptions.create("ImportsChannel", {
connected() {
console.log("Connected to the imports channel!");
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
// Called when there's incoming data on the websocket for this channel
}
});

View file

@ -1,3 +1,4 @@
// Import all the channels to be used by Action Cable
import "notifications_channel"
import "points_channel"
import "imports_channel"

View file

@ -0,0 +1,51 @@
import { Controller } from "@hotwired/stimulus";
import consumer from "../channels/consumer";
export default class extends Controller {
static targets = ["index"];
connect() {
console.log("Imports controller connected", {
hasIndexTarget: this.hasIndexTarget,
element: this.element,
userId: this.element.dataset.userId
});
this.setupSubscription();
}
setupSubscription() {
const userId = this.element.dataset.userId;
console.log("Setting up subscription with userId:", userId);
this.channel = consumer.subscriptions.create(
{ channel: "ImportsChannel" },
{
connected: () => {
console.log("Successfully connected to ImportsChannel");
// Test that we can receive messages
console.log("Subscription object:", this.channel);
},
disconnected: () => {
console.log("Disconnected from ImportsChannel");
},
received: (data) => {
console.log("Received data:", data);
const row = this.element.querySelector(`tr[data-import-id="${data.import.id}"]`);
if (row) {
const pointsCell = row.querySelector('[data-points-count]');
if (pointsCell) {
pointsCell.textContent = new Intl.NumberFormat().format(data.import.points_count);
}
}
}
}
);
}
disconnect() {
if (this.channel) {
this.channel.unsubscribe();
}
}
}

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Geojson::ImportParser
include Imports::Broadcaster
attr_reader :import, :json, :user_id
def initialize(import, user_id)
@ -12,10 +14,12 @@ class Geojson::ImportParser
def call
data = Geojson::Params.new(json).call
data.each do |point|
data.each.with_index(1) do |point, index|
next if point_exists?(point, user_id)
Point.create!(point.merge(user_id:, import_id: import.id))
broadcast_import_progress(import, index)
end
end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Gpx::TrackParser
include Imports::Broadcaster
attr_reader :import, :json, :user_id
def initialize(import, user_id)
@ -13,7 +15,9 @@ class Gpx::TrackParser
tracks = json['gpx']['trk']
tracks_arr = tracks.is_a?(Array) ? tracks : [tracks]
tracks_arr.map { parse_track(_1) }.flatten
tracks_arr.map { parse_track(_1) }.flatten.each.with_index(1) do |point, index|
create_point(point, index)
end
end
private
@ -22,12 +26,10 @@ class Gpx::TrackParser
segments = track['trkseg']
segments_array = segments.is_a?(Array) ? segments : [segments]
segments_array.map do |segment|
segment['trkpt'].each { create_point(_1) }
end
segments_array.map { |segment| segment['trkpt'] }
end
def create_point(point)
def create_point(point, index)
return if point['lat'].blank? || point['lon'].blank? || point['time'].blank?
return if point_exists?(point)
@ -40,6 +42,8 @@ class Gpx::TrackParser
raw_data: point,
user_id:
)
broadcast_import_progress(import, index)
end
def point_exists?(point)

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Imports::Broadcaster
def broadcast_import_progress(import, index)
ImportsChannel.broadcast_to(
import.user,
{
action: 'update',
import: {
id: import.id,
points_count: index
}
}
)
end
end

View file

@ -39,18 +39,23 @@
<th>Created at</th>
</tr>
</thead>
<tbody>
<tbody
data-controller="imports"
data-imports-target="index"
data-user-id="<%= current_user.id %>"
>
<% @imports.each do |import| %>
<tr>
<tr data-import-id="<%= import.id %>" id="import-<%= import.id %>">
<td>
<%= link_to import.name, import, class: 'underline hover:no-underline' %> (<%= import.source %>)
&nbsp
<%= link_to import.name, import, class: 'underline hover:no-underline' %>
(<%= import.source %>)
&nbsp;
<%= link_to '🗺️', map_path(import_id: import.id) %>
&nbsp
&nbsp;
<%= link_to '📋', points_path(import_id: import.id) %>
</td>
<td>
<%= "#{number_with_delimiter import.points_count}" %>
<td data-points-count>
<%= number_with_delimiter import.points_count %>
</td>
<td><%= import.created_at.strftime("%d.%m.%Y, %H:%M") %></td>
</tr>

View file

@ -21,3 +21,4 @@ pin '@rails/actioncable', to: 'actioncable.esm.js'
pin_all_from 'app/javascript/channels', under: 'channels'
pin 'notifications_channel', to: 'channels/notifications_channel.js'
pin 'points_channel', to: 'channels/points_channel.js'
pin 'imports_channel', to: 'channels/imports_channel.js'

View file

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe ImportsChannel, type: :channel do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -16,6 +16,12 @@ RSpec.describe Gpx::TrackParser do
it 'creates points' do
expect { parser }.to change { Point.count }.by(301)
end
it 'broadcasts importing progress' do
expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(301).times
parser
end
end
context 'when file has multiple segments' do
@ -24,6 +30,12 @@ RSpec.describe Gpx::TrackParser do
it 'creates points' do
expect { parser }.to change { Point.count }.by(558)
end
it 'broadcasts importing progress' do
expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(558).times
parser
end
end
end
@ -33,6 +45,12 @@ RSpec.describe Gpx::TrackParser do
it 'creates points' do
expect { parser }.to change { Point.count }.by(407)
end
it 'broadcasts importing progress' do
expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(407).times
parser
end
end
end
end