mirror of
https://gitea.baerentsen.space/FrederikBaerentsen/BrickTracker.git
synced 2026-05-03 21:50:11 -05:00
Feat(checkbox): Initial upload
This commit is contained in:
@@ -134,6 +134,10 @@
|
||||
# Default: false
|
||||
# BK_HIDE_TABLE_MISSING_PARTS=true
|
||||
|
||||
# Optional: Hide the 'Checked' column from the parts table.
|
||||
# Default: false
|
||||
# BK_HIDE_TABLE_CHECKED_PARTS=true
|
||||
|
||||
# Optional: Hide the 'Wishlist' entry from the menu. Does not disable the route.
|
||||
# Default: false
|
||||
# BK_HIDE_WISHES=true
|
||||
|
||||
@@ -34,6 +34,7 @@ CONFIG: Final[list[dict[str, Any]]] = [
|
||||
{'n': 'HIDE_SET_INSTRUCTIONS', 'c': bool},
|
||||
{'n': 'HIDE_TABLE_DAMAGED_PARTS', 'c': bool},
|
||||
{'n': 'HIDE_TABLE_MISSING_PARTS', 'c': bool},
|
||||
{'n': 'HIDE_TABLE_CHECKED_PARTS', 'c': bool},
|
||||
{'n': 'HIDE_WISHES', 'c': bool},
|
||||
{'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"rebrickable_minifigures"."name" ASC'}, # noqa: E501
|
||||
{'n': 'MINIFIGURES_FOLDER', 'd': 'minifigs', 's': True},
|
||||
|
||||
@@ -159,6 +159,43 @@ class BrickPart(RebrickablePart):
|
||||
|
||||
return self
|
||||
|
||||
# Update checked state for part walkthrough
|
||||
def update_checked(self, json: Any | None, /) -> bool:
|
||||
# Handle both direct 'checked' key and changer.js 'value' key format
|
||||
if json:
|
||||
checked = json.get('checked', json.get('value', False))
|
||||
else:
|
||||
checked = False
|
||||
|
||||
checked = bool(checked)
|
||||
|
||||
# Update the field
|
||||
self.fields.checked = checked
|
||||
|
||||
BrickSQL().execute_and_commit(
|
||||
'part/update/checked',
|
||||
parameters=self.sql_parameters()
|
||||
)
|
||||
|
||||
return checked
|
||||
|
||||
# Compute the url for updating checked state
|
||||
def url_for_checked(self, /) -> str:
|
||||
# Different URL for a minifigure part
|
||||
if self.minifigure is not None:
|
||||
figure = self.minifigure.fields.figure
|
||||
else:
|
||||
figure = None
|
||||
|
||||
return url_for(
|
||||
'set.checked_part',
|
||||
id=self.fields.id,
|
||||
figure=figure,
|
||||
part=self.fields.part,
|
||||
color=self.fields.color,
|
||||
spare=self.fields.spare,
|
||||
)
|
||||
|
||||
# Update a problematic part
|
||||
def update_problem(self, problem: str, json: Any | None, /) -> int:
|
||||
amount: str | int = json.get('value', '') # type: ignore
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- description: Add checked field to bricktracker_parts table for part walkthrough tracking
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- Add checked field to the bricktracker_parts table
|
||||
-- This allows users to track which parts they have checked during walkthroughs
|
||||
ALTER TABLE "bricktracker_parts" ADD COLUMN "checked" BOOLEAN DEFAULT 0;
|
||||
|
||||
COMMIT;
|
||||
@@ -9,6 +9,7 @@ SELECT
|
||||
--"bricktracker_parts"."rebrickable_inventory",
|
||||
"bricktracker_parts"."missing",
|
||||
"bricktracker_parts"."damaged",
|
||||
"bricktracker_parts"."checked",
|
||||
--"rebrickable_parts"."part",
|
||||
--"rebrickable_parts"."color_id",
|
||||
"rebrickable_parts"."color_name",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
UPDATE "bricktracker_parts"
|
||||
SET "checked" = :checked
|
||||
WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id
|
||||
AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure
|
||||
AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part
|
||||
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color
|
||||
AND "bricktracker_parts"."spare" IS NOT DISTINCT FROM :spare
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Final
|
||||
|
||||
__version__: Final[str] = '1.2.5'
|
||||
__database_version__: Final[int] = 17
|
||||
__database_version__: Final[int] = 18
|
||||
|
||||
@@ -294,6 +294,50 @@ def problem_part(
|
||||
return jsonify({problem: amount})
|
||||
|
||||
|
||||
# Update checked state of parts during walkthrough
|
||||
@set_page.route('/<id>/parts/<part>/<int:color>/<int:spare>/checked', defaults={'figure': None}, methods=['POST']) # noqa: E501
|
||||
@set_page.route('/<id>/minifigures/<figure>/parts/<part>/<int:color>/<int:spare>/checked', methods=['POST']) # noqa: E501
|
||||
@login_required
|
||||
@exception_handler(__file__, json=True)
|
||||
def checked_part(
|
||||
*,
|
||||
id: str,
|
||||
figure: str | None,
|
||||
part: str,
|
||||
color: int,
|
||||
spare: int,
|
||||
) -> Response:
|
||||
brickset = BrickSet().select_specific(id)
|
||||
|
||||
if figure is not None:
|
||||
brickminifigure = BrickMinifigure().select_specific(brickset, figure)
|
||||
else:
|
||||
brickminifigure = None
|
||||
|
||||
brickpart = BrickPart().select_specific(
|
||||
brickset,
|
||||
part,
|
||||
color,
|
||||
spare,
|
||||
minifigure=brickminifigure,
|
||||
)
|
||||
|
||||
checked = brickpart.update_checked(request.json)
|
||||
|
||||
# Info
|
||||
logger.info('Set {set} ({id}): updated part ({part} color: {color}, spare: {spare}, minifigure: {figure}) checked state to {checked}'.format( # noqa: E501
|
||||
set=brickset.fields.set,
|
||||
id=brickset.fields.id,
|
||||
figure=figure,
|
||||
part=brickpart.fields.part,
|
||||
color=brickpart.fields.color,
|
||||
spare=brickpart.fields.spare,
|
||||
checked=checked
|
||||
))
|
||||
|
||||
return jsonify({'checked': checked})
|
||||
|
||||
|
||||
# Refresh a set
|
||||
@set_page.route('/refresh/<set>/', methods=['GET'])
|
||||
@set_page.route('/<id>/refresh', methods=['GET'])
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// Bulk operations for parts in set details page
|
||||
class PartsBulkOperations {
|
||||
constructor(accordionId) {
|
||||
this.accordionId = accordionId;
|
||||
this.setupModal();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupModal() {
|
||||
// Create Bootstrap modal if it doesn't exist
|
||||
if (!document.getElementById('partsConfirmModal')) {
|
||||
const modalHTML = `
|
||||
<div class="modal fade" id="partsConfirmModal" tabindex="-1" aria-labelledby="partsConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="partsConfirmModalLabel">Confirm Action</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="partsConfirmModalMessage"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="partsConfirmModalConfirm">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Mark all as missing (only if missing parts are not hidden)
|
||||
const markAllMissingBtn = document.getElementById(`mark-all-missing-${this.accordionId}`);
|
||||
if (markAllMissingBtn) {
|
||||
markAllMissingBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.confirmAndExecute(
|
||||
'Mark all parts as missing?',
|
||||
'This will set the missing count to the maximum quantity for all parts in this section.',
|
||||
() => this.markAllMissing()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Clear all missing (only if missing parts are not hidden)
|
||||
const clearAllMissingBtn = document.getElementById(`clear-all-missing-${this.accordionId}`);
|
||||
if (clearAllMissingBtn) {
|
||||
clearAllMissingBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.confirmAndExecute(
|
||||
'Clear all missing parts?',
|
||||
'This will clear the missing field for all parts in this section.',
|
||||
() => this.clearAllMissing()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Check all checkboxes (only if checked parts are not hidden)
|
||||
const checkAllBtn = document.getElementById(`check-all-${this.accordionId}`);
|
||||
if (checkAllBtn) {
|
||||
checkAllBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.checkAll();
|
||||
});
|
||||
}
|
||||
|
||||
// Uncheck all checkboxes (only if checked parts are not hidden)
|
||||
const uncheckAllBtn = document.getElementById(`uncheck-all-${this.accordionId}`);
|
||||
if (uncheckAllBtn) {
|
||||
uncheckAllBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.uncheckAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
confirmAndExecute(title, message, callback) {
|
||||
const modal = document.getElementById('partsConfirmModal');
|
||||
const modalTitle = document.getElementById('partsConfirmModalLabel');
|
||||
const modalMessage = document.getElementById('partsConfirmModalMessage');
|
||||
const confirmBtn = document.getElementById('partsConfirmModalConfirm');
|
||||
|
||||
// Set modal content
|
||||
modalTitle.textContent = title;
|
||||
modalMessage.textContent = message;
|
||||
|
||||
// Remove any existing event listeners and add new one
|
||||
const newConfirmBtn = confirmBtn.cloneNode(true);
|
||||
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
||||
|
||||
newConfirmBtn.addEventListener('click', () => {
|
||||
const modalInstance = bootstrap.Modal.getInstance(modal);
|
||||
modalInstance.hide();
|
||||
callback();
|
||||
});
|
||||
|
||||
// Show modal
|
||||
const modalInstance = new bootstrap.Modal(modal);
|
||||
modalInstance.show();
|
||||
}
|
||||
|
||||
markAllMissing() {
|
||||
const accordionElement = document.getElementById(this.accordionId);
|
||||
if (!accordionElement) return;
|
||||
|
||||
// Find all rows in this accordion
|
||||
const rows = accordionElement.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
// Find the quantity cell (usually 4th column)
|
||||
const quantityCell = row.cells[3]; // Index 3 for quantity column
|
||||
const missingInput = row.querySelector('input[id*="-missing-"]');
|
||||
|
||||
if (quantityCell && missingInput) {
|
||||
// Extract quantity from cell text content
|
||||
const quantityText = quantityCell.textContent.trim();
|
||||
const quantity = parseInt(quantityText) || 1; // Default to 1 if can't parse
|
||||
|
||||
if (missingInput.value !== quantity.toString()) {
|
||||
missingInput.value = quantity.toString();
|
||||
// Trigger change event to activate BrickChanger
|
||||
missingInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearAllMissing() {
|
||||
const accordionElement = document.getElementById(this.accordionId);
|
||||
if (!accordionElement) return;
|
||||
|
||||
const missingInputs = accordionElement.querySelectorAll('input[id*="-missing-"]');
|
||||
missingInputs.forEach(input => {
|
||||
if (input.value !== '') {
|
||||
input.value = '';
|
||||
// Trigger change event to activate BrickChanger
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkAll() {
|
||||
const accordionElement = document.getElementById(this.accordionId);
|
||||
if (!accordionElement) return;
|
||||
|
||||
const checkboxes = accordionElement.querySelectorAll('input[id*="-checked-"][type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (!checkbox.checked) {
|
||||
checkbox.checked = true;
|
||||
// Trigger change event to activate BrickChanger
|
||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
uncheckAll() {
|
||||
const accordionElement = document.getElementById(this.accordionId);
|
||||
if (!accordionElement) return;
|
||||
|
||||
const checkboxes = accordionElement.querySelectorAll('input[id*="-checked-"][type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (checkbox.checked) {
|
||||
checkbox.checked = false;
|
||||
// Trigger change event to activate BrickChanger
|
||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize bulk operations for all part accordions when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Find all hamburger menus and initialize bulk operations
|
||||
const hamburgerMenus = document.querySelectorAll('button[id^="hamburger-"]');
|
||||
hamburgerMenus.forEach(button => {
|
||||
const accordionId = button.id.replace('hamburger-', '');
|
||||
new PartsBulkOperations(accordionId);
|
||||
});
|
||||
});
|
||||
@@ -50,6 +50,67 @@
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
/* Checkbox column width constraint */
|
||||
.table-td-input:has(.form-check-input[type="checkbox"]) {
|
||||
width: 120px;
|
||||
max-width: 120px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Reserve space for status icon to prevent layout shift */
|
||||
.form-check-label i[id^="status-"] {
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
/* Hamburger menu styling */
|
||||
.table th .dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table th .dropdown-toggle {
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.table th .dropdown-toggle:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.table th .dropdown-toggle:hover {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
/* Style dropdown items */
|
||||
.dropdown-menu .dropdown-header {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: #6c757d;
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
|
||||
.dropdown-menu .dropdown-item {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.dropdown-menu .dropdown-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.dropdown-menu .dropdown-item i {
|
||||
width: 1.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Fixes for sortable.js */
|
||||
.sortable {
|
||||
--th-color: #000 !important;
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
{% if request.endpoint == 'set.list' %}
|
||||
<script src="{{ url_for('static', filename='scripts/sets.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'set.details' %}
|
||||
<script src="{{ url_for('static', filename='scripts/parts-bulk-operations.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'instructions.download' or request.endpoint == 'instructions.do_download' %}
|
||||
<script src="{{ url_for('static', filename='scripts/socket/peeron.js') }}"></script>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% macro header(title, id, parent, quantity=none, expanded=false, icon=none, class=none, danger=none, image=none, alt=none) %}
|
||||
{% macro header(title, id, parent, quantity=none, expanded=false, icon=none, class=none, danger=none, image=none, alt=none, hamburger_menu=none) %}
|
||||
{% if danger %}
|
||||
{% set icon='alert-fill' %}
|
||||
{% endif %}
|
||||
@@ -43,10 +43,10 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro table(table_collection, title, id, parent, target, quantity=none, icon=none, image=none, alt=none, details=none, read_only=none) %}
|
||||
{% macro table(table_collection, title, id, parent, target, quantity=none, icon=none, image=none, alt=none, details=none, read_only=none, hamburger_menu=none) %}
|
||||
{% set size=table_collection | length %}
|
||||
{% if size %}
|
||||
{{ header(title, id, parent, quantity=quantity, icon=icon, class='p-0', image=image, alt=alt) }}
|
||||
{{ header(title, id, parent, quantity=quantity, icon=icon, class='p-0', image=image, alt=alt, hamburger_menu=hamburger_menu) }}
|
||||
{% if details %}
|
||||
<p class="border-top border-bottom p-2 text-center">
|
||||
{% if image %}
|
||||
@@ -57,7 +57,7 @@
|
||||
<a class="btn border bg-secondary-text" href="{{ details }}">{% if icon %}<i class="ri-{{ icon }}"></i>{% endif %} Details</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% with solo=true, all=false %}
|
||||
{% with solo=true, all=false, accordion_id=id %}
|
||||
{% include target %}
|
||||
{% endwith %}
|
||||
{{ footer() }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% macro header(image=true, color=false, parts=false, quantity=false, missing=true, missing_parts=false, damaged=true, damaged_parts=false, sets=false, minifigures=false) %}
|
||||
{% macro header(image=true, color=false, parts=false, quantity=false, missing=true, missing_parts=false, damaged=true, damaged_parts=false, sets=false, minifigures=false, checked=false, hamburger_menu=false, accordion_id='') %}
|
||||
<thead>
|
||||
<tr>
|
||||
{% if image %}
|
||||
@@ -26,6 +26,37 @@
|
||||
{% if minifigures %}
|
||||
<th data-table-number="true" scope="col"><i class="ri-group-line fw-normal"></i> Minifigures</th>
|
||||
{% endif %}
|
||||
{% if checked and not config['HIDE_TABLE_CHECKED_PARTS'] %}
|
||||
<th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-checkbox-line fw-normal"></i> Checked</th>
|
||||
{% endif %}
|
||||
{% if hamburger_menu and g.login.is_authenticated() %}
|
||||
{% set show_missing_menu = not config['HIDE_TABLE_MISSING_PARTS'] %}
|
||||
{% set show_checked_menu = not config['HIDE_TABLE_CHECKED_PARTS'] %}
|
||||
{% if show_missing_menu or show_checked_menu %}
|
||||
<th data-table-no-sort-and-search="true" class="no-sort text-end" scope="col">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="hamburger-{{ accordion_id }}" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="ri-menu-line"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="hamburger-{{ accordion_id }}">
|
||||
{% if show_missing_menu %}
|
||||
<li><h6 class="dropdown-header">Missing Parts</h6></li>
|
||||
<li><a class="dropdown-item" href="#" id="mark-all-missing-{{ accordion_id }}"><i class="ri-question-line me-2"></i>Mark all as missing</a></li>
|
||||
<li><a class="dropdown-item" href="#" id="clear-all-missing-{{ accordion_id }}"><i class="ri-close-circle-line me-2"></i>Clear all missing</a></li>
|
||||
{% endif %}
|
||||
{% if show_missing_menu and show_checked_menu %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% endif %}
|
||||
{% if show_checked_menu %}
|
||||
<li><h6 class="dropdown-header">Checked Status</h6></li>
|
||||
<li><a class="dropdown-item" href="#" id="check-all-{{ accordion_id }}"><i class="ri-checkbox-line me-2"></i>Check all</a></li>
|
||||
<li><a class="dropdown-item" href="#" id="uncheck-all-{{ accordion_id }}"><i class="ri-checkbox-blank-line me-2"></i>Uncheck all</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</th>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table data-table="{% if all %}true{% endif %}" class="table table-striped align-middle {% if not all %}sortable mb-0{% endif %}" {% if all %}id="parts"{% endif %}>
|
||||
{{ table.header(color=true, quantity=not no_quantity, sets=all, minifigures=all) }}
|
||||
{{ table.header(color=true, quantity=not no_quantity, sets=all, minifigures=all, checked=not all, hamburger_menu=not all, accordion_id=accordion_id|default('')) }}
|
||||
<tbody>
|
||||
{% for item in table_collection %}
|
||||
<tr>
|
||||
@@ -40,6 +40,19 @@
|
||||
{% if all %}
|
||||
<td>{{ item.fields.total_sets }}</td>
|
||||
<td>{{ item.fields.total_minifigures }}</td>
|
||||
{% else %}
|
||||
{% if not config['HIDE_TABLE_CHECKED_PARTS'] %}
|
||||
<td class="table-td-input">
|
||||
<center>{{ form.checkbox('', item.fields.id, item.html_id('checked'), item.url_for_checked(), item.fields.checked | default(false), parent='part', delete=read_only) }}</center>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if g.login.is_authenticated() %}
|
||||
{% set show_missing_menu = not config['HIDE_TABLE_MISSING_PARTS'] %}
|
||||
{% set show_checked_menu = not config['HIDE_TABLE_CHECKED_PARTS'] %}
|
||||
{% if show_missing_menu or show_checked_menu %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -104,14 +104,14 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if g.login.is_authenticated() %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('instructions.download', set=item.fields.set) }}"><i class="ri-download-line"></i> Download instructions from Rebrickable</a>
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('instructions.download', set=item.fields.set) }}"><i class="ri-download-line"></i> Download instructions</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'set-details', 'part/table.html', icon='shapes-line')}}
|
||||
{{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'set-details', 'part/table.html', icon='shapes-line', hamburger_menu=g.login.is_authenticated())}}
|
||||
{% for minifigure in item.minifigures() %}
|
||||
{{ accordion.table(minifigure.parts(), minifigure.fields.name, minifigure.fields.figure, 'set-details', 'part/table.html', quantity=minifigure.fields.quantity, icon='group-line', image=minifigure.url_for_image(), alt=minifigure.fields.figure, details=minifigure.url())}}
|
||||
{{ accordion.table(minifigure.parts(), minifigure.fields.name, minifigure.fields.figure, 'set-details', 'part/table.html', quantity=minifigure.fields.quantity, icon='group-line', image=minifigure.url_for_image(), alt=minifigure.fields.figure, details=minifigure.url(), hamburger_menu=g.login.is_authenticated())}}
|
||||
{% endfor %}
|
||||
{% include 'set/management.html' %}
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user