feat(sets): added filter on sets page to show duplicate sets. default is shown. can be hidden using env var. works with consolidated sets too.
This commit is contained in:
@@ -358,6 +358,10 @@
|
||||
# Default: false
|
||||
# BK_SHOW_GRID_SORT=true
|
||||
|
||||
# Optional: Show duplicate filter button on sets page
|
||||
# Default: true
|
||||
# BK_SHOW_SETS_DUPLICATE_FILTER=true
|
||||
|
||||
# Optional: Skip saving or displaying spare parts
|
||||
# Default: false
|
||||
# BK_SKIP_SPARE_PARTS=true
|
||||
|
||||
@@ -82,6 +82,7 @@ CONFIG: Final[list[dict[str, Any]]] = [
|
||||
{'n': 'SETS_CONSOLIDATION', 'd': False, 'c': bool},
|
||||
{'n': 'SHOW_GRID_FILTERS', 'c': bool},
|
||||
{'n': 'SHOW_GRID_SORT', 'c': bool},
|
||||
{'n': 'SHOW_SETS_DUPLICATE_FILTER', 'd': True, 'c': bool},
|
||||
{'n': 'SKIP_SPARE_PARTS', 'c': bool},
|
||||
{'n': 'SOCKET_NAMESPACE', 'd': 'bricksocket'},
|
||||
{'n': 'SOCKET_PATH', 'd': '/bricksocket/'},
|
||||
|
||||
@@ -44,6 +44,7 @@ LIVE_CHANGEABLE_VARS: Final[List[str]] = [
|
||||
'BK_REBRICKABLE_LINKS',
|
||||
'BK_SHOW_GRID_FILTERS',
|
||||
'BK_SHOW_GRID_SORT',
|
||||
'BK_SHOW_SETS_DUPLICATE_FILTER',
|
||||
'BK_SKIP_SPARE_PARTS',
|
||||
'BK_USE_REMOTE_IMAGES',
|
||||
'BK_PEERON_DOWNLOAD_DELAY',
|
||||
|
||||
@@ -86,6 +86,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
||||
storage_filter: str | None = None,
|
||||
tag_filter: str | None = None,
|
||||
year_filter: str | None = None,
|
||||
duplicate_filter: bool = False,
|
||||
use_consolidated: bool = True
|
||||
) -> tuple[Self, int]:
|
||||
# Convert theme name to theme ID for filtering
|
||||
@@ -94,7 +95,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
||||
theme_id_filter = self._theme_name_to_id(theme_filter)
|
||||
|
||||
# Check if any filters are applied
|
||||
has_filters = any([status_filter, theme_id_filter, owner_filter, purchase_location_filter, storage_filter, tag_filter, year_filter])
|
||||
has_filters = any([status_filter, theme_id_filter, owner_filter, purchase_location_filter, storage_filter, tag_filter, year_filter, duplicate_filter])
|
||||
|
||||
# Prepare filter context
|
||||
filter_context = {
|
||||
@@ -106,6 +107,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
||||
'storage_filter': storage_filter,
|
||||
'tag_filter': tag_filter,
|
||||
'year_filter': year_filter,
|
||||
'duplicate_filter': duplicate_filter,
|
||||
'owners': BrickSetOwnerList.as_columns(),
|
||||
'statuses': BrickSetStatusList.as_columns(),
|
||||
'tags': BrickSetTagList.as_columns(),
|
||||
|
||||
@@ -70,4 +70,12 @@ AND EXISTS (
|
||||
)
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if duplicate_filter %}
|
||||
AND (
|
||||
SELECT COUNT(*)
|
||||
FROM "bricktracker_sets" as "duplicate_check"
|
||||
WHERE "duplicate_check"."set" = "bricktracker_sets"."set"
|
||||
) > 1
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -147,15 +147,21 @@ AND NOT EXISTS (
|
||||
|
||||
GROUP BY "rebrickable_sets"."set"
|
||||
|
||||
{% if status_filter or duplicate_filter %}
|
||||
HAVING 1=1
|
||||
{% if status_filter %}
|
||||
{% if status_filter == 'has-missing' %}
|
||||
HAVING IFNULL(SUM("problem_join"."total_missing"), 0) > 0
|
||||
AND IFNULL(SUM("problem_join"."total_missing"), 0) > 0
|
||||
{% elif status_filter == '-has-missing' %}
|
||||
HAVING IFNULL(SUM("problem_join"."total_missing"), 0) = 0
|
||||
AND IFNULL(SUM("problem_join"."total_missing"), 0) = 0
|
||||
{% elif status_filter == 'has-damaged' %}
|
||||
HAVING IFNULL(SUM("problem_join"."total_damaged"), 0) > 0
|
||||
AND IFNULL(SUM("problem_join"."total_damaged"), 0) > 0
|
||||
{% elif status_filter == '-has-damaged' %}
|
||||
HAVING IFNULL(SUM("problem_join"."total_damaged"), 0) = 0
|
||||
AND IFNULL(SUM("problem_join"."total_damaged"), 0) = 0
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if duplicate_filter %}
|
||||
AND COUNT("bricktracker_sets"."id") > 1
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ def list() -> str:
|
||||
storage_filter = request.args.get('storage')
|
||||
tag_filter = request.args.get('tag')
|
||||
year_filter = request.args.get('year')
|
||||
duplicate_filter = request.args.get('duplicate', '').lower() == 'true'
|
||||
|
||||
# Get pagination configuration
|
||||
per_page, is_mobile = get_pagination_config('sets')
|
||||
@@ -67,6 +68,7 @@ def list() -> str:
|
||||
storage_filter=storage_filter,
|
||||
tag_filter=tag_filter,
|
||||
year_filter=year_filter,
|
||||
duplicate_filter=duplicate_filter,
|
||||
use_consolidated=current_app.config['SETS_CONSOLIDATION']
|
||||
)
|
||||
|
||||
@@ -102,6 +104,7 @@ def list() -> str:
|
||||
'current_storage_filter': storage_filter,
|
||||
'current_tag_filter': tag_filter,
|
||||
'current_year_filter': year_filter,
|
||||
'current_duplicate_filter': duplicate_filter,
|
||||
'brickset_statuses': BrickSetStatusList.list(),
|
||||
**set_metadata_lists(as_class=True)
|
||||
}
|
||||
|
||||
@@ -180,7 +180,12 @@ class BrickGridFilter {
|
||||
}
|
||||
|
||||
// If we passed all filters, we need to display it
|
||||
current.parentElement.classList.remove("d-none");
|
||||
// But also check if it's hidden by duplicate filter
|
||||
if (!current.parentElement.classList.contains("duplicate-filter-hidden")) {
|
||||
current.parentElement.classList.remove("d-none");
|
||||
} else {
|
||||
current.parentElement.classList.add("d-none");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const searchInput = document.getElementById('grid-search');
|
||||
const searchClear = document.getElementById('grid-search-clear');
|
||||
|
||||
// Initialize duplicate filter functionality
|
||||
initializeDuplicateFilter();
|
||||
|
||||
if (searchInput && searchClear) {
|
||||
if (isPaginationMode()) {
|
||||
// PAGINATION MODE - Server-side search
|
||||
@@ -559,4 +562,146 @@ function removeSetGrouping() {
|
||||
groupContainers.forEach(container => {
|
||||
container.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize duplicate/consolidated filter functionality
|
||||
function initializeDuplicateFilter() {
|
||||
const duplicateFilterButton = document.getElementById('duplicate-filter-toggle');
|
||||
if (!duplicateFilterButton) return;
|
||||
|
||||
// Check if the filter should be active from URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isDuplicateFilterActive = urlParams.get('duplicate') === 'true';
|
||||
|
||||
// Set initial button state
|
||||
if (isDuplicateFilterActive) {
|
||||
duplicateFilterButton.classList.remove('btn-outline-secondary');
|
||||
duplicateFilterButton.classList.add('btn-secondary');
|
||||
}
|
||||
|
||||
duplicateFilterButton.addEventListener('click', () => {
|
||||
const isCurrentlyActive = duplicateFilterButton.classList.contains('btn-secondary');
|
||||
const newState = !isCurrentlyActive;
|
||||
|
||||
// Update button appearance
|
||||
if (newState) {
|
||||
duplicateFilterButton.classList.remove('btn-outline-secondary');
|
||||
duplicateFilterButton.classList.add('btn-secondary');
|
||||
} else {
|
||||
duplicateFilterButton.classList.remove('btn-secondary');
|
||||
duplicateFilterButton.classList.add('btn-outline-secondary');
|
||||
}
|
||||
|
||||
if (isPaginationMode()) {
|
||||
// SERVER-SIDE MODE - Update URL parameter
|
||||
performDuplicateFilterServer(newState);
|
||||
} else {
|
||||
// CLIENT-SIDE MODE - Apply filtering directly
|
||||
applyDuplicateFilter(newState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Server-side duplicate filter
|
||||
function performDuplicateFilterServer(showOnlyDuplicates) {
|
||||
const currentUrl = new URL(window.location);
|
||||
|
||||
if (showOnlyDuplicates) {
|
||||
currentUrl.searchParams.set('duplicate', 'true');
|
||||
} else {
|
||||
currentUrl.searchParams.delete('duplicate');
|
||||
}
|
||||
|
||||
// Reset to page 1 when filtering
|
||||
currentUrl.searchParams.set('page', '1');
|
||||
window.location.href = currentUrl.toString();
|
||||
}
|
||||
|
||||
// Apply duplicate/consolidated filter
|
||||
function applyDuplicateFilter(showOnlyDuplicates) {
|
||||
// Get the grid container and all column containers (not just the cards)
|
||||
const gridContainer = document.getElementById('grid');
|
||||
if (!gridContainer) {
|
||||
console.warn('Grid container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Try multiple selectors to find column containers
|
||||
let columnContainers = gridContainer.querySelectorAll('.col-md-6');
|
||||
if (columnContainers.length === 0) {
|
||||
columnContainers = gridContainer.querySelectorAll('[class*="col-"]');
|
||||
}
|
||||
|
||||
if (!showOnlyDuplicates) {
|
||||
// Show all column containers by removing the duplicate-filter-hidden class
|
||||
columnContainers.forEach(col => {
|
||||
col.classList.remove('duplicate-filter-hidden');
|
||||
});
|
||||
// Trigger the existing grid filter to refresh
|
||||
triggerGridRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're in consolidated mode by looking for data-instance-count
|
||||
const consolidatedMode = document.querySelector('[data-instance-count]') !== null;
|
||||
|
||||
if (consolidatedMode) {
|
||||
// CONSOLIDATED MODE: Show only sets with instance count > 1
|
||||
columnContainers.forEach(col => {
|
||||
const card = col.querySelector('[data-set-id]');
|
||||
if (card) {
|
||||
const instanceCount = parseInt(card.dataset.instanceCount || '1');
|
||||
if (instanceCount > 1) {
|
||||
col.classList.remove('duplicate-filter-hidden');
|
||||
} else {
|
||||
col.classList.add('duplicate-filter-hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// NON-CONSOLIDATED MODE: Show only sets that appear multiple times
|
||||
const setByCounts = {};
|
||||
|
||||
// Count occurrences of each set
|
||||
columnContainers.forEach(col => {
|
||||
const card = col.querySelector('[data-set-id]');
|
||||
if (card) {
|
||||
const rebrickableId = card.dataset.rebrickableId;
|
||||
if (rebrickableId) {
|
||||
setByCounts[rebrickableId] = (setByCounts[rebrickableId] || 0) + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide based on count
|
||||
columnContainers.forEach(col => {
|
||||
const card = col.querySelector('[data-set-id]');
|
||||
if (card) {
|
||||
const rebrickableId = card.dataset.rebrickableId;
|
||||
if (rebrickableId && setByCounts[rebrickableId] > 1) {
|
||||
col.classList.remove('duplicate-filter-hidden');
|
||||
} else {
|
||||
col.classList.add('duplicate-filter-hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger the existing grid filter to refresh and respect our duplicate filter
|
||||
triggerGridRefresh();
|
||||
}
|
||||
|
||||
// Helper function to trigger grid filter refresh
|
||||
function triggerGridRefresh() {
|
||||
// Check if we have a grid instance with filter capability
|
||||
if (window.gridInstances) {
|
||||
const gridElement = document.getElementById('grid');
|
||||
if (gridElement && window.gridInstances[gridElement.id]) {
|
||||
const gridInstance = window.gridInstances[gridElement.id];
|
||||
if (gridInstance.filter) {
|
||||
// Trigger the existing filter to refresh
|
||||
gridInstance.filter.filter();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -178,4 +178,5 @@
|
||||
border-radius: 2px;
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
}/* Duplicate filter support */
|
||||
.duplicate-filter-hidden { display: none !important; }
|
||||
|
||||
@@ -236,6 +236,14 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input config-toggle" type="checkbox" id="BK_SHOW_SETS_DUPLICATE_FILTER" data-var="BK_SHOW_SETS_DUPLICATE_FILTER">
|
||||
<label class="form-check-label" for="BK_SHOW_SETS_DUPLICATE_FILTER">
|
||||
BK_SHOW_SETS_DUPLICATE_FILTER {{ config_badges('BK_SHOW_SETS_DUPLICATE_FILTER') }}
|
||||
<div class="text-muted small">Show duplicate/consolidated filter button on sets page</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input config-toggle" type="checkbox" id="BK_INDEPENDENT_ACCORDIONS" data-var="BK_INDEPENDENT_ACCORDIONS">
|
||||
<label class="form-check-label" for="BK_INDEPENDENT_ACCORDIONS">
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#grid-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="grid-filter">
|
||||
<i class="ri-filter-line"></i> Filters
|
||||
</button>
|
||||
{% if config['SHOW_SETS_DUPLICATE_FILTER'] %}
|
||||
<button class="btn {% if current_duplicate_filter %}btn-secondary{% else %}btn-outline-secondary{% endif %}" type="button" id="duplicate-filter-toggle" title="Show duplicate sets only">
|
||||
<i class="ri-stack-line"></i> Duplicates
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user