mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-22 01:52:17 -05:00
The locations board could only ever answer one question: where is this spool. The same card-and-drag interaction works for any field a spool carries, so make the field a view mode and keep the rest. Location is still the default view and behaves exactly as before. The other views come from the user's own spool fields: any single-value text or choice field now appears in a Group-by menu. A choice field renders a card per choice, including the empty ones, which turns a "Status" field into a kanban board. Which field a card belongs to is decided by one small interface (src/lib/dashboard/fields.ts), so the page never special-cases a field. A field only qualifies if the SPOOL OWNS IT, because a drag moves one spool and must mean exactly one change. That rules out material and vendor, where writing one spool's card would silently rewrite every other spool of the same filament, and lot number, which is stamped on by the manufacturer and would be recording something untrue. Those stay library filters. It also rules out numbers, dates, booleans and multi-choice, whose values don't name a card you can drag into. Backing changes: - /spool/group grows group_by=extra.<key> for a spool's custom fields. The field table is aliased so it stays a distinct FROM element -- the extra-field filters reference the un-aliased table inside correlated subqueries, and without the alias those would correlate to this join instead of standing alone. Grouping is rejected for field types whose value isn't a single repeatable string. group_by is no longer a Literal, so an unsupported value is a 400 rather than a 422. - The saved layout is per field: dashboard_groups and dashboard_spoolorders replace the location-only pair. The old `locations` / `locations_spoolorders` are read once as a seed for the location view and never written back, so an upgrade keeps the shelves it had and the old client keeps working. - The view mode lives in the URL (?by=) so a board can be bookmarked and shared, and is mirrored to localStorage so a bare /dashboard resumes the last one looked at. The URL wins when it says something; a stale ?by= naming a deleted field falls back to location. Verified against SQLite, PostgreSQL, MariaDB and CockroachDB -- Postgres caught a real difference here, it sorts NULLs last where SQLite sorts them first, so the new group assertions are keyed rather than ordered.
83 lines
3.1 KiB
Python
83 lines
3.1 KiB
Python
"""Settings that can be changed by the user.
|
|
|
|
All settings are JSON encoded and stored in the database.
|
|
"""
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
|
|
class SettingType(Enum):
|
|
"""The type of a setting."""
|
|
|
|
BOOLEAN = "boolean"
|
|
NUMBER = "number"
|
|
STRING = "string"
|
|
ARRAY = "array"
|
|
OBJECT = "object"
|
|
|
|
|
|
@dataclass
|
|
class SettingDefinition:
|
|
"""A setting that can be changed by the user."""
|
|
|
|
key: str
|
|
type: SettingType
|
|
default: str
|
|
|
|
def validate_type(self, value: str) -> None: # noqa: C901
|
|
"""Validate that the value has the correct type."""
|
|
obj = json.loads(value)
|
|
if self.type == SettingType.BOOLEAN:
|
|
if not isinstance(obj, bool):
|
|
raise ValueError(f"Setting {self.key} must be a boolean.")
|
|
elif self.type == SettingType.NUMBER:
|
|
if not isinstance(obj, (int, float)):
|
|
raise ValueError(f"Setting {self.key} must be a number.")
|
|
elif self.type == SettingType.STRING:
|
|
if not isinstance(obj, str):
|
|
raise ValueError(f"Setting {self.key} must be a string.")
|
|
elif self.type == SettingType.ARRAY:
|
|
if not isinstance(obj, list):
|
|
raise ValueError(f"Setting {self.key} must be an array.")
|
|
elif self.type == SettingType.OBJECT: # noqa: SIM102
|
|
if not isinstance(obj, dict):
|
|
raise ValueError(f"Setting {self.key} must be an object.")
|
|
|
|
|
|
SETTINGS: dict[str, SettingDefinition] = {}
|
|
|
|
|
|
def register_setting(key: str, typ: SettingType, default: str) -> None:
|
|
"""Register a setting."""
|
|
SETTINGS[key] = SettingDefinition(key, typ, default)
|
|
|
|
|
|
def parse_setting(key: str) -> SettingDefinition:
|
|
"""Parse a setting key."""
|
|
if key not in SETTINGS:
|
|
raise ValueError(f"Setting {key} does not exist.")
|
|
return SETTINGS[key]
|
|
|
|
|
|
register_setting("currency", SettingType.STRING, json.dumps("EUR"))
|
|
register_setting("round_prices", SettingType.BOOLEAN, json.dumps(obj=False))
|
|
register_setting("print_presets", SettingType.ARRAY, json.dumps([]))
|
|
register_setting("label_designs", SettingType.ARRAY, json.dumps([]))
|
|
|
|
register_setting("extra_fields_vendor", SettingType.ARRAY, json.dumps([]))
|
|
register_setting("extra_fields_filament", SettingType.ARRAY, json.dumps([]))
|
|
register_setting("extra_fields_spool", SettingType.ARRAY, json.dumps([]))
|
|
register_setting("base_url", SettingType.STRING, json.dumps(""))
|
|
|
|
register_setting("locations", SettingType.ARRAY, json.dumps([]))
|
|
register_setting("locations_spoolorders", SettingType.OBJECT, json.dumps({}))
|
|
|
|
# The dashboard groups spools by a field the user picks (location, lot_nr, material, or a spool
|
|
# extra field), so its layout is stored per field rather than per location like the two settings
|
|
# above. dashboard_groups maps a field key to its ordered group keys ("" being the unset group);
|
|
# dashboard_spoolorders maps a field key to a group key to its ordered spool ids.
|
|
register_setting("dashboard_groups", SettingType.OBJECT, json.dumps({}))
|
|
register_setting("dashboard_spoolorders", SettingType.OBJECT, json.dumps({}))
|