Compare commits

...

9 Commits

13 changed files with 332 additions and 57 deletions
+2 -5
View File
@@ -32,11 +32,8 @@ LICENSE
**/__pycache__
*.pyc
# Git
.git
# IDE
.vscode
# Hidden directories
.?*
# Dev
test-server.sh
+3
View File
@@ -36,3 +36,6 @@ vitepress/
# Local data
offline/
data/
# Hidden folders
.?*
+13
View File
@@ -36,6 +36,19 @@
- Notes display as an info alert box below badges when enabled
- Both settings can be toggled in Admin -> Live Settings panel without container restart
- Fixed consolidated SQL query to include description field for proper notes display in server-side pagination
- **Fixed permission denied when running as non-root user** (Issue #138): Resolved container startup failure when using `user:` directive in docker-compose
- Added `chmod -R a+rX /app` to Dockerfile to ensure all files are readable regardless of build environment
- Added commented `user:` example in `compose.yaml` to document non-root support
### Breaking Changes
- **Parts default order column names changed**: The `BK_PARTS_DEFAULT_ORDER` environment variable now uses `"combined"` instead of `"bricktracker_parts"` for column references
- If you have a custom `BK_PARTS_DEFAULT_ORDER` setting, update column references:
- `"bricktracker_parts"."spare"``"combined"."spare"`
- `"bricktracker_parts"."part"``"combined"."part"`
- `"bricktracker_parts"."quantity"``"combined"."quantity"`
- Or remove the custom setting to use the new defaults
- See `.env.sample` for the full list of available column names
### New Features
+3
View File
@@ -11,6 +11,9 @@ RUN pip install --no-cache-dir -r requirements.txt
# Bricktracker
COPY . .
# Ensure all files are readable by non-root users (supports user: directive in compose)
RUN chmod -R a+rX /app
# Set executable permissions for entrypoint script
RUN chmod +x entrypoint.sh
+2
View File
@@ -6,6 +6,8 @@ A web application for organizing and tracking LEGO sets, parts, and minifigures.
<a href="https://www.buymeacoffee.com/frederikb" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="41" width="174"></a>
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=48JEEKLCGB8DJ"><img src="./docs/images/blue.svg" height="40"></a>
## Features
- Track multiple LEGO sets with their parts and minifigures
+37
View File
@@ -94,6 +94,36 @@ class BrickSet(RebrickableSet):
)
self.fields.purchase_location = purchase_location.fields.id
# Save the purchase date
purchase_date = data.get('purchase_date', None)
if purchase_date == '':
purchase_date = None
if purchase_date is not None:
try:
purchase_date = datetime.strptime(
purchase_date, '%Y/%m/%d'
).timestamp()
except Exception:
purchase_date = None
self.fields.purchase_date = purchase_date
# Save the purchase price
purchase_price = data.get('purchase_price', None)
if purchase_price == '':
purchase_price = None
if purchase_price is not None:
try:
purchase_price = float(purchase_price)
except Exception:
purchase_price = None
self.fields.purchase_price = purchase_price
# Save the description/notes
description = data.get('description', None)
if description == '':
description = None
self.fields.description = description
# Insert into database (deferred - will execute at final commit)
# All operations are atomic - if anything fails, nothing is committed
self.insert(commit=False)
@@ -105,6 +135,13 @@ class BrickSet(RebrickableSet):
owner = BrickSetOwnerList.get(id)
owner.update_set_state(self, state=True, commit=False)
# Save the statuses (deferred - will execute at final commit)
statuses: list[str] = list(data.get('statuses', []))
for id in statuses:
status = BrickSetStatusList.get(id)
status.update_set_state(self, state=True, commit=False)
# Save the tags (deferred - will execute at final commit)
tags: list[str] = list(data.get('tags', []))
+8 -2
View File
@@ -2,10 +2,16 @@ INSERT OR IGNORE INTO "bricktracker_sets" (
"id",
"set",
"storage",
"purchase_location"
"purchase_location",
"purchase_date",
"purchase_price",
"description"
) VALUES (
:id,
:set,
:storage,
:purchase_location
:purchase_location,
:purchase_date,
:purchase_price,
:description
)
+3
View File
@@ -4,6 +4,7 @@ from flask_login import login_required
from ..configuration_list import BrickConfigurationList
from .exceptions import exception_handler
from ..set_list import set_metadata_lists
from ..set_status_list import BrickSetStatusList
from ..socket import MESSAGES
add_page = Blueprint('add', __name__, url_prefix='/add')
@@ -21,6 +22,7 @@ def add() -> str:
path=current_app.config['SOCKET_PATH'],
namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES,
brickset_statuses=BrickSetStatusList.list(all=True),
**set_metadata_lists()
)
@@ -38,6 +40,7 @@ def bulk() -> str:
namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES,
bulk=True,
brickset_statuses=BrickSetStatusList.list(all=True),
**set_metadata_lists()
)
+2
View File
@@ -3,6 +3,8 @@ services:
container_name: BrickTracker
restart: unless-stopped
image: gitea.baerentsen.space/frederikbaerentsen/bricktracker:latest
# Optional: Run as non-root user (uncomment to enable)
# user: "1000:1000"
ports:
- "3333:3333"
volumes:
+118
View File
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1050 200" style="enable-background:new 0 0 1050 200;" xml:space="preserve">
<style type="text/css">
.st0{fill:#009CDE;}
.st1{fill:#FFFFFF;}
.st2{filter:url(#Adobe_OpacityMaskFilter);}
.st3{opacity:0.678;mask:url(#e_1_);fill:#FFFFFF;enable-background:new ;}
.st4{filter:url(#Adobe_OpacityMaskFilter_1_);}
.st5{opacity:0.696;mask:url(#g_1_);fill:#FFFFFF;enable-background:new ;}
.st6{filter:url(#Adobe_OpacityMaskFilter_2_);}
.st7{mask:url(#i_1_);fill:#FFFFFF;}
</style>
<g id="button">
<path id="blue" class="st0" d="M1018,200H32c-17.7,0-32-14.3-32-32L0,32C0,14.3,14.3,0,32,0l986,0c17.7,0,32,14.3,32,32v136
C1050,185.7,1035.7,200,1018,200z"/>
</g>
<g id="donate">
<g>
<path class="st1" d="M73.3,110.6c0-13.1,6.8-21.4,17.2-21.4c5.9,0,10.6,3.1,12.8,7.7h0.2V79.3c0-3.3,2-5.1,4.8-5.1
s4.9,1.8,4.9,5.1v47.9c0,3.1-2,4.9-4.8,4.9s-4.9-1.7-4.9-4.9V124h-0.2c-2,4.8-6.4,8-12.9,8C80,132,73.3,123.8,73.3,110.6z
M103.5,110.6c0-8.1-4-13.4-10.2-13.4c-6.3,0-10.2,5.2-10.2,13.4c0,8.2,3.9,13.4,10.2,13.4C99.5,124,103.5,118.8,103.5,110.6z"/>
<path class="st1" d="M205.5,131.7c-3.3,0-5.2-2-5.2-5.7v-19.7c0-5.4-2.7-8.5-7.5-8.5c-5.4,0-8.7,3.9-8.7,10.4v17.8
c0,3.6-1.9,5.7-5.2,5.7s-5.2-2-5.2-5.7V95c0-3.4,2-5.5,5.1-5.5c3.1,0,4.9,1.8,5,5.2v2.5h0.5c1.6-4.9,6.3-8,12.3-8
c8.8,0,13.8,5.2,13.8,14.3v22.5C210.6,129.7,208.8,131.7,205.5,131.7z"/>
<path class="st1" d="M232,131.9c-8.1,0-13.6-5.1-13.6-12.6c0-7.7,5.7-12.4,15.1-12.4h9.8v-3.4c0-4.2-2.6-6.4-7.3-6.4
c-3.1,0-5.4,1.2-7.7,3.2c-1.2,0.9-2.3,1.4-3.9,1.4c-2.3,0-3.7-1.5-3.7-3.6c0-2.4,1.7-4.9,5.3-6.7c2.6-1.4,6.1-2.1,10.6-2.1
c10.9,0,17,5.2,17,14.3v22.7c0,3.5-1.8,5.5-5,5.5c-2.7,0-4.5-1.5-4.8-4.2v-1.4h-0.5C241.1,129.8,237,131.9,232,131.9z
M235.4,124.5c4.5,0,7.9-3,7.9-7v-4.3h-8c-4.2,0-6.7,2-6.7,5.4C228.6,122.1,231.4,124.5,235.4,124.5z"/>
<path class="st1" d="M266.3,98.3h-2.1c-2.8,0-4.4-1.5-4.4-4s1.6-4,4.4-4h2.3v-5.4c0-3.6,1.8-5.5,5-5.5c3.2,0,5,2,5,5.5v5.4h4.1
c2.8,0,4.4,1.5,4.4,4s-1.6,4-4.4,4h-4v20.9c0,3.8,1,5,4.1,5c1.1,0,1.8-0.2,2.7-0.2c1.8,0,3,1.2,3,3.1c0,1.5-0.7,2.8-2.1,3.7
c-1.4,0.9-3.6,1.4-6.4,1.4c-8.2,0-11.7-3.4-11.7-11.9V98.3z"/>
<path class="st1" d="M291.3,108c0-11.3,7.5-19,18.6-19c10.5,0,18,7.5,18,17.7c0,4.6-1.3,6.2-5.2,6.2h-21v1.8
c0,5.8,3.7,9.5,9.8,9.5c3.2,0,5.5-0.8,7.3-2.3c2-1.5,2.7-2.1,4.3-2.1c2.1,0,3.6,1.5,3.6,3.8c0,1.8-1.1,3.6-3.1,5
c-2.7,2.3-7.5,3.7-12.8,3.7c-12.1,0-19.6-7.1-19.6-18.9V108z M318.1,106.4L318.1,106.4c0-5.7-3.1-9.4-8-9.4
c-5.1,0-8.3,3.8-8.3,9.3v0.1H318.1z"/>
<path class="st1" d="M359.5,97.1c-0.3-1.2-0.4-1.9-0.4-2.5c0-2.9,2.1-5,5.2-5c2.7,0,4.5,1.8,5,5.1l5.2,25.9h0.5l6.2-24.5
c1.2-4.8,2.9-6.6,6.3-6.6c3.4,0,5.2,1.8,6.4,6.6l6.3,24.5h0.5l5.4-25.9c0.6-3.2,2.4-5.1,5-5.1c3.1,0,5,2,5,4.7
c0,0.6-0.2,1.5-0.5,2.7l-7.2,26.5c-1.7,6.2-3.7,8.2-7.3,8.2c-3.9,0-5.9-2-7.5-8.2l-6-21.8h-0.4l-5.8,21.8
c-1.6,6.3-3.4,8.2-7.3,8.2c-3.8,0-5.9-2.1-7.5-8.2L359.5,97.1z"/>
<path class="st1" d="M426.9,74c3.4,0,5.9,2.3,5.9,5.4s-2.5,5.4-5.9,5.4c-3.3,0-5.8-2.3-5.8-5.4S423.6,74,426.9,74z M426.9,131.7
c-3.2,0-5.1-2-5.1-5.7V95.2c0-3.8,2-5.7,5.2-5.7c3.2,0,5.1,2,5.1,5.7v30.9C432.1,129.8,430.1,131.7,426.9,131.7z"/>
<path class="st1" d="M444,98.3h-2.1c-2.8,0-4.4-1.5-4.4-4s1.6-4,4.4-4h2.3v-5.4c0-3.6,1.8-5.5,5-5.5c3.2,0,5,2,5,5.5v5.4h4.1
c2.8,0,4.4,1.5,4.4,4s-1.6,4-4.4,4h-4v20.9c0,3.8,1,5,4.1,5c1.1,0,1.8-0.2,2.7-0.2c1.8,0,3,1.2,3,3.1c0,1.5-0.7,2.8-2.1,3.7
c-1.4,0.9-3.6,1.4-6.4,1.4c-8.2,0-11.7-3.4-11.7-11.9V98.3z"/>
<path class="st1" d="M501.1,131.7c-3.3,0-5.2-2-5.2-5.7v-19.6c0-5.4-2.8-8.5-7.6-8.5c-5.3,0-8.8,4-8.8,10.4v17.8
c0,3.6-1.9,5.7-5.2,5.7s-5.1-2-5.1-5.7V80.6c0-3.6,1.8-5.7,5.2-5.7c3.3,0,5.1,2.1,5.1,5.7v16.6h0.5c1.4-4.7,6.5-8,12.4-8
c8.6,0,13.8,5.4,13.8,14.4v22.4C506.3,129.7,504.4,131.7,501.1,131.7z"/>
</g>
<path id="heart_2_" class="st1" d="M154.3,89.2c-4.3,0-8.5,2.1-10.8,6.7c-2.3-4.6-6.5-6.7-10.8-6.7c-6.7,0-12.7,4.7-12.7,13.1
c0,14.2,19.4,21.4,23.5,29.9c4.1-8.5,23.5-15.7,23.5-29.9C167,93.3,160.3,89.2,154.3,89.2z M150.2,99.2c3,0,5.7,2,5.7,6
c0,6.3-8.3,10.6-12.8,15.2c-4.4-4.6-12.8-8.9-12.8-15.2c0-3.9,2.6-6,5.7-6c4.1,0,6.2,4.6,7,6.8C144,103.8,146.2,99.2,150.2,99.2z"
/>
</g>
<g id="PayPal">
<path class="st1" d="M888.7,89.7c-1.2,8-7.3,8-13.2,8h-3.4l2.4-14.9c0.1-0.9,0.9-1.6,1.8-1.6h1.5c4,0,7.8,0,9.7,2.3
C888.8,84.9,889.1,86.9,888.7,89.7 M886.1,68.9h-22.2c-1.5,0-2.8,1.1-3,2.6l-9,56.9c-0.2,1,0.5,2,1.5,2.1c0.1,0,0.2,0,0.3,0h11.4
c1.1,0,2-0.8,2.1-1.8l2.5-16.1c0.2-1.5,1.5-2.6,3.1-2.6h7c14.6,0,23.1-7.1,25.3-21.1c1-6.1,0-11-2.8-14.3
C899.2,70.9,893.6,68.9,886.1,68.9 M730.2,89.7c-1.2,8-7.3,8-13.2,8h-3.4l2.4-14.9c0.1-0.9,0.9-1.6,1.8-1.6h1.5c4,0,7.8,0,9.7,2.3
C730.3,84.9,730.7,86.9,730.2,89.7 M727.7,68.9h-22.2c-1.5,0-2.8,1.1-3.1,2.6l-9,56.9c-0.2,1,0.5,2,1.5,2.1c0.1,0,0.2,0,0.3,0h10.6
c1.5,0,2.8-1.1,3-2.6l2.4-15.4c0.2-1.5,1.5-2.6,3-2.6h7c14.6,0,23.1-7.1,25.3-21.1c1-6.1,0-11-2.8-14.3
C740.7,70.9,735.1,68.9,727.7,68.9 M779.3,110.2c-1,6.1-5.9,10.2-12,10.2c-3.1,0-5.6-1-7.1-2.9c-1.6-1.9-2.2-4.5-1.7-7.5
c1-6,5.9-10.2,11.9-10.2c3,0,5.5,1,7.1,2.9C779.1,104.5,779.7,107.2,779.3,110.2 M794.1,89.5h-10.6c-0.9,0-1.7,0.7-1.8,1.6l-0.5,3
l-0.7-1.1c-2.3-3.3-7.4-4.5-12.6-4.5c-11.8,0-21.8,8.9-23.7,21.4c-1,6.2,0.4,12.2,4,16.3c3.2,3.8,7.9,5.4,13.4,5.4
c9.5,0,14.7-6.1,14.7-6.1l-0.5,3c-0.2,1,0.5,2,1.5,2.1c0.1,0,0.2,0,0.3,0h9.6c1.5,0,2.8-1.1,3-2.6l5.8-36.4c0.2-1-0.5-2-1.5-2.1
C794.3,89.5,794.2,89.5,794.1,89.5 M937.7,110.2c-1,6.1-5.9,10.2-12,10.2c-3.1,0-5.6-1-7.1-2.9c-1.6-1.9-2.2-4.5-1.7-7.5
c1-6,5.9-10.2,11.9-10.2c3,0,5.5,1,7.1,2.9C937.5,104.5,938.2,107.2,937.7,110.2 M952.5,89.5h-10.6c-0.9,0-1.7,0.7-1.8,1.6l-0.5,3
l-0.7-1.1c-2.3-3.3-7.4-4.5-12.6-4.5c-11.8,0-21.8,8.9-23.7,21.4c-1,6.2,0.4,12.2,4,16.3c3.2,3.8,7.9,5.4,13.4,5.4
c9.5,0,14.7-6.1,14.7-6.1l-0.5,3c-0.2,1,0.5,2,1.5,2.1c0.1,0,0.2,0,0.3,0h9.6c1.5,0,2.8-1.1,3-2.6l5.8-36.4c0.2-1-0.5-2-1.5-2.1
C952.7,89.5,952.6,89.5,952.5,89.5 M850.7,89.5H840c-1,0-2,0.5-2.6,1.4l-14.7,21.7l-6.3-20.9c-0.4-1.3-1.6-2.2-3-2.2H803
c-1,0-1.9,0.8-1.9,1.9c0,0.2,0,0.4,0.1,0.6l11.8,34.5L802,142.1c-0.9,1.2,0,2.9,1.5,2.9h10.7c1,0,2-0.5,2.5-1.3l35.6-51.3
C853.1,91.1,852.2,89.5,850.7,89.5 M965.1,70.5l-9.1,58c-0.2,1,0.5,2,1.5,2.1c0.1,0,0.2,0,0.3,0h9.2c1.5,0,2.8-1.1,3-2.6l9-56.9
c0.2-1-0.5-2-1.5-2.1c-0.1,0-0.2,0-0.3,0h-10.3C966,68.9,965.2,69.6,965.1,70.5"/>
<g>
<defs>
<filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="575" y="47" width="83.1" height="98">
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
</defs>
<mask maskUnits="userSpaceOnUse" x="575" y="47" width="83.1" height="98" id="e_1_">
<g class="st2">
<path id="d_1_" class="st1" d="M575,47h83.1v98H575V47z"/>
</g>
</mask>
<path class="st3" d="M649.9,71.9c1.2-7.6,0-12.8-4.1-17.5c-4.5-5.2-12.7-7.4-23.2-7.4H592c-2.1,0-4,1.6-4.3,3.7L575,131.1
c-0.2,1.4,0.7,2.8,2.2,3c0.1,0,0.3,0,0.4,0h18.8l-1.3,8.2c-0.2,1.2,0.7,2.4,1.9,2.6c0.1,0,0.2,0,0.4,0h15.9c1.9,0,3.5-1.4,3.8-3.2
l0.2-0.8l3-18.9l0.2-1c0.3-1.9,1.9-3.2,3.8-3.2h2.4c15.4,0,27.4-6.2,30.9-24.3c1.5-7.5,0.7-13.8-3.2-18.3
C653,73.9,651.6,72.8,649.9,71.9"/>
<defs>
<filter id="Adobe_OpacityMaskFilter_1_" filterUnits="userSpaceOnUse" x="575" y="47" width="75.3" height="87.1">
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
</defs>
<mask maskUnits="userSpaceOnUse" x="575" y="47" width="75.3" height="87.1" id="g_1_">
<g class="st4">
<path id="f_1_" class="st1" d="M575,47h75.3v87.1H575V47z"/>
</g>
</mask>
<path class="st5" d="M649.9,71.9c1.2-7.6,0-12.8-4.1-17.5c-4.5-5.2-12.7-7.4-23.2-7.4H592c-2.1,0-4,1.6-4.3,3.7L575,131.1
c-0.2,1.4,0.7,2.8,2.2,3c0.1,0,0.3,0,0.4,0h18.8l4.7-30l-0.1,0.9c0.3-2.1,2.1-3.7,4.3-3.7h8.9c17.6,0,31.3-7.1,35.3-27.8
C649.7,73.1,649.8,72.5,649.9,71.9"/>
<defs>
<filter id="Adobe_OpacityMaskFilter_2_" filterUnits="userSpaceOnUse" x="575" y="47" width="75.3" height="87.1">
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
</defs>
<mask maskUnits="userSpaceOnUse" x="575" y="47" width="75.3" height="87.1" id="i_1_">
<g class="st6">
<path id="h_1_" class="st1" d="M575,47h75.3v87.1H575V47z"/>
</g>
</mask>
<path class="st7" d="M606.2,72c0.3-1.9,1.9-3.2,3.8-3.2h23.9c2.8,0,5.5,0.2,7.9,0.6c0.7,0.1,1.4,0.2,2,0.4
c0.9,0.2,1.9,0.5,2.8,0.8c1.1,0.4,2.2,0.8,3.3,1.4c1.2-7.6,0-12.8-4.1-17.5c-4.5-5.2-12.7-7.4-23.2-7.4H592c-2.1,0-4,1.6-4.3,3.7
L575,131.1c-0.2,1.4,0.7,2.8,2.2,3c0.1,0,0.3,0,0.4,0h18.8l4.7-30L606.2,72L606.2,72z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

+8
View File
@@ -1,5 +1,13 @@
// Add page - handles both sets and individual minifigures
document.addEventListener("DOMContentLoaded", () => {
// Initialize date pickers
document.querySelectorAll('[data-add-date="true"]').forEach(el => {
new Datepicker(el, {
buttonClass: 'btn',
format: 'yyyy/mm/dd',
});
});
// Get template data from data attributes
const addContainer = document.getElementById('add-set');
if (!addContainer) return;
+52
View File
@@ -16,9 +16,13 @@ class BrickSetSocket extends BrickSocket {
this.html_input = document.getElementById(`${id}-set`);
this.html_no_confim = document.getElementById(`${id}-no-confirm`);
this.html_owners = document.getElementById(`${id}-owners`);
this.html_purchase_date = document.getElementById(`${id}-purchase-date`);
this.html_purchase_location = document.getElementById(`${id}-purchase-location`);
this.html_purchase_price = document.getElementById(`${id}-purchase-price`);
this.html_statuses = document.getElementById(`${id}-statuses`);
this.html_storage = document.getElementById(`${id}-storage`);
this.html_tags = document.getElementById(`${id}-tags`);
this.html_description = document.getElementById(`${id}-description`);
// Card elements
this.html_card = document.getElementById(`${id}-card`);
@@ -181,12 +185,44 @@ class BrickSetSocket extends BrickSocket {
this.html_progress_bar.scrollIntoView();
}
// Grab the purchase date
let purchase_date = '';
if (this.html_purchase_date) {
purchase_date = this.html_purchase_date.value;
}
// Grab the purchase price
let purchase_price = '';
if (this.html_purchase_price) {
purchase_price = this.html_purchase_price.value;
}
// Grab the statuses
const statuses = [];
if (this.html_statuses) {
this.html_statuses.querySelectorAll('input').forEach(input => {
if (input.checked) {
statuses.push(input.value);
}
});
}
// Grab the description/notes
let description = '';
if (this.html_description) {
description = this.html_description.value;
}
this.socket.emit(this.messages.IMPORT_SET, {
set: (set !== undefined) ? set : this.html_input.value,
owners: owners,
purchase_date: purchase_date,
purchase_location: purchase_location,
purchase_price: purchase_price,
statuses: statuses,
storage: storage,
tags: tags,
description: description,
refresh: this.refresh
});
} else {
@@ -361,10 +397,22 @@ class BrickSetSocket extends BrickSocket {
this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled);
}
if (this.html_purchase_date) {
this.html_purchase_date.disabled = !enabled;
}
if (this.html_purchase_location) {
this.html_purchase_location.disabled = !enabled;
}
if (this.html_purchase_price) {
this.html_purchase_price.disabled = !enabled;
}
if (this.html_statuses) {
this.html_statuses.querySelectorAll('input').forEach(input => input.disabled = !enabled);
}
if (this.html_storage) {
this.html_storage.disabled = !enabled;
}
@@ -373,6 +421,10 @@ class BrickSetSocket extends BrickSocket {
this.html_tags.querySelectorAll('input').forEach(input => input.disabled = !enabled);
}
if (this.html_description) {
this.html_description.disabled = !enabled;
}
if (this.html_card_confirm) {
this.html_card_confirm.disabled = !enabled;
}
+81 -50
View File
@@ -63,30 +63,43 @@
</div>
<h6 class="border-bottom mt-2">Metadata</h6>
<div class="accordion accordion" id="metadata">
{% if not (brickset_owners | length) and not (brickset_purchase_locations | length) and not (brickset_storages | length) and not (brickset_tags | length) %}
<div class="alert alert-warning" role="alert">
You have no metadata configured.
You can add entries in the <a href="{{ url_for('admin.admin', open_metadata=true) }}" class="btn btn-warning" role="button"><i class="ri-profile-line"></i> Set metadata management</a> section of the Admin panel.
{% if brickset_owners | length %}
{{ accordion.header('Owners', 'owners', 'metadata', icon='user-line') }}
<div id="add-owners">
{% for owner in brickset_owners %}
{% with id=owner.as_dataset() %}
<div class="form-check">
<input class="form-check-input" type="checkbox" value="{{ owner.fields.id }}" id="{{ id }}" autocomplete="off">
<label class="form-check-label" for="{{ id }}">{{ owner.fields.name }}</label>
</div>
{% endwith %}
{% endfor %}
</div>
{% else %}
{% if brickset_owners | length %}
{{ accordion.header('Owners', 'owners', 'metadata', icon='user-line') }}
<div id="add-owners">
{% for owner in brickset_owners %}
{% with id=owner.as_dataset() %}
<div class="form-check">
<input class="form-check-input" type="checkbox" value="{{ owner.fields.id }}" id="{{ id }}" autocomplete="off">
<label class="form-check-label" for="{{ id }}">{{ owner.fields.name }}</label>
</div>
{% endwith %}
{% endfor %}
</div>
{{ accordion.footer() }}
{% endif %}
{% if brickset_purchase_locations | length %}
{{ accordion.header('Purchase location', 'purchase-location', 'metadata', icon='building-line') }}
<label class="visually-hidden" for="add-purchase-location">{{ name }}</label>
{{ accordion.footer() }}
{% endif %}
{{ accordion.header('Purchase', 'purchase', 'metadata', icon='wallet-3-line') }}
<div class="alert alert-info" role="alert">The expected date format here is <code>yyyy/mm/dd</code> (year/month/day), but you can configure how it is displayed in the set card with the <code>PURCHASE_DATE_FORMAT</code> variable.</div>
<div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
<div class="col-12">
<label for="add-purchase-date" class="form-label visually-hidden">Date</label>
<div class="input-group">
<span class="input-group-text px-1"><i class="ri-calendar-line me-1"></i><span class="ms-1 d-none d-md-inline"> Date</span></span>
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" id="add-purchase-date" data-add-date="true" autocomplete="off">
</div>
</div>
<div class="col-12">
<label for="add-purchase-price" class="form-label visually-hidden">Price</label>
<div class="input-group">
<span class="input-group-text px-1"><i class="ri-wallet-3-line me-1"></i><span class="ms-1 d-none d-md-inline"> Price</span></span>
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" id="add-purchase-price" autocomplete="off">
{% if config['PURCHASE_CURRENCY'] %}<span class="input-group-text d-none d-md-inline px-1">{{ config['PURCHASE_CURRENCY'] }}</span>{% endif %}
</div>
</div>
{% if brickset_purchase_locations | length %}
<div class="col-12 flex-grow-1">
<label for="add-purchase-location" class="form-label visually-hidden">Location</label>
<div class="input-group">
<span class="input-group-text px-1"><i class="ri-building-line me-1"></i><span class="ms-1 d-none d-md-inline"> Location</span></span>
<select id="add-purchase-location" class="form-select" autocomplete="off">
<option value="" selected><i>None</i></option>
{% for purchase_location in brickset_purchase_locations %}
@@ -94,35 +107,53 @@
{% endfor %}
</select>
</div>
{{ accordion.footer() }}
{% endif %}
{% if brickset_storages | length %}
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
<label class="visually-hidden" for="add-storage">{{ name }}</label>
<div class="input-group">
<select id="add-storage" class="form-select" autocomplete="off">
<option value="" selected><i>None</i></option>
{% for storage in brickset_storages %}
<option value="{{ storage.fields.id }}">{{ storage.fields.name }}</option>
{% endfor %}
</select>
</div>
{{ accordion.footer() }}
{% endif %}
{% if brickset_tags | length %}
{{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }}
<div id="add-tags">
{% for tag in brickset_tags %}
{% with id=tag.as_dataset() %}
<div class="form-check">
<input class="form-check-input" type="checkbox" value="{{ tag.fields.id }}" id="{{ id }}" autocomplete="off">
<label class="form-check-label" for="{{ id }}">{{ tag.fields.name }}</label>
</div>
{% endwith %}
{% endfor %}
</div>
{{ accordion.footer() }}
</div>
{% endif %}
</div>
{{ accordion.footer() }}
{{ accordion.header('Notes', 'notes', 'metadata', icon='sticky-note-line') }}
<textarea class="form-control" id="add-description" rows="3" placeholder="Optional notes..."></textarea>
{{ accordion.footer() }}
{% if brickset_storages | length %}
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
<label class="visually-hidden" for="add-storage">Storage</label>
<div class="input-group">
<select id="add-storage" class="form-select" autocomplete="off">
<option value="" selected><i>None</i></option>
{% for storage in brickset_storages %}
<option value="{{ storage.fields.id }}">{{ storage.fields.name }}</option>
{% endfor %}
</select>
</div>
{{ accordion.footer() }}
{% endif %}
{% if brickset_statuses | length %}
{{ accordion.header('Statuses', 'statuses', 'metadata', icon='checkbox-circle-line') }}
<div id="add-statuses">
{% for status in brickset_statuses %}
{% with id=status.as_dataset() %}
<div class="form-check">
<input class="form-check-input" type="checkbox" value="{{ status.fields.id }}" id="{{ id }}" autocomplete="off">
<label class="form-check-label" for="{{ id }}">{{ status.fields.name }}</label>
</div>
{% endwith %}
{% endfor %}
</div>
{{ accordion.footer() }}
{% endif %}
{% if brickset_tags | length %}
{{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }}
<div id="add-tags">
{% for tag in brickset_tags %}
{% with id=tag.as_dataset() %}
<div class="form-check">
<input class="form-check-input" type="checkbox" value="{{ tag.fields.id }}" id="{{ id }}" autocomplete="off">
<label class="form-check-label" for="{{ id }}">{{ tag.fields.name }}</label>
</div>
{% endwith %}
{% endfor %}
</div>
{{ accordion.footer() }}
{% endif %}
</div>
<hr>