Extract tag serializer to its own file

This commit is contained in:
Eugene Burmakin
2025-11-19 19:33:28 +01:00
parent 449884796f
commit 1d07eb652d
11 changed files with 121 additions and 2990 deletions

View File

@@ -1,171 +0,0 @@
# Layer Control Upgrade - Leaflet.Control.Layers.Tree
## Summary
Successfully installed and integrated the `Leaflet.Control.Layers.Tree` plugin to replace the standard Leaflet layer control with a hierarchical tree-based control that better organizes map layers and styles.
## Changes Made
### 1. Installation
- **Plugin**: Installed `leaflet.control.layers.tree` via importmap
- **CSS**: Added plugin CSS file at `app/assets/stylesheets/leaflet.control.layers.tree.css`
### 2. Maps Controller Updates
#### File: `app/javascript/controllers/maps_controller.js`
**Import Changes:**
- Added import for `leaflet.control.layers.tree`
- Removed import for `createPlacesControl` (now integrated into tree control)
**Initialization Changes:**
- Removed standalone Places control initialization
- Added `this.userTags` property to store user tags for places filtering
- Updated layer control initialization to use `createTreeLayerControl()`
**New Methods:**
1. **`createTreeLayerControl(additionalLayers = {})`**
- Creates a hierarchical tree structure for map layers
- Organizes layers into two main groups:
- **Map Styles**: All available base map layers
- **Layers**: All overlay layers with nested groups
- Supports dynamic additional layers (e.g., Family Members)
Structure:
```
+ Map Styles
- OpenStreetMap
- OpenStreetMap.HOT
- ...
+ Layers
- Points
- Routes
- Tracks
- Heatmap
- Fog of War
- Scratch map
- Areas
- Photos
+ Visits
- Suggested
- Confirmed
+ Places
- All
- Untagged
- (each tag with icon)
```
**Updated Methods:**
- **`updateLayerControl()`**: Simplified to just recreate the tree control with additional layers
- Updated all layer control recreations throughout the file to use `createTreeLayerControl()`
### 3. Places Manager Updates
#### File: `app/javascript/maps/places.js`
**New Methods:**
1. **`createFilteredLayer(tagIds)`**
- Creates a layer group for filtered places
- Returns a layer that loads places when added to the map
- Supports tag-based and untagged filtering
2. **`loadPlacesIntoLayer(layer, tagIds)`**
- Loads places into a specific layer with tag filtering
- Handles API calls with tag_ids or untagged parameters
- Creates markers using existing `createPlaceMarker()` method
## Features
### Hierarchical Organization
- Map styles and layers are now clearly separated
- Related layers are grouped together (Visits, Places)
- Easy to expand/collapse sections
### Places Layer Integration
- No longer needs a separate control
- All places filters are now in the tree control
- Each tag gets its own layer in the tree
- Places group has "All", "Untagged", and individual tag layers regardless of tags
- "Untagged" shows only places without tags
### Dynamic Layer Support
- Family Members layer can be added dynamically
- Additional layers can be easily integrated
- Maintains compatibility with existing layer management
### Improved User Experience
- Cleaner UI with collapsible sections
- Better organization of many layers
- Consistent interface for all layer types
- Select All checkbox for grouped layers (Visits, Places)
## API Changes
### Places API
The Places API now supports an `untagged` parameter:
- `GET /api/v1/places?untagged=true` - Returns only untagged places
- `GET /api/v1/places?tag_ids=1,2,3` - Returns places with specified tags
## Testing Recommendations
1. **Basic Functionality**
- Verify all map styles load correctly
- Test all overlay layers (Points, Routes, Tracks, etc.)
- Confirm layer visibility persists correctly
2. **Places Integration**
- Test "All" layer shows all places
- Verify "Untagged" layer shows only untagged places
- Test individual tag layers show correct places
- Confirm places load when layer is enabled
3. **Visits Integration**
- Test Suggested and Confirmed visits layers
- Verify visits load correctly when enabled
4. **Family Members**
- Test Family Members layer appears when family is available
- Verify layer updates when family locations change
5. **Layer State Persistence**
- Verify enabled layers are saved to user settings
- Confirm layer state is restored on page load
## Migration Notes
### Removed Components
- Standalone Places control button (📍)
- `createPlacesControl` function no longer used in maps_controller
### Behavioral Changes
- Places layer is no longer managed by a separate control
- All places filtering is now done through the layer control
- Places markers are created on-demand when layer is enabled
## Future Enhancements
1. **Layer Icons**: Add custom icons for each layer type
2. **Layer Counts**: Show number of items in each layer
3. **Custom Styling**: Theme the tree control to match app theme
4. **Layer Search**: Add search functionality for finding layers
5. **Layer Presets**: Allow saving custom layer combinations
## Files Modified
1. `app/javascript/controllers/maps_controller.js` - Main map controller
2. `app/javascript/maps/places.js` - Places manager with new filtering methods
3. `config/importmap.rb` - Added tree control import (via bin/importmap)
4. `app/assets/stylesheets/leaflet.control.layers.tree.css` - Plugin CSS
## Rollback Plan
If needed, to rollback:
1. Remove `import "leaflet.control.layers.tree"` from maps_controller.js
2. Restore `import { createPlacesControl }` from places_control
3. Revert `createTreeLayerControl()` to `L.control.layers()`
4. Restore Places control initialization
5. Remove `leaflet.control.layers.tree` from importmap
6. Remove CSS file

View File

@@ -1,141 +0,0 @@
# Places Integration Checklist
## Files Modified:
-`app/javascript/controllers/stat_page_controller.js` - Added PlacesManager integration
-`app/javascript/maps/places.js` - Fixed API authentication headers
-`app/views/stats/_month.html.erb` - Added Places button and tag filters
-`app/views/shared/_place_creation_modal.html.erb` - Already exists
## What Should Appear:
### On Monthly Stats Page (`/stats/YYYY/MM`):
1. **Map Controls** (top right of map):
- [ ] "Heatmap" button
- [ ] "Points" button
- [ ] **"Places" button** ← NEW!
2. **Below the Map**:
- [ ] **"Filter Places by Tags"** section ← NEW!
- [ ] Checkboxes for each tag you've created
- [ ] Each checkbox shows: icon + name + color dot
## Troubleshooting Steps:
### Step 1: Restart Server
```bash
# Stop server (Ctrl+C)
bundle exec rails server
# Or with Docker:
docker-compose restart web
```
### Step 2: Hard Refresh Browser
- Mac: `Cmd + Shift + R`
- Windows/Linux: `Ctrl + Shift + R`
### Step 3: Check Browser Console
1. Open Developer Tools (F12)
2. Go to Console tab
3. Look for errors (red text)
4. You should see: "StatPage controller connected"
### Step 4: Verify URL
Make sure you're on a monthly stats page:
-`/stats/2024/11` ← Correct
-`/stats` ← Wrong (main stats index)
-`/stats/2024` ← Wrong (yearly stats)
### Step 5: Check JavaScript Loading
In browser console, type:
```javascript
console.log(document.querySelector('[data-controller="stat-page"]'))
```
Should show the element, not null.
### Step 6: Verify Controller Registration
In browser console:
```javascript
console.log(application.controllers)
```
Should include "stat-page" in the list.
## Expected Behavior:
### When You Click "Places" Button:
1. Places layer toggles on/off
2. Button highlights when active
3. Map shows custom markers with tag icons
### When You Check Tag Filters:
1. Map updates immediately
2. Shows only places with selected tags
3. Unchecking all shows all places
## If Nothing Shows:
### Check if you have any places created:
```bash
bundle exec rails console
# In console:
user = User.find_by(email: 'your@email.com')
user.places.count # Should be > 0
user.tags.count # Should be > 0
```
### Create test data:
```bash
bundle exec rails console
user = User.first
tag = user.tags.create!(name: "Test", icon: "📍", color: "#FF5733")
# Create via API or console:
place = user.places.create!(
name: "Test Place",
latitude: 40.7128,
longitude: -74.0060,
source: :manual
)
place.tags << tag
```
## Verification Script:
Run this in Rails console to verify everything:
```ruby
user = User.first
puts "Tags: #{user.tags.count}"
puts "Places: #{user.places.count}"
puts "Places with tags: #{user.places.joins(:tags).distinct.count}"
if user.tags.any?
puts "\nYour tags:"
user.tags.each do |tag|
puts " #{tag.icon} #{tag.name} (#{tag.places.count} places)"
end
end
if user.places.any?
puts "\nYour places:"
user.places.limit(5).each do |place|
puts " #{place.name} at (#{place.latitude}, #{place.longitude})"
puts " Tags: #{place.tags.map(&:name).join(', ')}"
end
end
```
## Still Having Issues?
Check these files exist and have the right content:
- `app/javascript/maps/places.js` - Should export PlacesManager class
- `app/javascript/controllers/stat_page_controller.js` - Should import PlacesManager
- `app/views/stats/_month.html.erb` - Should have Places button at line ~73
Look for JavaScript errors in browser console that might indicate:
- Import/export issues
- Syntax errors
- Missing dependencies

View File

@@ -1,194 +0,0 @@
# Layer Control Upgrade - Testing Checklist
## Pre-Testing Setup
1. **Start the development server**
```bash
bin/dev
```
2. **Clear browser cache** to ensure new JavaScript and CSS are loaded
3. **Log in** to the application with demo credentials or your account
4. **Navigate to the Map page** (`/map`)
## Visual Verification
- [ ] Layer control appears in the top-right corner
- [ ] Layer control shows a hierarchical tree structure (not flat list)
- [ ] Control has two main sections: "Map Styles" and "Layers"
- [ ] Sections can be expanded/collapsed
- [ ] No standalone Places control button (📍) is visible
## Map Styles Testing
- [ ] Expand "Map Styles" section
- [ ] All map styles are listed (OpenStreetMap, OpenStreetMap.HOT, etc.)
- [ ] Selecting a different style changes the base map
- [ ] Only one map style can be selected at a time
- [ ] Selected style is indicated with a radio button
## Layers Testing
### Basic Layers
- [ ] Expand "Layers" section
- [ ] All basic layers are present:
- [ ] Points
- [ ] Routes
- [ ] Tracks
- [ ] Heatmap
- [ ] Fog of War
- [ ] Scratch map
- [ ] Areas
- [ ] Photos
- [ ] Toggle each layer on/off
- [ ] Verify each layer displays correctly when enabled
- [ ] Multiple layers can be enabled simultaneously
### Visits Group
- [ ] Expand "Visits" section
- [ ] Two sub-layers are present:
- [ ] Suggested
- [ ] Confirmed
- [ ] Enable "Suggested" - suggested visits appear on map
- [ ] Enable "Confirmed" - confirmed visits appear on map
- [ ] Disable both - no visits visible on map
- [ ] Select All checkbox works for Visits group
### Places Group
- [ ] Expand "Places" section
- [ ] At least these options are present:
- [ ] Places (top-level checkbox)
- [ ] Untagged
- [ ] (Individual tags if any exist)
**Testing "Places (top-level checkbox)":**
- [ ] Enable "Places (top-level checkbox)"
- [ ] All places appear on map regardless of tags
- [ ] Place markers are clickable
- [ ] Place popups show correct information
**Testing "Untagged":**
- [ ] Enable "Untagged" (disable "Places (top-level checkbox)" first)
- [ ] Only places without tags appear
- [ ] Verify by checking places that have tags don't appear
**Testing Individual Tags:**
(If you have tags created)
- [ ] Each tag appears as a separate layer
- [ ] Tag icon is displayed before tag name
- [ ] Enable a tag layer
- [ ] Only places with that tag appear
- [ ] Multiple tag layers can be enabled simultaneously
- [ ] Select All checkbox works for Places group
### Family Members (if applicable)
- [ ] If in a family, "Family Members" layer appears
- [ ] Enable Family Members layer
- [ ] Family member locations appear on map
- [ ] Family member markers are distinguishable from own markers
## Functional Testing
### Layer Persistence
- [ ] Enable several layers (e.g., Points, Routes, Suggested Visits, Places (top-level checkbox))
- [ ] Refresh the page
- [ ] Verify enabled layers remain enabled after refresh
- [ ] Verify disabled layers remain disabled after refresh
### Places API Integration
- [ ] Open browser console (F12)
- [ ] Enable "Network" tab
- [ ] Enable "Untagged" places layer
- [ ] Verify API call: `GET /api/v1/places?api_key=...&untagged=true`
- [ ] Enable a tag layer
- [ ] Verify API call: `GET /api/v1/places?api_key=...&tag_ids=<tag_id>`
- [ ] Verify no JavaScript errors in console
### Layer Interaction
- [ ] Enable Routes layer
- [ ] Click on a route segment
- [ ] Verify route details popup appears
- [ ] Enable Places "Places (top-level checkbox)" layer
- [ ] Click on a place marker
- [ ] Verify place details popup appears
- [ ] Verify layers don't interfere with each other
### Performance
- [ ] Enable all layers simultaneously
- [ ] Map remains responsive
- [ ] No significant lag when toggling layers
- [ ] No memory leaks (check browser dev tools)
## Edge Cases
### No Tags Scenario
- [ ] If no tags exist, Places section should show:
- [ ] Places (top-level checkbox)
- [ ] Untagged
- [ ] No error in console
### No Places Scenario
- [ ] Disable all place layers
- [ ] Enable "Untagged"
- [ ] Verify appropriate message or empty state
- [ ] No errors in console
### No Family Scenario
- [ ] If not in a family, "Family Members" layer shouldn't appear
- [ ] No errors in console
## Regression Testing
### Existing Functionality
- [ ] Routes/Tracks selector still works (if visible with `tracks_debug=true`)
- [ ] Settings panel still works
- [ ] Calendar panel still works
- [ ] Visit selection tool still works
- [ ] Add visit button still works
### Other Controllers
- [ ] Family members controller still works (if applicable)
- [ ] Photo markers still load correctly
- [ ] Area drawing still works
- [ ] Fog of war updates correctly
## Mobile Testing (if applicable)
- [ ] Layer control is accessible on mobile
- [ ] Tree structure expands/collapses on tap
- [ ] Layers can be toggled on mobile
- [ ] No layout issues on small screens
## Error Scenarios
- [ ] Disconnect internet, try to load a layer that requires API call
- [ ] Verify appropriate error handling
- [ ] Verify user gets feedback about the failure
- [ ] Verify app doesn't crash
## Console Checks
Throughout all testing, monitor the browser console for:
- [ ] No JavaScript errors
- [ ] No unexpected warnings
- [ ] No failed API requests (except during error scenario testing)
- [ ] Appropriate log messages for debugging
## Sign-off
- [ ] All critical tests pass
- [ ] Any failures are documented
- [ ] Ready for production deployment
---
## Notes
Record any issues, unexpected behavior, or suggestions for improvement:
```
[Your notes here]
```

View File

@@ -6,23 +6,7 @@ module Api
def privacy_zones
zones = current_api_user.tags.privacy_zones.includes(:places)
render json: zones.map { |tag|
{
tag_id: tag.id,
tag_name: tag.name,
tag_icon: tag.icon,
tag_color: tag.color,
radius_meters: tag.privacy_radius_meters,
places: tag.places.map { |place|
{
id: place.id,
name: place.name,
latitude: place.latitude,
longitude: place.longitude
}
}
}
}
render json: zones.map { |tag| TagSerializer.new(tag).call }
end
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
# frozen_string_literal: true
class TagSerializer
def initialize(tag)
@tag = tag
end
def call
{
tag_id: tag.id,
tag_name: tag.name,
tag_icon: tag.icon,
tag_color: tag.color,
radius_meters: tag.privacy_radius_meters,
places: places
}
end
private
attr_reader :tag
def places
tag.places.map do |place|
{
id: place.id,
name: place.name,
latitude: place.latitude.to_f,
longitude: place.longitude.to_f
}
end
end
end

View File

@@ -1,5 +1,6 @@
class AddUserIdToPlaces < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def up
# Add nullable for backward compatibility, will enforce later via data migration
add_reference :places, :user, null: true, index: {algorithm: :concurrently} unless foreign_key_exists?(:places, :users)

View File

@@ -14,6 +14,8 @@ RSpec.describe User, type: :model do
it { is_expected.to have_many(:places).through(:visits) }
it { is_expected.to have_many(:trips).dependent(:destroy) }
it { is_expected.to have_many(:tracks).dependent(:destroy) }
it { is_expected.to have_many(:tags).dependent(:destroy) }
it { is_expected.to have_many(:visited_places).through(:visits) }
end
describe 'enums' do

View File

@@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Api::V1::Tags', type: :request do
let(:user) { create(:user) }
let(:tag) { create(:tag, user: user, name: 'Home', icon: '🏠', color: '#4CAF50', privacy_radius_meters: 500) }
let!(:place) { create(:place, name: 'My Place', latitude: 10.0, longitude: 20.0) }
before do
tag.places << place
end
describe 'GET /api/v1/tags/privacy_zones' do
context 'when authenticated' do
before do
user.create_api_key unless user.api_key.present?
get privacy_zones_api_v1_tags_path, params: { api_key: user.api_key }
end
it 'returns success' do
expect(response).to be_successful
end
it 'returns the correct JSON structure' do
json_response = JSON.parse(response.body)
expect(json_response).to be_an(Array)
expect(json_response.first).to include(
'tag_id' => tag.id,
'tag_name' => 'Home',
'tag_icon' => '🏠',
'tag_color' => '#4CAF50',
'radius_meters' => 500
)
expect(json_response.first['places']).to be_an(Array)
expect(json_response.first['places'].first).to include(
'id' => place.id,
'name' => 'My Place',
'latitude' => 10.0,
'longitude' => 20.0
)
end
end
context 'when not authenticated' do
it 'returns unauthorized' do
get privacy_zones_api_v1_tags_path
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe TagSerializer do
let(:tag) { create(:tag, name: 'Home', icon: '🏠', color: '#4CAF50', privacy_radius_meters: 500) }
let!(:place) { create(:place, name: 'My Place', latitude: 10.0, longitude: 20.0) }
before do
tag.places << place
end
subject { described_class.new(tag).call }
it 'returns the correct JSON structure' do
expect(subject).to eq({
tag_id: tag.id,
tag_name: 'Home',
tag_icon: '🏠',
tag_color: '#4CAF50',
radius_meters: 500,
places: [
{
id: place.id,
name: 'My Place',
latitude: 10.0,
longitude: 20.0
}
]
})
end
end

View File

@@ -1,105 +0,0 @@
# Run with: bundle exec rails runner verify_places_integration.rb
puts "🔍 Verifying Places Integration..."
puts "=" * 50
# Check files exist
files_to_check = [
'app/javascript/maps/places.js',
'app/javascript/controllers/stat_page_controller.js',
'app/javascript/controllers/place_creation_controller.js',
'app/views/stats/_month.html.erb',
'app/views/shared/_place_creation_modal.html.erb'
]
puts "\n📁 Checking Files:"
files_to_check.each do |file|
if File.exist?(file)
puts "#{file}"
else
puts " ❌ MISSING: #{file}"
end
end
# Check view has our changes
puts "\n🎨 Checking View Changes:"
month_view = File.read('app/views/stats/_month.html.erb')
if month_view.include?('placesBtn')
puts " ✅ Places button found in view"
else
puts " ❌ Places button NOT found in view"
end
if month_view.include?('Filter Places by Tags')
puts " ✅ Tag filter section found in view"
else
puts " ❌ Tag filter section NOT found in view"
end
if month_view.include?('place_creation_modal')
puts " ✅ Place creation modal included"
else
puts " ❌ Place creation modal NOT included"
end
# Check JavaScript has our changes
puts "\n💻 Checking JavaScript Changes:"
controller_js = File.read('app/javascript/controllers/stat_page_controller.js')
if controller_js.include?('PlacesManager')
puts " ✅ PlacesManager imported"
else
puts " ❌ PlacesManager NOT imported"
end
if controller_js.include?('togglePlaces()')
puts " ✅ togglePlaces() method found"
else
puts " ❌ togglePlaces() method NOT found"
end
if controller_js.include?('filterPlacesByTags')
puts " ✅ filterPlacesByTags() method found"
else
puts " ❌ filterPlacesByTags() method NOT found"
end
# Check database
puts "\n🗄️ Checking Database:"
user = User.first
if user
puts " ✅ Found user: #{user.email}"
puts " Tags: #{user.tags.count}"
puts " Places: #{user.places.count}"
if user.tags.any?
puts "\n 📌 Your Tags:"
user.tags.limit(5).each do |tag|
puts " #{tag.icon} #{tag.name} (#{tag.places.count} places)"
end
else
puts " ⚠️ No tags created yet. Create some at /tags"
end
if user.places.any?
puts "\n 📍 Your Places:"
user.places.limit(5).each do |place|
puts " #{place.name} - #{place.tags.map(&:name).join(', ')}"
end
else
puts " ⚠️ No places created yet. Use the API or create via console."
end
else
puts " ❌ No users found"
end
puts "\n" + "=" * 50
puts "✅ Integration files are in place!"
puts "\n📋 Next Steps:"
puts " 1. Restart your Rails server"
puts " 2. Hard refresh your browser (Cmd+Shift+R)"
puts " 3. Navigate to /stats/#{Date.today.year}/#{Date.today.month}"
puts " 4. Look for 'Places' button next to 'Heatmap' and 'Points'"
puts " 5. Create tags at /tags if you haven't already"
puts " 6. Create places via API with those tags"