Fix tag layers restoration and filtering logic

This commit is contained in:
Eugene Burmakin 2025-11-23 23:16:37 +01:00
parent 289a2a5373
commit f447039bbe
7 changed files with 84 additions and 79 deletions

View file

@ -65,7 +65,7 @@ module Api
private private
def set_place def set_place
@place = current_api_user.places.find(params[:id]) @place = current_api_user.places.includes(:tags, :visits).find(params[:id])
end end
def place_params def place_params

View file

@ -636,6 +636,21 @@ export default class extends BaseController {
// Add event listeners for overlay layer changes to keep routes/tracks selector in sync // Add event listeners for overlay layer changes to keep routes/tracks selector in sync
this.map.on('overlayadd', (event) => { this.map.on('overlayadd', (event) => {
// Track place tag layer restoration
if (this.isRestoringLayers && event.layer && this.placesFilteredLayers) {
// Check if this is a place tag layer being restored
const isPlaceTagLayer = Object.values(this.placesFilteredLayers).includes(event.layer);
if (isPlaceTagLayer && this.restoredPlaceTagLayers !== undefined) {
const tagId = event.layer._placeTagId;
this.restoredPlaceTagLayers.add(tagId);
// Check if all expected place tag layers have been restored
if (this.restoredPlaceTagLayers.size >= this.expectedPlaceTagLayerCount) {
this.isRestoringLayers = false;
}
}
}
// Save enabled layers whenever a layer is added (unless we're restoring from settings) // Save enabled layers whenever a layer is added (unless we're restoring from settings)
if (!this.isRestoringLayers) { if (!this.isRestoringLayers) {
this.saveEnabledLayers(); this.saveEnabledLayers();
@ -1784,42 +1799,30 @@ export default class extends BaseController {
} }
}); });
// Handle place tag layers (format: "place_tag:ID" or "place_tag:untagged") // Place tag layers will be restored by updateTreeControlCheckboxes
const placeTagLayers = enabledLayers.filter(key => key.startsWith('place_tag:')); // which triggers the tree control's change events to properly add/remove layers
if (placeTagLayers.length > 0) {
// Set flag once before restoring all place tag layers
this.isRestoringLayers = true;
placeTagLayers.forEach(layerKey => { // Track expected place tag layers to be restored
const tagId = layerKey.replace('place_tag:', ''); const expectedPlaceTagLayers = enabledLayers.filter(key => key.startsWith('place_tag:'));
let layer; this.restoredPlaceTagLayers = new Set();
this.expectedPlaceTagLayerCount = expectedPlaceTagLayers.length;
if (tagId === 'untagged') { // Set flag to prevent saving during layer restoration
// Find untagged layer this.isRestoringLayers = true;
layer = Object.values(this.placesFilteredLayers || {}).find(l => l._placeTagId === 'untagged');
} else {
// Find layer by tag ID
const tagIdNum = parseInt(tagId);
layer = Object.values(this.placesFilteredLayers || {}).find(l => l._placeTagId === tagIdNum);
}
if (layer && !this.map.hasLayer(layer)) {
layer.addTo(this.map);
console.log(`Enabled place tag layer: ${tagId}`);
}
});
// Reset flag after all layers have been added and events processed
setTimeout(() => {
this.isRestoringLayers = false;
console.log('[initializeLayersFromSettings] Finished restoring place tag layers');
}, 150);
}
// Update the tree control checkboxes to reflect the layer states // Update the tree control checkboxes to reflect the layer states
// The tree control will handle adding/removing layers when checkboxes change
// Wait a bit for the tree control to be fully initialized // Wait a bit for the tree control to be fully initialized
setTimeout(() => { setTimeout(() => {
this.updateTreeControlCheckboxes(enabledLayers); this.updateTreeControlCheckboxes(enabledLayers);
// Set a fallback timeout in case not all layers get added
setTimeout(() => {
if (this.isRestoringLayers) {
console.warn('[initializeLayersFromSettings] Timeout reached, forcing restoration complete');
this.isRestoringLayers = false;
}
}, 2000);
}, 200); }, 200);
} }
@ -1849,19 +1852,30 @@ export default class extends BaseController {
// Check if this is a standard layer // Check if this is a standard layer
let shouldBeEnabled = enabledLayers.includes(layerName); let shouldBeEnabled = enabledLayers.includes(layerName);
// Check if this is a place tag layer by finding the layer object // Also check if this is a place tag layer
if (!shouldBeEnabled && this.placesFilteredLayers) { let placeLayer = null;
const placeLayer = this.placesFilteredLayers[layerName]; if (this.placesFilteredLayers) {
placeLayer = this.placesFilteredLayers[layerName];
if (placeLayer && placeLayer._placeTagId !== undefined) { if (placeLayer && placeLayer._placeTagId !== undefined) {
shouldBeEnabled = enabledTagIds.has(placeLayer._placeTagId); // This is a place tag layer - check if it should be enabled
const placeLayerEnabled = enabledTagIds.has(placeLayer._placeTagId);
if (placeLayerEnabled) {
shouldBeEnabled = true;
}
} }
} }
// Skip group headers that might have checkboxes // Skip group headers that might have checkboxes
if (layerName && !layerName.includes('Map Styles') && !layerName.includes('Layers')) { if (layerName && !layerName.includes('Map Styles') && !layerName.includes('Layers')) {
if (shouldBeEnabled !== input.checked) { if (shouldBeEnabled !== input.checked) {
input.checked = shouldBeEnabled; // Checkbox state needs to change - simulate a click to trigger tree control
console.log(`Updated checkbox for ${layerName}: ${shouldBeEnabled}`); // The tree control listens for click events, not change events
input.click();
} else if (shouldBeEnabled && placeLayer && !this.map.hasLayer(placeLayer)) {
// Checkbox is already checked but layer isn't on map (edge case)
// This can happen if the checkbox was checked in HTML but layer wasn't added
// Manually add the layer since clicking won't help (checkbox is already checked)
placeLayer.addTo(this.map);
} }
} }
} }

View file

@ -25,7 +25,6 @@ export class PlacesManager {
this.loadPlaces(); this.loadPlaces();
}); });
console.log("[PlacesManager] Initializing, loading places for first time...");
await this.loadPlaces(); await this.loadPlaces();
this.setupMapClickHandler(); this.setupMapClickHandler();
this.setupEventListeners(); this.setupEventListeners();
@ -55,14 +54,19 @@ export class PlacesManager {
}); });
} }
async loadPlaces(tagIds = null) { async loadPlaces(tagIds = null, untaggedOnly = false) {
try { try {
const url = new URL('/api/v1/places', window.location.origin); const url = new URL('/api/v1/places', window.location.origin);
if (tagIds && tagIds.length > 0) {
if (untaggedOnly) {
// Load only untagged places
url.searchParams.append('untagged', 'true');
} else if (tagIds && tagIds.length > 0) {
// Load places with specific tags
tagIds.forEach(id => url.searchParams.append('tag_ids[]', id)); tagIds.forEach(id => url.searchParams.append('tag_ids[]', id));
} }
// If neither untaggedOnly nor tagIds, load all places
console.log("[PlacesManager] loadPlaces called, fetching from:", url.toString());
const response = await fetch(url, { const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${this.apiKey}` } headers: { 'Authorization': `Bearer ${this.apiKey}` }
}); });
@ -261,9 +265,9 @@ export class PlacesManager {
} }
} }
filterByTags(tagIds) { filterByTags(tagIds, untaggedOnly = false) {
this.selectedTags = new Set(tagIds); this.selectedTags = new Set(tagIds || []);
this.loadPlaces(tagIds.length > 0 ? tagIds : null); this.loadPlaces(tagIds && tagIds.length > 0 ? tagIds : null, untaggedOnly);
} }
/** /**
@ -278,11 +282,9 @@ export class PlacesManager {
// Add event listener to load places when layer is added to map // Add event listener to load places when layer is added to map
filteredLayer.on('add', () => { filteredLayer.on('add', () => {
console.log(`[PlacesManager] Filtered layer added to map, tagIds:`, tagIds);
this.loadPlacesIntoLayer(filteredLayer, tagIds); this.loadPlacesIntoLayer(filteredLayer, tagIds);
}); });
console.log(`[PlacesManager] Created filtered layer for tagIds:`, tagIds);
return filteredLayer; return filteredLayer;
} }
@ -291,21 +293,20 @@ export class PlacesManager {
*/ */
async loadPlacesIntoLayer(layer, tagIds) { async loadPlacesIntoLayer(layer, tagIds) {
try { try {
console.log(`[PlacesManager] loadPlacesIntoLayer called with tagIds:`, tagIds); const url = new URL('/api/v1/places', window.location.origin);
let url = `/api/v1/places?api_key=${this.apiKey}`;
if (Array.isArray(tagIds) && tagIds.length > 0) { if (Array.isArray(tagIds) && tagIds.length > 0) {
// Specific tags requested // Specific tags requested
url += `&tag_ids=${tagIds.join(',')}`; tagIds.forEach(id => url.searchParams.append('tag_ids[]', id));
} else if (Array.isArray(tagIds) && tagIds.length === 0) { } else if (Array.isArray(tagIds) && tagIds.length === 0) {
// Empty array means untagged places only // Empty array means untagged places only
url += '&untagged=true'; url.searchParams.append('untagged', 'true');
} }
console.log(`[PlacesManager] Fetching from URL:`, url); const response = await fetch(url, {
const response = await fetch(url); headers: { 'Authorization': `Bearer ${this.apiKey}` }
});
const data = await response.json(); const data = await response.json();
console.log(`[PlacesManager] Received ${data.length} places for tagIds:`, tagIds);
// Clear existing markers in this layer // Clear existing markers in this layer
layer.clearLayers(); layer.clearLayers();
@ -315,8 +316,6 @@ export class PlacesManager {
const marker = this.createPlaceMarker(place); const marker = this.createPlaceMarker(place);
layer.addLayer(marker); layer.addLayer(marker);
}); });
console.log(`[PlacesManager] Added ${data.length} markers to layer`);
} catch (error) { } catch (error) {
console.error('Error loading places into layer:', error); console.error('Error loading places into layer:', error);
} }
@ -330,14 +329,12 @@ export class PlacesManager {
ensurePlacesLayerVisible() { ensurePlacesLayerVisible() {
// Check if the main places layer is already on the map // Check if the main places layer is already on the map
if (this.map.hasLayer(this.placesLayer)) { if (this.map.hasLayer(this.placesLayer)) {
console.log('Places layer already visible');
return; return;
} }
// Try to find and enable the Places checkbox in the tree control // Try to find and enable the Places checkbox in the tree control
const layerControl = document.querySelector('.leaflet-control-layers'); const layerControl = document.querySelector('.leaflet-control-layers');
if (!layerControl) { if (!layerControl) {
console.log('Layer control not found, adding places layer directly');
this.map.addLayer(this.placesLayer); this.map.addLayer(this.placesLayer);
return; return;
} }
@ -356,7 +353,6 @@ export class PlacesManager {
input.checked = true; input.checked = true;
input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true }));
console.log('Enabled Places layer in tree control');
// Reset the flag after a short delay to allow the event to process // Reset the flag after a short delay to allow the event to process
setTimeout(() => { setTimeout(() => {

View file

@ -171,21 +171,18 @@ export function createPlacesControl(placesManager, tags, userTheme = 'dark') {
applyCurrentFilters: function() { applyCurrentFilters: function() {
if (!this.placesEnabled) return; if (!this.placesEnabled) return;
// If no specific filters, show all places // Build filter criteria
if (this.activeFilters.size === 0 && !this.showUntagged) { const tagIds = Array.from(this.activeFilters);
this.placesManager.filterByTags(null);
} else {
// Build filter criteria
const tagIds = Array.from(this.activeFilters);
// For now, just filter by tags if (this.showUntagged && tagIds.length === 0) {
// TODO: Add support for untagged filter in PlacesManager // Show only untagged places
if (tagIds.length > 0) { this.placesManager.filterByTags(null, true);
this.placesManager.filterByTags(tagIds); } else if (tagIds.length > 0) {
} else if (this.showUntagged) { // Show places with specific tags
// Show only untagged places this.placesManager.filterByTags(tagIds, false);
this.placesManager.filterByTags([]); } else {
} // Show all places (no filters)
this.placesManager.filterByTags(null, false);
} }
}, },

View file

@ -15,23 +15,23 @@ module Omniauthable
return user if user return user if user
# If not found, try to find by email # If not found, try to find by email
user = find_by(email: data['email']) user = find_by(email: data['email']) if data['email'].present?
if user if user
# Update provider and uid for existing user (first-time linking) # Update provider and uid for existing user (first-time linking)
user.update!(provider: provider, uid: uid) user.update!(provider: provider, uid: uid)
return user return user
end end
# Create new user if not found return nil unless data['email'].present?
user = create(
create(
email: data['email'], email: data['email'],
password: Devise.friendly_token[0, 20], password: Devise.friendly_token[0, 20],
provider: provider, provider: provider,
uid: uid uid: uid
) )
user
end end
end end
end end

View file

@ -42,7 +42,7 @@ METRICS_PASSWORD = ENV.fetch('METRICS_PASSWORD', 'prometheus')
OMNIAUTH_PROVIDERS = OMNIAUTH_PROVIDERS =
if SELF_HOSTED if SELF_HOSTED
# Self-hosted: only OpenID Connect # Self-hosted: only OpenID Connect
ENV['OIDC_CLIENT_ID'].present? ? %i[openid_connect] : [] (ENV['OIDC_CLIENT_ID'].present? && ENV['OIDC_CLIENT_SECRET'].present?) ? %i[openid_connect] : []
else else
# Cloud: only GitHub and Google # Cloud: only GitHub and Google
providers = [] providers = []

View file

@ -3,8 +3,6 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Shared::Stats', type: :request do RSpec.describe 'Shared::Stats', type: :request do
context 'public sharing' do context 'public sharing' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:stat) { create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6) } let(:stat) { create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6) }