Skip to content

Add ability to generate query params from table state#14

Merged
Krever merged 4 commits intomainfrom
datatable-queryparams
Jan 20, 2026
Merged

Add ability to generate query params from table state#14
Krever merged 4 commits intomainfrom
datatable-queryparams

Conversation

@Krever
Copy link
Collaborator

@Krever Krever commented Jan 20, 2026

Solves #13

Summary by CodeRabbit

  • New Features

    • Persist and share table state (filters, sorting, pagination) via URL query parameters; programmatic export/import of state as query strings or key-value pairs
  • Documentation

    • Added guide describing URL query parameter formats and usage
  • Examples

    • Example showing export and restore of table state via URLs
  • Tests

    • Comprehensive tests for serialization, parsing, roundtrips, encoding, and edge cases

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Adds URL query parameter serialization/deserialization for datatables: new TableStateQueryParams utilities, convenience methods on TableState, comprehensive tests, docs and example updates, plus a small test dependency addition in build.sbt.

Changes

Cohort / File(s) Summary
Build Configuration
build.sbt
Added a jsSettings block to include a test dependency on io.github.cquiroz %%% scala-java-time % 2.6.0.
Core Query Parameter Utilities
forms4s-core/src/main/scala/forms4s/datatable/TableStateQueryParams.scala
New public types and API: TableStateParams and TableStateQueryParams with toQueryString/toQueryParams, fromQueryString/fromQueryParams, and applyToState. Handles filters (text/select/multi-select/boolean/number/date ranges), sorting, pagination, encoding/decoding, and repeated keys.
TableState Extensions
forms4s-core/src/main/scala/forms4s/datatable/TableState.scala
Added methods on TableState[T]: toQueryString, toQueryParams, loadFromQueryString, loadFromQueryParams delegating to TableStateQueryParams.
Test Coverage
forms4s-core/src/test/scala/forms4s/datatable/TableStateQueryParamsSpec.scala
New comprehensive spec covering serialization/deserialization, roundtrips, sorting, pagination, multiple filter types, repeated keys, URL encoding, and edge cases.
Documentation & Examples
forms4s-examples/src/main/scala/forms4s/example/docs/DatatableExample.scala, website/docs/datatables/index.mdx
Added example usage and docs describing query parameter formats and usage patterns.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant TableState
  participant TableStateQueryParams
  participant URL

  Client->>TableState: toQueryString()
  TableState->>TableStateQueryParams: toQueryString(state)
  TableStateQueryParams->>TableStateQueryParams: serialize filters/sort/page/pageSize
  TableStateQueryParams->>URL: return query string
  URL-->>Client: URL with query

  Client->>URL: provide query string
  URL->>TableStateQueryParams: fromQueryString(query)
  TableStateQueryParams->>TableStateQueryParams: parse -> TableStateParams
  TableStateQueryParams->>TableState: applyToState(state, params)
  TableState-->>Client: updated TableState
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I nibble keys and encode each view,
Filters hop, sorts twirl, pages peek through,
Query strings trail from burrow to sun,
Roundtrip returns — my hopping is done. 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR: adding functionality to generate and work with query parameters from table state.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@build.sbt`:
- Around line 30-32: Move the "io.github.cquiroz" %%% "scala-java-time" %
"2.6.0" dependency out of the test scope and into the main JS classpath so JS
compilation can resolve java.time types used by shared code like
TableStateQueryParams (which imports java.time.LocalDate); update the existing
.jsSettings(...) entry that currently adds the dependency with % "test" to
instead add it to the main JS project scope (remove the test classifier) so the
dependency is available to CrossType.Pure shared sources.

In `@forms4s-core/src/main/scala/forms4s/datatable/TableStateQueryParams.scala`:
- Around line 161-172: Serialization currently emits identical keys for
different FilterState variants (FilterState.TextValue, SelectValue,
MultiSelectValue, BooleanValue) using FilterPrefix+columnId, causing ambiguous
parsing; change the serialization in TableStateQueryParams (the match arms for
FilterState.TextValue, SelectValue, MultiSelectValue, BooleanValue) to encode
the filter kind in the key (e.g. append ".text", ".select", ".multi", ".bool" to
s"$FilterPrefix$columnId"), update the corresponding parsing logic in the same
file to read that suffix and recreate the exact FilterState variant (or
alternatively accept a TableDef-based type hint in the parser if you prefer
inferring types), and adjust any unit tests/docs that assert round-trip behavior
to expect the new keyed format so select/text/boolean filters round-trip
faithfully.
🧹 Nitpick comments (1)
forms4s-core/src/test/scala/forms4s/datatable/TableStateQueryParamsSpec.scala (1)

217-280: Consider a select-filter round‑trip test.

Round‑trip coverage is strong; adding a select filter round‑trip would guard against the TextValue/SelectValue ambiguity in parsing.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@forms4s-examples/src/main/scala/forms4s/example/docs/DatatableExample.scala`:
- Around line 52-59: The example comments are misleading because the shown
output assumes filters/sorting but the `tableState` used is empty; update the
snippet so `tableState` is mutated/constructed with the corresponding sort, page
and filters before calling `toQueryString`/`toQueryParams` (e.g. set sort to
"name:asc", page to 1, add filter f.department=Engineering and
f.salary.min=50000) so the printed example output matches the actual return
values of `tableState.toQueryString` and `tableState.toQueryParams`.
🧹 Nitpick comments (2)
forms4s-core/src/test/scala/forms4s/datatable/TableStateQueryParamsSpec.scala (1)

17-28: Missing test coverage for DateRangeValue filters.

The TableDef setup doesn't include a date column, and there are no tests for DateRangeFilter serialization/deserialization. The implementation in TableStateQueryParams.scala (lines 235-240, 265-268) supports date range filters with ISO-8601 format.

Consider adding a date column to the test model and corresponding tests:

🧪 Suggested additions for DateRange coverage
// Add to Person case class:
case class Person(name: String, age: Int, department: String, active: Boolean, tags: Set[String], hireDate: LocalDate)

// Add import:
import java.time.LocalDate

// Add to tableDef columns:
Column[Person, LocalDate]("hireDate", "Hire Date", _.hireDate, _.toString)
  .withFilter(ColumnFilter.dateRange(d => Some(d))),

// Add test cases:
"serializes date range filter - from only" in {
  val state = initialState.update(TableUpdate.SetFilter("hireDate", 
    FilterState.DateRangeValue(Some(LocalDate.of(2020, 1, 1)), None)))
  assert(state.toQueryString == "f.hireDate.min=2020-01-01")
}

"date range filter roundtrip" in {
  val state = initialState.update(TableUpdate.SetFilter("hireDate",
    FilterState.DateRangeValue(Some(LocalDate.of(2020, 1, 1)), Some(LocalDate.of(2020, 12, 31)))))
  val qs = state.toQueryString
  val newState = initialState.loadFromQueryString(qs)
  assert(newState.filters("hireDate") == state.filters("hireDate"))
}
forms4s-core/src/main/scala/forms4s/datatable/TableStateQueryParams.scala (1)

149-172: Consider functional style for applyToState.

The method works correctly, but the imperative style with var could be refactored to a more functional approach using foldLeft. This is optional and purely stylistic.

♻️ Optional: Functional refactor
 def applyToState[T](state: TableState[T], params: TableStateParams): TableState[T] = {
-  var updated = state
-
-  params.filters.foreach { case (columnId, parsedValue) =>
-    val filterState = toFilterState(parsedValue, state.definition, columnId)
-    filterState.foreach { fs =>
-      updated = updated.update(TableUpdate.SetFilter(columnId, fs))
-    }
-  }
-
-  params.sort.foreach { s =>
-    updated = updated.update(TableUpdate.SetSort(s.columnId, s.direction))
-  }
-
-  params.pageSize.foreach { size =>
-    updated = updated.update(TableUpdate.SetPageSize(size))
-  }
-
-  params.page.foreach { p =>
-    updated = updated.update(TableUpdate.SetPage(p))
-  }
-
-  updated
+  val withFilters = params.filters.foldLeft(state) { case (s, (columnId, parsedValue)) =>
+    toFilterState(parsedValue, state.definition, columnId)
+      .map(fs => s.update(TableUpdate.SetFilter(columnId, fs)))
+      .getOrElse(s)
+  }
+  val withSort = params.sort.foldLeft(withFilters) { (s, sort) =>
+    s.update(TableUpdate.SetSort(sort.columnId, sort.direction))
+  }
+  val withSize = params.pageSize.foldLeft(withSort) { (s, size) =>
+    s.update(TableUpdate.SetPageSize(size))
+  }
+  params.page.foldLeft(withSize) { (s, p) =>
+    s.update(TableUpdate.SetPage(p))
+  }
 }

@Krever Krever merged commit 7febb41 into main Jan 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant