Add filtering and sorting for custom fields - #773
Conversation
|
Is there something I can do to move this forward or make this ready for consideration? :) |
|
I've cloned daften repo and i'm using this to filter extra.nfc_id created by filaman. Works great to retieve the spool id to activate the spool in moonraker. |
|
+1 for this to merge into master :) |
|
Something I missed before and noticed when starting to rebase, the integration tests for postgres fail. There were also significant issues in the tests written. i've fixed the tests, but haven't had time to fix the issues with postgres, which is because of json_extract. I'll come back to that at a later time |
1ae07d5 to
f545ae5
Compare
|
Actually was able to fix it and the tests seem to run well and I was able to rebase everything on master. All feedback welcome :) |
f545ae5 to
33c669e
Compare
|
I tested some more, fixed some bugs and added test coverage for more scenario's :) |
|
@akira69 I think you mean #846 instead of #856? i also don't see in your PR's what is encompassed here, searching on any custom field added by the user. You added a lot of other information in structured data, but this seems more generic (unless I'm looking over that part). Can you point me to where your PR's encompass what's in this PR and why this one would be superfluous? |
yes #846: fixed that in the comment. |
|
@akira69 while your intentions are noble, I have 2 small issues with your approach:
So I'm wondering what is your goal with those big PR's? Because you give a comment here that your PR makes mine no longer necessary, but you didn't even test your functionality. |
|
you're right - I had the worry about the scale of changes as they grew - maybe there's a way to separate the different features/improvements into individual PRs without too much work. probably the best approach. Especially for testing - That I will consider and see what I can do to simplify. Unfortunately I didn't look for what was already done in the PR list, an omission on my part and not a intention to bull over what's already done. Would have been smarter to build upon. Nevertheless, seems the implementation is similar to your approach. While there's no intention to disrespect, it's easy to see how it looks that way. The scope-creep express was over-eager. Lesson Learned. I've updated the PR for now to reference your code as prior art. At this point, rebase to #773 and build up looks very difficult. Let's see. |
|
I appreciate the feedback and understanding. Don't let my (hopefully constructive) feedback to you discourage you from contributing, even with mistakes and misunderstandings in communication, it's valuable to contribute :) The main thing I'd still advise to do is to split it in more granular items, if that's possible. Mainly because I have contributed to and maintained some projects, and reviewing big chunks can be very cumbersome. I'm closing off for today, have a great night @akira69 |
|
@daften Hallo mein Freund! Ich spreche ein bisschen Deutsch und glaube, dass du Deutscher bist. Wohnst du in Deutschland? Ich habe drei Jahre in Deutschland gelebt, in der Nähe von Nürnberg. Jetzt bin ich in Chicago. Aber, Englisch ist besser fur mich: Check #858 Therefore the suggestion is to fold commit Why this matters:
Scope is intentionally small (2 files) and can be cherry-picked cleanly from If this is folded into #773, #858 can be closed as superseded. |
|
And, by the way, I've split up my 2 or 3 PRs to a whole bunch of separate smaller ones - thanks for the tips |
|
@akira69 I've cherry-picked the commit you indicated and also fixed the linting issues that were present in the frontend because of all the commits. I wasn't able to validate it, it's midnight here now, but I'm trusting what you wrote :) Thanks for notifying so I can update this PR, I really appreciate it! I won't close any other PR (#858), I'll leave it to you to decide on that and do it. I'm also seeing the separate PR's, I think that makes things easier. I hope at least. Good luck in Chicage, I hope you can stay safe, with all the news I see, I worry sometimes. |
| def get_field_table_for_entity(entity_type: Any) -> Type[models.Base]: | ||
| """Get the field table class for a given entity type.""" | ||
| # Import here to avoid circular imports | ||
| from spoolman.extra_fields import EntityType |
There was a problem hiding this comment.
importing stuff like this is never the correct answer, fix the circular dependency issues properly by rearranging in the files/adding new files if necessary
There was a problem hiding this comment.
Fixed in 9711495.
I removed the local imports that were acting as the circular-import workaround and split the responsibilities into dedicated modules instead, so the query paths can import them directly without importing inside function bodies. Helper branch: akira69/Spoolman_Labels:feat/pr773-review-feedback.
| value_parts = value.split(",") | ||
|
|
||
| # Handle filtering for empty values | ||
| if any(p == "<empty>" or len(p) == 0 for p in value_parts): |
There was a problem hiding this comment.
empty strings are used to filter empty values, this "empty>" is an unnecessary new concept
There was a problem hiding this comment.
Fixed across 9711495 and 3208f8d.
I kept <empty> only as the dropdown label for readability, but restored the backend/API contract to the existing empty-string semantics. So the UI label is still <empty>, but the actual filter value sent to the API is now "".
| # Condition A subquery | ||
| empty_conditions = [ | ||
| field_table.value.is_(None), | ||
| field_table.value == "null", |
There was a problem hiding this comment.
see add_where_clause_str on how empty values should be handled to stay consistent with the rest of the API
There was a problem hiding this comment.
Fixed in 9711495.
The extra-field path now follows the same empty-value semantics as the rest of the API: empty string means missing or unset value, including both missing rows and effectively empty/null stored values.
| try: | ||
| conditions.append(field_table.value == json.dumps(int(value_part))) | ||
| except ValueError: | ||
| pass |
There was a problem hiding this comment.
silently failing if bad value is not good api design
There was a problem hiding this comment.
Fixed in 9711495.
Invalid custom-field filter values no longer get ignored or silently coerced. The API now returns 400 for invalid integer, float, boolean, and range inputs. I also added coverage for the invalid integer/boolean cases and verified the behavior locally against a running app.
…e filter/sort - Add comprehensive tests covering all 9 field types (text, integer, float, boolean, single-choice, multi-choice, datetime, integer_range, float_range) for filter and sort on spool, filament, and vendor entities - Add invalid-filter 400 tests for float, integer_range, and float_range - Fix integer_range/float_range filter to use LIKE pattern matching against Python's deterministic json.dumps output instead of fragile .contains() - Add integer_range/float_range sort support via a @compiles helper (_JsonArrayFirstElement) that emits CAST(col AS JSON)->>0 on PostgreSQL and JSON_EXTRACT(col, '$[0]') on SQLite/MariaDB - Add logger and __all__ to extra_fields.py (aligns with PR Donkie#893) - All tests verified passing on postgres, sqlite, and mariadb
- Add text input filter dropdown for text fields (substring search) - Add range input filter dropdowns (min/max) for integer, float, integer_range, and float_range fields - Add datetime picker filter dropdown for datetime fields - Fix boolean "No" filter to use empty value so it correctly matches unset/false fields - Fix integer_range/float_range filter to use numeric comparisons instead of LIKE-based exact matching (stored_min >= filter_min, stored_max <= filter_max) - Add range filter support for integer/float fields (min:max format) - Add _JsonArraySecondElement cross-database helper for extracting second JSON array element
…ntics Backend: - Add _JsonArraySecondElement cross-database helper (mirrors _JsonArrayFirstElement) - integer_range/float_range filter now uses numeric comparisons: stored_min >= filter_min and stored_max <= filter_max (was LIKE exact match) - integer/float filter now supports min:max range format in addition to exact match: stored_value >= min and/or stored_value <= max Frontend: - integer and float fields now use a range filter dropdown (min/max inputs) instead of a single exact-value input Tests: - Fix integer_range/float_range spool and vendor tests broken by new >= semantics (filter values updated to discriminate between test entries) - Add range filter tests (min only, max only, both) for integer and float fields
Introduce a _create_entity helper and @pytest.mark.parametrize("entity_type",
["spool", "filament", "vendor"]) to run each numeric field type test against
all three entity types from a single test function.
- Removes 12 individual tests (integer/float/integer_range/float_range × 3 entities)
- Adds 4 parametrized tests covering the same ground plus the previously missing
min-only / max-only range filter cases for filament and vendor
- Uses try/finally for cleanup so entities are always deleted even on assertion failure
126b199 to
f718d8a
Compare
- Backend: add datetime range filter using '|' separator (ISO dates contain ':') - Backend: restore integer/float range filter support (lost during attribution rewrite) - Frontend: replace single DateTimePicker with From/To range pickers for datetime fields
The DateTimeRangeFilterDropdown was initializing pickers with dayjs.utc(), causing them to display and accept UTC times directly. The entry form shows local time and converts to UTC, so the filter was inconsistent: entering the same displayed time would produce a filter value 2h offset from what was stored (in UTC+2), making exact boundaries fail unexpectedly. Fix: initialize picker values with dayjs() (local mode) so the filter picker behaves the same as the entry form — shows local times and converts to UTC. Tests: replace 3 separate entity-specific datetime tests with one parametrized test covering spool/filament/vendor, and add range filter cases (start|, |end, start|end).
Replace 16 separate spool/filament/vendor tests for text, boolean, single-choice, multi-choice, and empty-filter with 5 parametrized tests that run against all three entity types. Each test is now defined once and executed 3 times, eliminating copy-paste and ensuring consistent coverage across all entity types. Parametrize invalid-filter-value 400 tests across all entity types The non-numeric-value error uses "Invalid integer/float range filter value" while the missing-colon error uses "Invalid range filter value". Both contain "range filter value", so use that as the assertion substring.
All three custom filter dropdowns (text, number range, datetime range) now match the built-in choice filter footer: a border-top separator, Reset as a link button on the left (disabled when no filter is active), and OK as a primary button on the right.
|
I have incorporated the proposed changed by @akira69 and added on them.
I think all feedback was covered, please let me know if anything is missing! Thanks @akira69 for the helping hand and @Donkie for the detailed feedback :) |
…-delta, calibration (#2) * Remove forced vertical scrollbar from location spool containers Drops the always-on overflow-y: scroll from .loc-container .location so the locations view no longer shows a permanent scrollbar. Ported from upstream Donkie#859 by @VeeBack. Co-authored-by: VeeBack <VeeBack@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Use configured external DB sync interval instead of hardcoded default schedule_tasks scheduled the cyclic sync with DEFAULT_SYNC_INTERVAL instead of the user-configured sync_interval value, so the configured interval had no effect. Use sync_interval. Ported from upstream Donkie#943 by @chruoss. Co-authored-by: chruoss <chruoss@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Allow including archived spools in spool export Adds an allow_archived query parameter to the spool export endpoint, passed through to spool.find(allow_archived=...), which already supports it. Defaults to False to preserve existing behavior. Ported from upstream Donkie#919 by @velzi. Co-authored-by: velzi <velzi@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Publish spoolman_build_info Prometheus metric at startup Adds a prometheus_client Info metric exposing version, commit and build_date so build provenance is scrapeable. Ported from upstream Donkie#877 by @sw1nn. Co-authored-by: sw1nn <sw1nn@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * docs: add spoolman-mcp server to the integrations list README documentation only. Ported from upstream Donkie#890 by @Disane87. Co-authored-by: Disane87 <Disane87@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Add Docker HEALTHCHECK and a sample docker-compose.yml Adds a container HEALTHCHECK that probes /api/v1/health (which exists) using the bundled Python interpreter, plus a sample docker-compose.yml. Ported from upstream Donkie#908 by @meek2100. Co-authored-by: meek2100 <meek2100@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Reload page on 401 to recover from forward-auth proxies Adds an axios response interceptor that reloads the SPA on 401 for idempotent requests (with a cooldown and service-worker unregister) so forward-auth proxies can re-run their login flow. Ported from upstream Donkie#924 by @sherrmann. Co-authored-by: sherrmann <sherrmann@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Handle comma decimal separators in numeric inputs Wires the existing comma-aware formatNumberOnUserInput / numberParser(AllowEmpty) helpers into the remaining InputNumber fields across filaments, spools, vendors, printing, extra fields and settings. Ported from upstream Donkie#948 by @nkgotcode. Co-authored-by: nkgotcode <nkgotcode@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Update client npm dependencies (upstream dependabot bumps) Regenerates client/package-lock.json to pull the security/patch bumps from the upstream dependabot PRs. uuid required a major bump (only used via the stable `v4` export). Resulting versions meet or exceed each PR's target: uuid 13 -> 14.0.1 (Donkie#927) axios -> 1.18.1 (Donkie#928) vite -> 7.3.6 (Donkie#906) i18next-http-backend -> 3.0.6 (Donkie#920) qs -> 6.14.2 (Donkie#850) minimatch (transitive bump) (Donkie#873) flatted -> 3.4.2 (Donkie#889) handlebars -> 4.7.9 (Donkie#894) lodash-es -> 4.18.1 (Donkie#901) lodash -> 4.18.1 (Donkie#909) follow-redirects -> 1.16.0 (Donkie#914) fast-uri -> 3.1.2 (Donkie#930) @babel/plugin-transform-modules-systemjs 7.29.7 (Donkie#931) Verified with `npm run build` (tsc typecheck + refine/vite build). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Update Python dependencies cryptography and mako (upstream dependabot bumps) Bumps two transitive dependencies in uv.lock to the security/patch versions targeted by the upstream dependabot PRs: cryptography 46.0.3 -> 46.0.7 (Donkie#907) mako 1.3.10 -> 1.3.12 (Donkie#929) Pinned to the dependabot targets rather than the newest available (cryptography 49.x) to keep the change to a patch-level security update. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Address Copilot review feedback on numeric inputs and 401 handler - numberFormatter: render a legitimate 0 as "0" instead of a blank field. The previous truthiness check formatted 0 as an empty string, so inputs where 0 is common (printing margins/spacing) looked blank / like state loss when not focused. Now only undefined/empty/non-finite values blank out. - InputNumberRange.parseInputNumberValue: parse string values (antd typings allow them, e.g. stringMode) instead of dropping them to null, while still treating empty/null as a deliberate clear. - authReloadHandler: guard the axios response interceptor against double-registration. Vite/React fast refresh can re-evaluate the module and stack duplicate interceptors, firing multiple reloads per 401; the flag now lives on the shared axios instance so it survives module re-evaluation. Verified with npm run build (tsc typecheck + vite build) and prettier. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Format spoolman/main.py with ruff to fix style CI The Prometheus BUILD_INFO block (ported from upstream Donkie#877) wasn't ruff-format compliant, failing the `style` job's `ruff format --check`. Wrap the BUILD_INFO.info({...}) dict argument per ruff's formatting. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Include weight_delta in spool update websocket events Adds an optional payload_extras field to SpoolEvent and passes {"weight_delta": weight} from use_weight/use_length so websocket consumers can see the change amount. Follow-up to upstream Donkie#689. Ported from upstream Donkie#902 by @chof747. Co-authored-by: chof747 <chof747@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Add filament import from 3dfilamentprofiles.com Adds a 3D Filament Profiles ID input to the filament create/edit forms and a backend proxy endpoint to fetch and map that profile data. Resolves upstream Donkie#939. Ported from upstream Donkie#940 by @t0ny-peng. Co-authored-by: t0ny-peng <t0ny-peng@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Add filtering and sorting for extra/custom fields in table views Ports upstream Donkie#773: extra fields can now be shown, sorted, and filtered in the spool/filament/vendor tables. Adds an extra-field query subsystem (extra_field_query.py, extra_field_registry.py) and the frontend filtering/ sorting utilities and column filter dropdowns. Choice and boolean fields get filter options; all extra field types support filtering for empty values. Chosen over the overlapping Donkie#904 (filtering-only) as the more complete superset implementation. Ported from upstream Donkie#773 by @daften. Co-authored-by: daften <daften@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Conform ported feature PRs to fork lint/format/type rules Fixes issues surfaced by the local CI checks (ruff, eslint, prettier, build) on the three ported feature PRs so the branch passes the style job: - spool.py (Donkie#902): fix a real NameError -- spool_changed used Optional[dict] without importing Optional. Switched to "dict | None" (codebase style); the app failed to import before this. - externaldb.py (Donkie#940): hoist inline imports to module top, narrow the blind except to httpx.HTTPError, use logging.exception + "raise ... from e", add the return annotation, wrap long lines, and noqa the complexity warnings (consistent with find()). - filaments/edit.tsx (Donkie#940): replace an "as any" cast (eslint no-explicit-any) with a cast to setFieldsValue's parameter type. - ruff format + prettier formatting on the remaining ported files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Fix CockroachDB extra-field filtering and address review feedback - extra_field_query.py: the int/float-range extra-field filters compiled to json_extract(), which CockroachDB lacks. CockroachDB's SQLAlchemy dialect name is "cockroachdb" (not "postgresql"), so the existing PG @compiles variant didn't apply and it fell back to the json_extract default -- test (cockroachdb) failed with "unknown function: json_extract()". Add a cockroachdb variant emitting CAST(value AS JSONB)->>N. Verified the generated SQL for sqlite/mysql/postgresql/cockroachdb locally. - filaments/create.tsx, edit.tsx: await importFilament so errors propagate to the catch and the success toast only shows after the import completes. - externaldb.py: reject a non-numeric profile_id before interpolating it into the upstream URL (prevents path injection / unintended fetches). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Add filament calibration feature Ports upstream Donkie#855: a calibration module with DB tables, REST API, and a multi-step calibration wizard UI. Includes two Alembic migrations (c3a7f2e8b091 calibration_tables, a1b2c3d4e5f6 calibration_filament_fk) that chain from the current head, and integration tests. Ruff-formatted to the fork's style; verified imports and a single Alembic head. Ported from upstream Donkie#855 by @SmoothBrainIT. Co-authored-by: SmoothBrainIT <SmoothBrainIT@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Fix calibration migration for PostgreSQL/CockroachDB/MariaDB The a1b2c3d4e5f6 (calibration_filament_fk) migration used batch_alter_table(recreate="always"), which forces Alembic's SQLite-style table-copy workflow on every backend. On PostgreSQL/CockroachDB/MariaDB the copy drops calibration_session's primary key, which calibration_step_result's foreign key depends on -- "cannot drop constraint calibration_session_pkey because other objects depend on it" -- so app startup (alembic upgrade) failed and test (postgres) went red. Use recreate="auto": only SQLite recreates the table (it must); the other backends apply direct ALTERs that don't touch the PK. Verified the full upgrade chain on a fresh SQLite DB locally; CI validates the other three. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Fix calibration session cascade-delete on SQLite test (sqlite) failed test_delete_filament_cascades_to_sessions: deleting a filament left its calibration sessions orphaned (expected 404, got 200). The codebase does not enable SQLite's PRAGMA foreign_keys, so DB-level ON DELETE CASCADE is not enforced there -- cascades are done at the ORM level instead. The Filament model had no relationship to CalibrationSession, so nothing deleted the sessions on SQLite (the other three DBs enforce the FK, so they passed). Add Filament.calibration_sessions with cascade "save-update, merge, delete, delete-orphan" (matching the extra-field pattern) plus the CalibrationSession.filament back-reference. The delete path loads relations via joinedload("*"), so the ORM cascade fires on every backend. Verified ruff, imports, the relationship registration, and the full SQLite migration chain locally; CI validates the cascade test across all four databases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Fix calibration migration column-drop for MySQL/MariaDB After switching to recreate="auto", MariaDB failed alembic upgrade with "Cannot drop index 'spool_id': needed in a foreign key constraint" -- MySQL/ MariaDB refuse to drop a column still referenced by a foreign key, whereas PostgreSQL/CockroachDB drop the FK together with the column. Drop the spool_id FK explicitly before dropping the column. It was created unnamed, so reflect its real name; skip on SQLite (no named FK there, and the batch recreate handles it). Apply the same guard to the downgrade for the named filament_id FK. Verified upgrade + downgrade + re-upgrade on a fresh SQLite DB locally; CI validates MariaDB/PostgreSQL/CockroachDB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Collapse calibration migrations to create filament_id directly The calibration feature shipped two migrations: create calibration_session with spool_id, then swap spool_id -> filament_id. That swap was an artifact of the PR's development history and broke differently on every backend (PostgreSQL pkey drop, MariaDB drop-column-with-FK, CockroachDB DDL+DML in one transaction). Calibration tables are brand new, so there is no data to migrate. Create calibration_session with filament_id (FK filament.id ON DELETE CASCADE) directly in the first migration and delete the second migration. Verified on a fresh SQLite DB: single head c3a7f2e8b091, calibration_session has filament_id and no spool_id. The ORM cascade relationship added earlier is unchanged, so filament deletion still cascades to sessions on SQLite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Fix correctness issues found in review (calibration, filtering, weight events) Backend: - spool.delete: commit in-request like filament.delete. Fixes a deterministic Postgres StaleDataError 500 when deleting a filament after a websocket+use flow (uncommitted spool delete left a phantom row in the next request's unit of work). - use_weight_safe: return the actually-applied delta (after clamping at 0) and broadcast that as weight_delta instead of the requested weight. Consumption (weight>=0, the concurrent hot path) stays a single atomic UPDATE with no read-before-write, so concurrent /use no longer loses updates on MariaDB/CockroachDB; only refills (weight<0) read the prior value to report the clamped delta. - calibration.list_sessions: load steps via selectinload, not joinedload, so LIMIT/OFFSET paginate sessions rather than joined step rows. - calibration delete_session/delete_step_result: commit in-request. - extra-field float exact-match filter: match both "5" and "5.0" so whole-number floats stored verbatim are found. - export_spools: default allow_archived to True (backups include archived spools). - Sorting: shared parse_sort() helper rejects malformed sort params with 400 instead of an uncaught 500 (spool/filament/vendor). Frontend: - Calibration VFA step: stop nulling restored/typed min/max_avoidance_speed when the artifact-speeds list is empty (was wiping saved values on edit/resume). Tests: regression tests for calibration pagination, weight_delta clamping, whole-number float filtering, malformed-sort 400, and export-includes-archived. Full integration matrix (sqlite/postgres/mariadb/cockroachdb) passes 297 each. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Harden testing, CI, and Docker (unit tests, dependabot, type/i18n checks) - Add a fast, non-Docker pytest unit suite under tests/ (26 tests for spoolman.math and extra-field validation) plus pytest config ([tool.pytest.ini_options]) and pytest-cov. - lefthook ci: run client `tsc --noEmit` and `npm run check-i18n` so type and translation regressions fail CI. Register the previously-unwired `lt` and `tr` locales in client/src/i18n.ts so check-i18n passes. - Add .github/dependabot.yml (weekly pip + npm). - Expand .dockerignore (.git, .github, tests, env files, etc.). - Drop the obsolete top-level `version:` key from the compose files. Note: the matching .github/workflows/ci.yml additions (unit-tests, CodeQL and Hadolint jobs) are kept as a separate patch (ci-workflow-additions.patch) because pushing workflow changes needs a token with the `workflow` scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: add unit-tests, CodeQL, and Hadolint jobs - unit-tests: run the fast non-Docker pytest suite (uv run pytest tests/) with coverage, no fail threshold. - codeql: static analysis for python and javascript-typescript. - hadolint: lint the Dockerfile. Split from the hardening commit because pushing workflow files needs a token with the `workflow` scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Cleanup and minor-correctness pass from review Backend: - Deduplicate utc_timezone_naive into spoolman/database/utils.py (was copied in spool.py and calibration.py). - Collapse the two _JsonArray*Element classes into one parametrized _JsonArrayElement(col, index). - Introduce EXTRA_FIELD_PREFIX constant (extra_field_registry) and use it instead of the hardcoded "extra."/[6:] in the spool/filament/vendor find endpoints and the query builder. - Escape LIKE/ILIKE wildcards (% and _) in extra-field text/multi-choice filters so a value like "50%" matches literally instead of acting as a wildcard. - calibration_models.from_db: parse JSON columns with `is not None` instead of truthiness so an empty stored value isn't dropped to None. - get_external_db_sync_interval: tolerate a non-integer EXTERNAL_DB_SYNC_INTERVAL (warn + default) instead of crashing startup. - Dockerfile HEALTHCHECK: add a urlopen timeout. Frontend: - Extract the duplicated 3DFP fetchProfile + field-mapping into shared utils/queryExternalDB.ts::fetchExternalProfile, using the axios instance (so it carries auth and works behind forward-auth proxies instead of a bare fetch). - Extract a shared FilterDropdownFooter and translate the custom-field filter dropdown labels via the i18n instance (antd renders filterDropdown as a callback, so translate statically, not via a prop/hook); add the table.filter.* en keys. - Add CUSTOM_FIELD_PREFIX constant in queryFields.ts; remove dead formatCustomFieldFilterValue / getCustomFieldSorters / isCustomFieldSorter. - CalibrationWizard: re-initialise when the session prop changes while mounted (deps), and guard the "skip step" handler against double-submit. Verified: ruff/eslint/tsc/prettier/check-i18n, 26 unit tests, and the full integration matrix (sqlite/postgres/mariadb/cockroachdb) all pass 297 each. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: satisfy hadolint on the Dockerfile The hadolint job (added in b3ed419) runs with the action's default failure-threshold of "info", which flagged five findings on the pre-existing build/runtime apt+pip layers. - Add --no-install-recommends to both apt-get install layers. This is the exact remediation hadolint recommends for DL3015 and trims the images by skipping recommended-but-unneeded packages. - Add .hadolint.yaml ignoring DL3008 (pin apt versions) and DL3013 (pin pip versions). The images build on the rolling python:3.x-slim-bookworm base, whose apt package index moves over time; pinning exact apt/pip versions would break the build as soon as those versions are superseded upstream. Reproducibility of the actual Python dependency set is already handled by uv.lock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Deduplicate the entity-type -> extra-field-table mapping Replace the two parallel if/elif ladders in _get_field_table_for_entity and _get_entity_id_column with a single _ENTITY_FIELD_TABLES dict mapping each EntityType to its (field table, owning-entity id column). Pure refactor: verified the dict returns the identical table/column objects the ladders did for spool/filament/vendor and still raises ValueError on unknown inputs. Verified: ruff, 26 unit tests, and sqlite+postgres integration matrix (297 each) all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: VeeBack <VeeBack@users.noreply.github.com> Co-authored-by: chruoss <chruoss@users.noreply.github.com> Co-authored-by: velzi <velzi@users.noreply.github.com> Co-authored-by: sw1nn <sw1nn@users.noreply.github.com> Co-authored-by: Disane87 <Disane87@users.noreply.github.com> Co-authored-by: meek2100 <meek2100@users.noreply.github.com> Co-authored-by: sherrmann <sherrmann@users.noreply.github.com> Co-authored-by: nkgotcode <nkgotcode@users.noreply.github.com> Co-authored-by: chof747 <chof747@users.noreply.github.com> Co-authored-by: t0ny-peng <t0ny-peng@users.noreply.github.com> Co-authored-by: daften <daften@users.noreply.github.com> Co-authored-by: SmoothBrainIT <SmoothBrainIT@users.noreply.github.com>
- Float exact-match filters now compare numerically (cast to Float) instead of by JSON string, so float fields stored as integer JSON (e.g. "2") or non-canonical decimals (e.g. "2.50") match an equivalent filter. - integer_range/float_range filter and sort no longer return HTTP 500 on CockroachDB: the PostgreSQL '->>' JSON extraction is now also registered for the cockroachdb dialect, which previously fell back to the unsupported JSON_EXTRACT function. - Text exact-match and single-choice equality use ensure_ascii=False so non-ASCII values match how the frontend's JSON.stringify stores them. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>
Add tests covering filter composition (multiple extra fields, extra + built-in, filter one field / sort another), pagination total-count, unknown-field handling, text case-insensitivity/substring/non-ASCII, numeric multi-value OR, boolean empty semantics and token rejection, multi-choice substring collisions, empty filters on numeric fields, null-bounded ranges, and additional invalid-value 400 paths. Verified against sqlite, postgres, mariadb and cockroachdb. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>
The custom-field filter dropdowns' Reset buttons were written as one-line onClick handlers that prettier wraps across multiple lines. Apply prettier so the CI style check passes. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>
…m-field filters
Match text, single-choice and datetime custom-field filters against the
database-decoded JSON scalar (new _JsonScalarText cross-dialect helper:
#>> '{}' on postgres/cockroachdb, JSON_UNQUOTE(JSON_EXTRACT) on mysql,
json_extract on sqlite) instead of reconstructing the exact JSON
serialization the client wrote. This decouples matching from
json.dumps/JSON.stringify encoding quirks (non-ASCII escaping, surrounding
quotes). Also escape LIKE wildcards so '%' and '_' in text and multi-choice
queries match literally rather than acting as wildcards.
Verified against sqlite, postgres, mariadb and cockroachdb.
Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>
Add parametrized tests (spool/filament/vendor) asserting that '%' and '_' are matched literally in text and multi-choice filters, that a literal '/' (the internal LIKE escape char) matches literally, and that single-choice equality handles a '%' value exactly. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>
|
Hey, sorry for taking so long. I took the liberty of fixing up some last things I found. Merging now :) |
|
No worries, thanks so much for reviewing and accepting! :) |
|
Also thanks @akira69 for the contributions and collaboration 🙏 |
* Add conditional logic to drop permissions for unprivileged Docker This change enables the container to run in unprivileged mode (without --privileged or additional capabilities) by making permission drops conditional: - Changed users group GID from 1000 to 1001 to avoid conflicts with the app user's primary group (UID 1000) - Only modify user/group IDs when PUID/PGID environment variables differ from the default value of 1000, avoiding unnecessary privilege requirements - Skip su-exec entirely when running as the default user, since changing user context requires additional capabilities that unprivileged containers don't have This allows the container to work in restricted environments like Kubernetes with security contexts or rootless Docker while maintaining backward compatibility for users who customize PUID/PGID. Fixes Donkie#791 * Allow archived spool export * Tidy up docker file and entrypoint * Fix spool price disappearing from the list on live updates (Donkie#814) The REST endpoints serialize with response_model_exclude_none=True (unset fields are omitted), but websocket events were serialized with a plain .json() that includes unset fields as explicit null. The spool list fills a missing price from the filament price only when it is `undefined`, so a live update carrying `price: null` (e.g. after a quick weight adjust) blanked the price column until a full page reload. Fixed on both sides so the two serialization paths can no longer drift: - Server: websocket payloads now use exclude_none=True (spoolman/ws.py), giving live updates and REST responses an identical shape. Note this is a small websocket wire-format change external consumers will see: unset fields are now omitted instead of sent as null, matching REST. - Client: collapseSpool and the price column now fall back for both `null` and `undefined`, keeping the list resilient regardless of serialization. * Commit generated requirements.txt for bare-metal/Moonraker installs (Donkie#830) requirements.txt was moved to .gitignore when the project switched to uv, so bare-metal installs lost it. Moonraker's update_manager runs `pip install -r requirements.txt` against the checkout, so upgrading to 0.23 broke with "Invalid path for option `requirements`". Track requirements.txt again and regenerate it from uv on every release (spoolman/bump.py exports it alongside the uv.lock bump), keeping uv as the single source of truth while staying backwards compatible with installs that expect a requirements.txt. * Document that extra field values are JSON-encoded strings (Donkie#849) The `extra` map on vendors, filaments and spools is intentionally typed as dict[str, str] where every value is a JSON-encoded string (the client round- trips them through JSON.parse/stringify). This surprised API consumers, who saw numeric fields come back as strings. Spell out the encoding on all three response models via a shared helper so the generated OpenAPI docs make clear that consumers must JSON-decode each value. No behaviour change. * Update Python dependencies (uv lock --upgrade) Refresh all backend dependencies to the latest versions allowed by the pyproject constraints and regenerate requirements.txt to match. Notable bumps: starlette 0.50 -> 1.3, fastapi 0.128 -> 0.139, cryptography 46 -> 49, uvicorn 0.40 -> 0.50, pydantic 2.12 -> 2.13, sqlalchemy 2.0.45 -> 2.0.51, alembic 1.17 -> 1.18. Locally verified: uv sync, ruff check, uv lock --check, app import, and a live startup smoke (SQLite) — migrations run, /health + /info respond, vendor CRUD works, and a websocket update event is delivered correctly on starlette 1.x. The Docker-based 4-DB integration suite still needs to run (CI). * Update client dependencies (npm update) Refresh transitive client dependencies to the latest versions within the existing package.json semver ranges (package.json unchanged). Verified: npm ci, tsc --noEmit, eslint, prettier --check, and npm run build all pass. (3 high-severity advisories remain that require major/breaking bumps via `npm audit fix --force`; left for a separate, deliberate change.) * Make the integration test suite runnable under Podman Running tests_integration against rootless Podman surfaced three issues; none affect the Docker/CI path: - run.py: add a SPOOLMAN_CONTAINER_ENGINE env var (defaults to "docker") so the build/compose commands can be driven by podman instead. - Dockerfile: drop `--chown=app:app` from the builder-stage COPYs. The "app" user only exists in the runner stage; Docker/BuildKit tolerated the dangling reference but Podman rejects it. Final ownership is unchanged — it is set when the files are copied into the runner stage. - docker-compose-postgres.yml: add a pg_isready healthcheck and gate spoolman on `condition: service_healthy`, matching the mariadb/cockroachdb compose files. Without it spoolman raced postgres' initdb and died on connection-refused before running migrations. Verified: full 4-DB suite (sqlite/postgres/mariadb/cockroachdb) green via `SPOOLMAN_CONTAINER_ENGINE=podman uv run poe itest` — 223 tests each. * Fix commit-before-notify races in write paths (flaky postgres deletes) Several DB write functions mutated and then sent their websocket notification (or just returned) without committing, relying on the request session's teardown commit. On async postgres that commit races the HTTP response, so a fast follow-up read could still see the pre-write state — e.g. `DELETE /vendor/{id}` returns 200 but an immediate `GET` still returns 200. Latent since 2023 (a6d527a); the async-timing dependency bump widened the window enough to intermittently fail `test (postgres)` in CI. Commit inline before notifying, matching the functions that already did (filament.delete, vendor/spool create/update) and the commit-first invariant from the earlier websocket-ordering fix: - vendor.delete, spool.delete - setting.update, setting.delete - vendor/filament/spool.clear_extra_field - spool.rename_location (use/measure already commit via their wrappers; reads are unaffected.) Verified via `SPOOLMAN_CONTAINER_ENGINE=podman poe itest`: full 4-DB suite green plus 7/7 repeat postgres runs with zero flakes (was ~66% failing before). * Bump deps to clear Dependabot alerts (path-to-regexp, pytest) - path-to-regexp: force >=8.4.0 (8.4.2) under @ant-design/pro-layout via a scoped npm override, fixing the ReDoS advisories (GHSA sequential optional groups + multiple wildcards). Express keeps its own 0.1.x. - pytest 8.3 -> 9.1.1 and pytest-asyncio 0.23/0.25 -> 1.4.0 in tests_integration/requirements.txt and the dev group; regenerated uv.lock. Verified: client build + typecheck pass, npm audit clean, and the full integration suite (223 tests) passes on postgres, sqlite, mariadb, and cockroachdb under podman. Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V * Add Playwright frontend integration tests; fix broken SPA static serving Add a browser-driven integration suite (tests_frontend/) that runs against the real production image + PostgreSQL and drives the UI with Playwright: a smoke test that navigates every page via the sidebar with no console errors, and a CRUD test that creates a vendor -> filament -> spool entirely through the UI. Wired up as `poe itest-frontend` and a new `test-frontend` CI job (reusing the image built for the backend tests, gating releases). Navigation goes through real UI buttons (language-independent link targets) and the app language is forced to English so label matchers are stable regardless of the runner's browser locale. This immediately caught a real regression: Starlette 1.3.1 (pulled by a recent dep bump) removed the `method` kwarg from FileResponse, so SinglePageApplication 500'd on every static asset and the whole UI was dead. Drop the removed kwarg — Starlette now derives HEAD handling from the scope. Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V * Add CI guard blocking manual edits to non-English translations Non-English locales are managed exclusively through Weblate; any manual edit to an existing translation file gets silently overwritten on the next Weblate sync. This workflow fails any PR (except Weblate's own, detected via PR author login) that modifies or deletes an existing non-English file under client/public/locales/. Adding a brand-new language file is still allowed so contributors can bootstrap a new language alongside the client/src/i18n.ts entry. Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V * Update sync interval for external database synchronization (Donkie#943) The external database sync scheduler was ignoring the configured EXTERNAL_DB_SYNC_INTERVAL environment variable and always using the DEFAULT_SYNC_INTERVAL constant (3600 seconds). * Auto-merge Weblate translation PRs on green CI Weblate opens a PR for every translation sync. These are machine-generated, always scoped to client/public/locales/, and gated by the full CI suite, so hand-merging each one is pure friction. This enables GitHub auto-merge on Weblate-authored PRs so they merge themselves once required checks pass. A scope-guard step reads the PR's file list from the API and refuses to enable auto-merge unless every changed file is a .json under client/public/locales/, so the trust is in the diff, not just the author: a compromised Weblate token can open a PR but can only be auto-merged if it is confined to translations. Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V * Remove hashes from requirements.txt * Bump uuid from 13.0.2 to 14.0.0 in /client (Donkie#927) Bumps [uuid](https://github.com/uuidjs/uuid) from 13.0.2 to 14.0.0. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md) - [Commits](uuidjs/uuid@v13.0.2...v14.0.0) --- updated-dependencies: - dependency-name: uuid dependency-version: 14.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * docs: add MCP server to integrations list (Donkie#890) Add spoolman-mcp to the list of integrations. It's a Model Context Protocol server that lets you manage your filament inventory through AI assistants like Claude. * feat(scanner): accept Data Matrix and other 2D codes (Donkie#887) Widen the code scanner's accepted formats beyond qr_code to the common 2D matrix codes (micro/rm QR, Data Matrix, Aztec, PDF417) so manually-generated labels using those symbologies can be scanned. Payloads that don't match the spoolman spool format are ignored, so this is a safe superset. Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V * feat(theme): add 'System' option that follows OS/browser theme (Donkie#947) Replace the light/dark switch with a 3-way System/Light/Dark control. The new 'System' preference follows prefers-color-scheme live via a matchMedia listener. New installs default to System; existing users keep their stored light/dark choice (legacy values remain valid preferences). Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V * Translations update (Donkie#825) * Translated using Weblate (Estonian) Currently translated at 17.3% (50 of 288 strings) Translated using Weblate (Persian) Currently translated at 96.5% (278 of 288 strings) Translated using Weblate (French) Currently translated at 98.9% (285 of 288 strings) Translated using Weblate (Greek) Currently translated at 72.9% (210 of 288 strings) Translated using Weblate (Dutch) Currently translated at 99.3% (286 of 288 strings) Translated using Weblate (Hungarian) Currently translated at 72.2% (208 of 288 strings) Translated using Weblate (Danish) Currently translated at 85.4% (246 of 288 strings) Translated using Weblate (Thai) Currently translated at 98.6% (284 of 288 strings) Translated using Weblate (Japanese) Currently translated at 99.3% (286 of 288 strings) Co-authored-by: Anonymous <noreply@weblate.org> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/el/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/et/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fa/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ja/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/th/ Translation: Spoolman/Web Client * Translated using Weblate (Ukrainian) Currently translated at 93.0% (268 of 288 strings) Translated using Weblate (Greek) Currently translated at 72.9% (210 of 288 strings) Translated using Weblate (Hungarian) Currently translated at 72.2% (208 of 288 strings) Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/el/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/ Translation: Spoolman/Web Client * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 98.6% (284 of 288 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 99.3% (286 of 288 strings) Translated using Weblate (Hindi (Latin script)) Currently translated at 3.8% (11 of 288 strings) Translated using Weblate (Swedish) Currently translated at 94.7% (273 of 288 strings) Translated using Weblate (Ukrainian) Currently translated at 93.0% (268 of 288 strings) Co-authored-by: Anonymous <noreply@weblate.org> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hi_Latn/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sv/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/ Translation: Spoolman/Web Client * Translated using Weblate (Danish) Currently translated at 87.1% (251 of 288 strings) Co-authored-by: srbjessen <srbjessen@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/ Translation: Spoolman/Web Client * Translated using Weblate (Czech) Currently translated at 100.0% (288 of 288 strings) Co-authored-by: Miloslav Kos <kos.m@post.cz> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/ Translation: Spoolman/Web Client * Translated using Weblate (Korean) Currently translated at 100.0% (288 of 288 strings) Translated using Weblate (Korean) Currently translated at 78.1% (225 of 288 strings) Added translation using Weblate (Korean) Co-authored-by: 김태남 <imedia0@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ko/ Translation: Spoolman/Web Client * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (288 of 288 strings) Co-authored-by: Kayz C <kayzed.x@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/ Translation: Spoolman/Web Client * Translated using Weblate (Slovenian) Currently translated at 100.0% (288 of 288 strings) Translated using Weblate (Slovenian) Currently translated at 38.1% (110 of 288 strings) Translated using Weblate (Slovenian) Currently translated at 14.9% (43 of 288 strings) Added translation using Weblate (Slovenian) Co-authored-by: Jernej Pangerc <jernejp21@tuta.com> Co-authored-by: jernejp21 <jernejp21@tuta.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sl/ Translation: Spoolman/Web Client * Translated using Weblate (Slovak) Currently translated at 10.4% (30 of 288 strings) Translated using Weblate (Slovak) Currently translated at 9.7% (28 of 288 strings) Added translation using Weblate (Slovak) Co-authored-by: Pavol Vrba <pvrba2000@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sk/ Translation: Spoolman/Web Client * Translated using Weblate (Hungarian) Currently translated at 94.0% (271 of 288 strings) Co-authored-by: Tamas Veres <tamas.veres@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/ Translation: Spoolman/Web Client * Translated using Weblate (Danish) Currently translated at 100.0% (288 of 288 strings) Translated using Weblate (Danish) Currently translated at 92.0% (265 of 288 strings) Co-authored-by: JonasBentin <jonasbentin19+github@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/ Translation: Spoolman/Web Client * Translated using Weblate (Latvian) Currently translated at 4.5% (13 of 288 strings) Added translation using Weblate (Latvian) Co-authored-by: Jurijs Kiricenko <jurijskiricenko@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/lv/ Translation: Spoolman/Web Client --------- Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com> Co-authored-by: srbjessen <srbjessen@gmail.com> Co-authored-by: Miloslav Kos <kos.m@post.cz> Co-authored-by: 김태남 <imedia0@gmail.com> Co-authored-by: Kayz C <kayzed.x@gmail.com> Co-authored-by: jernejp21 <jernejp21@tuta.com> Co-authored-by: Pavol Vrba <pvrba2000@gmail.com> Co-authored-by: Tamas Veres <tamas.veres@gmail.com> Co-authored-by: JonasBentin <jonasbentin19+github@gmail.com> Co-authored-by: Jurijs Kiricenko <jurijskiricenko@gmail.com> * feat(i18n): add Lithuanian and Turkish locales Rework check-i18n to measure real translation coverage against en (non-empty and differing from the reference) instead of file size, gating inclusion at 50%. Add lt and tr, both ~96% translated. Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V * Bump version to 0.24.0 * Add filtering and sorting for custom fields (Donkie#773) * Add filtering and sorting for custom fields * Fix tests and make code postgres compatible * Fix bugs and expand test coverage for custom field filter/sort * trigger CI * fix: address extra field filter review feedback * fix: clean up custom field table state handling * docs: update extra field table view description * fix: narrow boolean custom field filter syntax * Expand test coverage for all custom field types/entities and fix range filter/sort - Add comprehensive tests covering all 9 field types (text, integer, float, boolean, single-choice, multi-choice, datetime, integer_range, float_range) for filter and sort on spool, filament, and vendor entities - Add invalid-filter 400 tests for float, integer_range, and float_range - Fix integer_range/float_range filter to use LIKE pattern matching against Python's deterministic json.dumps output instead of fragile .contains() - Add integer_range/float_range sort support via a @compiles helper (_JsonArrayFirstElement) that emits CAST(col AS JSON)->>0 on PostgreSQL and JSON_EXTRACT(col, '$[0]') on SQLite/MariaDB - Add logger and __all__ to extra_fields.py (aligns with PR Donkie#893) - All tests verified passing on postgres, sqlite, and mariadb * Add filter UI for all custom field types and fix range filters - Add text input filter dropdown for text fields (substring search) - Add range input filter dropdowns (min/max) for integer, float, integer_range, and float_range fields - Add datetime picker filter dropdown for datetime fields - Fix boolean "No" filter to use empty value so it correctly matches unset/false fields - Fix integer_range/float_range filter to use numeric comparisons instead of LIKE-based exact matching (stored_min >= filter_min, stored_max <= filter_max) - Add range filter support for integer/float fields (min:max format) - Add _JsonArraySecondElement cross-database helper for extracting second JSON array element * Range filter for integer/float fields and fix range_field filter semantics Backend: - Add _JsonArraySecondElement cross-database helper (mirrors _JsonArrayFirstElement) - integer_range/float_range filter now uses numeric comparisons: stored_min >= filter_min and stored_max <= filter_max (was LIKE exact match) - integer/float filter now supports min:max range format in addition to exact match: stored_value >= min and/or stored_value <= max Frontend: - integer and float fields now use a range filter dropdown (min/max inputs) instead of a single exact-value input Tests: - Fix integer_range/float_range spool and vendor tests broken by new >= semantics (filter values updated to discriminate between test entries) - Add range filter tests (min only, max only, both) for integer and float fields * Replace 12 copy-pasted numeric field tests with 4 parametrized tests Introduce a _create_entity helper and @pytest.mark.parametrize("entity_type", ["spool", "filament", "vendor"]) to run each numeric field type test against all three entity types from a single test function. - Removes 12 individual tests (integer/float/integer_range/float_range × 3 entities) - Adds 4 parametrized tests covering the same ground plus the previously missing min-only / max-only range filter cases for filament and vendor - Uses try/finally for cleanup so entities are always deleted even on assertion failure * Add datetime range filter and restore integer/float range filter support - Backend: add datetime range filter using '|' separator (ISO dates contain ':') - Backend: restore integer/float range filter support (lost during attribution rewrite) - Frontend: replace single DateTimePicker with From/To range pickers for datetime fields * Use local timezone in filter picker; add range filter tests The DateTimeRangeFilterDropdown was initializing pickers with dayjs.utc(), causing them to display and accept UTC times directly. The entry form shows local time and converts to UTC, so the filter was inconsistent: entering the same displayed time would produce a filter value 2h offset from what was stored (in UTC+2), making exact boundaries fail unexpectedly. Fix: initialize picker values with dayjs() (local mode) so the filter picker behaves the same as the entry form — shows local times and converts to UTC. Tests: replace 3 separate entity-specific datetime tests with one parametrized test covering spool/filament/vendor, and add range filter cases (start|, |end, start|end). * Replace all entity-specific tests with parametrized tests Replace 16 separate spool/filament/vendor tests for text, boolean, single-choice, multi-choice, and empty-filter with 5 parametrized tests that run against all three entity types. Each test is now defined once and executed 3 times, eliminating copy-paste and ensuring consistent coverage across all entity types. Parametrize invalid-filter-value 400 tests across all entity types The non-numeric-value error uses "Invalid integer/float range filter value" while the missing-colon error uses "Invalid range filter value". Both contain "range filter value", so use that as the assertion substring. * Align custom filter dropdowns with Ant Design choice filter style All three custom filter dropdowns (text, number range, datetime range) now match the built-in choice filter footer: a border-top separator, Reset as a link button on the left (disabled when no filter is active), and OK as a primary button on the right. * fix(fields): correct custom-field filter edge cases across databases - Float exact-match filters now compare numerically (cast to Float) instead of by JSON string, so float fields stored as integer JSON (e.g. "2") or non-canonical decimals (e.g. "2.50") match an equivalent filter. - integer_range/float_range filter and sort no longer return HTTP 500 on CockroachDB: the PostgreSQL '->>' JSON extraction is now also registered for the cockroachdb dialect, which previously fell back to the unsupported JSON_EXTRACT function. - Text exact-match and single-choice equality use ensure_ascii=False so non-ASCII values match how the frontend's JSON.stringify stores them. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com> * test(fields): expand custom-field filter/sort coverage Add tests covering filter composition (multiple extra fields, extra + built-in, filter one field / sort another), pagination total-count, unknown-field handling, text case-insensitivity/substring/non-ASCII, numeric multi-value OR, boolean empty semantics and token rejection, multi-choice substring collisions, empty filters on numeric fields, null-bounded ranges, and additional invalid-value 400 paths. Verified against sqlite, postgres, mariadb and cockroachdb. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com> * style(client): format column.tsx with prettier The custom-field filter dropdowns' Reset buttons were written as one-line onClick handlers that prettier wraps across multiple lines. Apply prettier so the CI style check passes. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com> * fix(fields): decode JSON scalars in-DB for text/choice/datetime custom-field filters Match text, single-choice and datetime custom-field filters against the database-decoded JSON scalar (new _JsonScalarText cross-dialect helper: #>> '{}' on postgres/cockroachdb, JSON_UNQUOTE(JSON_EXTRACT) on mysql, json_extract on sqlite) instead of reconstructing the exact JSON serialization the client wrote. This decouples matching from json.dumps/JSON.stringify encoding quirks (non-ASCII escaping, surrounding quotes). Also escape LIKE wildcards so '%' and '_' in text and multi-choice queries match literally rather than acting as wildcards. Verified against sqlite, postgres, mariadb and cockroachdb. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com> * test(fields): cover LIKE-wildcard escaping in custom-field filters Add parametrized tests (spool/filament/vendor) asserting that '%' and '_' are matched literally in text and multi-choice filters, that a literal '/' (the internal LIKE escape char) matches literally, and that single-choice equality handles a '%' value exactly. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com> --------- Co-authored-by: Donkie <daniel.cf.hultgren@gmail.com> Co-authored-by: akira69 <akira69@gmail.com> * chore(ui): rename Hide Columns button label to Columns --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Joshua Piccari <joshua.piccari@gmail.com> Co-authored-by: Veli-Matti Leppänen <vellu@velhot.net> Co-authored-by: Donkie <daniel.cf.hultgren@gmail.com> Co-authored-by: chruoss <chrigi.ruoss@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marco Thiel <mfranke87@icloud.com> Co-authored-by: Weblate (bot) <noreply@weblate.org> Co-authored-by: srbjessen <srbjessen@gmail.com> Co-authored-by: Miloslav Kos <kos.m@post.cz> Co-authored-by: 김태남 <imedia0@gmail.com> Co-authored-by: Kayz C <kayzed.x@gmail.com> Co-authored-by: jernejp21 <jernejp21@tuta.com> Co-authored-by: Pavol Vrba <pvrba2000@gmail.com> Co-authored-by: Tamas Veres <tamas.veres@gmail.com> Co-authored-by: JonasBentin <jonasbentin19+github@gmail.com> Co-authored-by: Jurijs Kiricenko <jurijskiricenko@gmail.com> Co-authored-by: Donkie <2332094+Donkie@users.noreply.github.com> Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>

This PR adds filtering and sorting for custom fields in a generic way. This was tested in a limited capacity and doesn't encompass everything, e.g. filtering on text fields only works to filter empty items.
Nevertheless , I think this is already a useful addition.
My main use case is I have a boolean extra field that I check when I open a roll. This allows me to filter all open spools e.g.
The changes were done for all data types and several tests were written.
Disclaimer: I am not a Python developer and this was largely done using AI. Any feedback si more than welcome. High-level this looks okay to me, but I most likely miss items that are important to get this perfect. I also had issues with circular imports that I now fixed doing the imports not in the header, feedback on this is definitely more than welcome.