Skip to content

Commit db32051

Browse files
fix(data-table) Harden sort whitelist and row_id selection handling (#50)
1 parent 12866d0 commit db32051

9 files changed

Lines changed: 540 additions & 190 deletions

File tree

e2e/lib/e2e_web/data_table_pattern_state.ex

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,31 @@ defmodule E2eWeb.DataTablePatternState do
66
def handle_sort_ns(socket, %{"sort_by" => sort_by_param},
77
rows: rows_k,
88
sort_by: by_k,
9-
sort_order: order_k
9+
sort_order: order_k,
10+
sort_columns: sort_columns
1011
) do
11-
sort_by = String.to_existing_atom(sort_by_param)
12-
current_by = Map.fetch!(socket.assigns, by_k)
13-
current_order = Map.fetch!(socket.assigns, order_k)
14-
15-
{next_by, next_order} =
16-
if current_by == sort_by do
17-
{sort_by, toggle(current_order)}
18-
else
19-
{sort_by, :asc}
20-
end
21-
22-
raw_rows = Map.fetch!(socket.assigns, rows_k)
23-
24-
socket
25-
|> assign(by_k, next_by)
26-
|> assign(order_k, next_order)
27-
|> assign(rows_k, sort_rows(raw_rows, next_by, next_order))
12+
case Corex.DataTable.Sort.parse_sort_by(sort_by_param, sort_columns) do
13+
{:ok, sort_by} ->
14+
current_by = Map.fetch!(socket.assigns, by_k)
15+
current_order = Map.fetch!(socket.assigns, order_k)
16+
17+
{next_by, next_order} =
18+
if current_by == sort_by do
19+
{sort_by, toggle(current_order)}
20+
else
21+
{sort_by, :asc}
22+
end
23+
24+
raw_rows = Map.fetch!(socket.assigns, rows_k)
25+
26+
socket
27+
|> assign(by_k, next_by)
28+
|> assign(order_k, next_order)
29+
|> assign(rows_k, sort_rows(raw_rows, next_by, next_order))
30+
31+
:error ->
32+
socket
33+
end
2834
end
2935

3036
def handle_select_ns(

e2e/lib/e2e_web/demos/data_table_demo.ex

Lines changed: 193 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -110,23 +110,95 @@ defmodule E2eWeb.Demos.DataTableDemo do
110110
"""
111111
end
112112

113-
def styling_rows, do: @anatomy_rows
113+
@style_users [
114+
%{id: 1, name: "Alice", email: "alice@example.com", role: "Admin", status: "Active"},
115+
%{id: 2, name: "Bob", email: "bob@example.com", role: "User", status: "Inactive"},
116+
%{id: 3, name: "Charlie", email: "charlie@example.com", role: "User", status: "Active"}
117+
]
118+
119+
def styling_rows, do: @style_users
114120

115121
def playground_rows, do: @anatomy_rows
116122

117-
def styling_color_code do
118-
styling_full_table_code(
123+
def styling_color_code_tabs do
124+
styling_code_tabs(
119125
"data-table-styling-color-accent",
120126
"data-table data-table--accent max-w-none"
121127
)
122128
end
123129

124-
def styling_size_code do
125-
styling_full_table_code("data-table-styling-size-lg", "data-table data-table--lg max-w-none")
130+
def styling_size_code_tabs do
131+
styling_code_tabs("data-table-styling-size-lg", "data-table data-table--lg max-w-none")
126132
end
127133

128-
def styling_max_width_code do
129-
styling_full_table_code("data-table-styling-max-w-md", "data-table max-w-md")
134+
def styling_max_width_code_tabs do
135+
styling_code_tabs("data-table-styling-max-w-md", "data-table max-w-md")
136+
end
137+
138+
def styling_code_tabs(table_id, class) do
139+
[
140+
%{value: "heex", label: "Heex", language: :heex, code: styling_heex(table_id, class)},
141+
%{value: "elixir", label: "Elixir", language: :elixir, code: styling_elixir()}
142+
]
143+
end
144+
145+
def styling_heex(table_id, class) do
146+
styling_full_table_code(table_id, class)
147+
end
148+
149+
def styling_elixir do
150+
~S"""
151+
@list_users [
152+
%{id: 1, name: "Alice", email: "alice@example.com", role: "Admin", status: "Active"},
153+
%{id: 2, name: "Bob", email: "bob@example.com", role: "User", status: "Inactive"},
154+
%{id: 3, name: "Charlie", email: "charlie@example.com", role: "User", status: "Active"}
155+
]
156+
157+
@style_sort_columns [:id, :name, :role, :status]
158+
159+
def mount(_params, _session, socket) do
160+
{:ok,
161+
socket
162+
|> assign(:style_rows, @list_users)
163+
|> Corex.DataTable.Sort.assign_for_sort(:style_rows,
164+
default_sort_by: :id,
165+
default_sort_order: :asc,
166+
sort_columns: @style_sort_columns
167+
)
168+
|> assign(:style_selected, [])}
169+
end
170+
171+
def handle_event("style_sort", params, socket) do
172+
{:noreply, Corex.DataTable.Sort.handle_sort(socket, params, :style_rows)}
173+
end
174+
175+
def handle_event("style_select", %{"id" => checkbox_id} = params, socket) do
176+
case String.split(checkbox_id, "-select-", parts: 2) do
177+
[table_id, _row_key] ->
178+
{:noreply,
179+
E2eWeb.DataTablePatternState.handle_select_ns(socket, params,
180+
rows: :style_rows,
181+
selected: :style_selected,
182+
table_id: table_id
183+
)}
184+
185+
_ ->
186+
{:noreply, socket}
187+
end
188+
end
189+
190+
def handle_event("style_select_all", %{"id" => checkbox_id} = params, socket) do
191+
table_id = String.replace_suffix(checkbox_id, "-select-all", "")
192+
193+
{:noreply,
194+
E2eWeb.DataTablePatternState.handle_select_all_ns(socket, params,
195+
rows: :style_rows,
196+
selected: :style_selected,
197+
table_id: table_id,
198+
row_id: &"#{table_id}-#{&1.id}"
199+
)}
200+
end
201+
"""
130202
end
131203

132204
defp styling_full_table_code(id, class) do
@@ -135,11 +207,12 @@ defmodule E2eWeb.Demos.DataTableDemo do
135207

136208
~S"""
137209
<.data_table
210+
id="ID_PLACEHOLDER"
138211
class="CLASS_PLACEHOLDER"
139212
rows={@style_rows}
140213
row_id={&"ID_PLACEHOLDER-#{&1.id}"}
141-
sort_by={@style_sort_by}
142-
sort_order={@style_sort_order}
214+
sort_by={@sort_by}
215+
sort_order={@sort_order}
143216
on_sort="style_sort"
144217
selectable
145218
selected={@style_selected}
@@ -266,9 +339,17 @@ defmodule E2eWeb.Demos.DataTableDemo do
266339
"""
267340
end
268341

342+
def patterns_sort_code_tabs do
343+
[
344+
%{value: "heex", label: "Heex", language: :heex, code: patterns_sort_heex()},
345+
%{value: "elixir", label: "Elixir", language: :elixir, code: patterns_sort_elixir()}
346+
]
347+
end
348+
269349
def patterns_sort_heex do
270350
~S"""
271351
<.data_table
352+
id="pattern-sort-table"
272353
class="data-table max-w-none"
273354
rows={@pattern_sort_rows}
274355
sort_by={@sort_by}
@@ -286,17 +367,34 @@ defmodule E2eWeb.Demos.DataTableDemo do
286367
</:sort_icon>
287368
<:col :let={row} label="ID" name={:id}>{row.id}</:col>
288369
<:col :let={row} label="Name" name={:name}>{row.name}</:col>
370+
<:col :let={row} label="Email" name={:email}>{row.email}</:col>
371+
<:col :let={row} label="Role" name={:role}>{row.role}</:col>
372+
<:col :let={row} label="Status" name={:status}>{row.status}</:col>
289373
</.data_table>
290374
"""
291375
end
292376

293377
def patterns_sort_elixir do
294378
~S"""
379+
@pattern_users [
380+
%{id: 1, name: "Alice", email: "alice@example.com", role: "Admin", status: "Active"},
381+
%{id: 2, name: "Bob", email: "bob@example.com", role: "User", status: "Inactive"},
382+
%{id: 3, name: "Charlie", email: "charlie@example.com", role: "User", status: "Active"},
383+
%{id: 4, name: "Diana", email: "diana@example.com", role: "Manager", status: "Active"},
384+
%{id: 5, name: "Eve", email: "eve@example.com", role: "Admin", status: "Inactive"}
385+
]
386+
387+
@pattern_sort_columns [:id, :name, :email, :role, :status]
388+
295389
def mount(_params, _session, socket) do
296390
{:ok,
297391
socket
298-
|> assign(:pattern_sort_rows, [%{id: 1, name: "Alice"}, %{id: 2, name: "Bob"}])
299-
|> Corex.DataTable.Sort.assign_for_sort(:pattern_sort_rows, default_sort_by: :id, default_sort_order: :asc)}
392+
|> assign(:pattern_sort_rows, @pattern_users)
393+
|> Corex.DataTable.Sort.assign_for_sort(:pattern_sort_rows,
394+
default_sort_by: :id,
395+
default_sort_order: :asc,
396+
sort_columns: @pattern_sort_columns
397+
)}
300398
end
301399
302400
def handle_event("pattern_sort", params, socket) do
@@ -305,9 +403,17 @@ defmodule E2eWeb.Demos.DataTableDemo do
305403
"""
306404
end
307405

406+
def patterns_select_code_tabs do
407+
[
408+
%{value: "heex", label: "Heex", language: :heex, code: patterns_select_heex()},
409+
%{value: "elixir", label: "Elixir", language: :elixir, code: patterns_select_elixir()}
410+
]
411+
end
412+
308413
def patterns_select_heex do
309414
~S"""
310415
<.data_table
416+
id="pattern-select-table"
311417
class="data-table max-w-none"
312418
rows={@pattern_select_rows}
313419
row_id={&"pselect-#{&1.id}"}
@@ -322,16 +428,27 @@ defmodule E2eWeb.Demos.DataTableDemo do
322428
</:checkbox_indicator>
323429
<:col :let={row} label="ID" name={:id}>{row.id}</:col>
324430
<:col :let={row} label="Name" name={:name}>{row.name}</:col>
431+
<:col :let={row} label="Email" name={:email}>{row.email}</:col>
432+
<:col :let={row} label="Role" name={:role}>{row.role}</:col>
433+
<:col :let={row} label="Status" name={:status}>{row.status}</:col>
325434
</.data_table>
326435
"""
327436
end
328437

329438
def patterns_select_elixir do
330439
~S"""
440+
@pattern_users [
441+
%{id: 1, name: "Alice", email: "alice@example.com", role: "Admin", status: "Active"},
442+
%{id: 2, name: "Bob", email: "bob@example.com", role: "User", status: "Inactive"},
443+
%{id: 3, name: "Charlie", email: "charlie@example.com", role: "User", status: "Active"},
444+
%{id: 4, name: "Diana", email: "diana@example.com", role: "Manager", status: "Active"},
445+
%{id: 5, name: "Eve", email: "eve@example.com", role: "Admin", status: "Inactive"}
446+
]
447+
331448
def mount(_params, _session, socket) do
332449
{:ok,
333450
socket
334-
|> assign(:pattern_select_rows, fetch_rows())
451+
|> assign(:pattern_select_rows, @pattern_users)
335452
|> Corex.DataTable.Selection.assign_for_selection(:pattern_select_rows,
336453
table_id: "pattern-select-table",
337454
row_id: &"pselect-#{&1.id}"
@@ -348,9 +465,17 @@ defmodule E2eWeb.Demos.DataTableDemo do
348465
"""
349466
end
350467

468+
def patterns_full_code_tabs do
469+
[
470+
%{value: "heex", label: "Heex", language: :heex, code: patterns_full_heex()},
471+
%{value: "elixir", label: "Elixir", language: :elixir, code: patterns_full_elixir()}
472+
]
473+
end
474+
351475
def patterns_full_heex do
352476
~S"""
353477
<.data_table
478+
id="pattern-full-table"
354479
class="data-table max-w-none"
355480
rows={@pattern_full_rows}
356481
row_id={&"pfull-#{&1.id}"}
@@ -377,6 +502,9 @@ defmodule E2eWeb.Demos.DataTableDemo do
377502
</:sort_icon>
378503
<:col :let={row} label="ID" name={:id}>{row.id}</:col>
379504
<:col :let={row} label="Name" name={:name}>{row.name}</:col>
505+
<:col :let={row} label="Email" name={:email}>{row.email}</:col>
506+
<:col :let={row} label="Role" name={:role}>{row.role}</:col>
507+
<:col :let={row} label="Status" name={:status}>{row.status}</:col>
380508
<:action :let={row}>
381509
<.action class="button button--sm" aria-label={"Edit #{row.name}"}>
382510
<.heroicon name="hero-pencil-square" />
@@ -391,6 +519,31 @@ defmodule E2eWeb.Demos.DataTableDemo do
391519

392520
def patterns_full_elixir do
393521
~S"""
522+
@pattern_users [
523+
%{id: 1, name: "Alice", email: "alice@example.com", role: "Admin", status: "Active"},
524+
%{id: 2, name: "Bob", email: "bob@example.com", role: "User", status: "Inactive"},
525+
%{id: 3, name: "Charlie", email: "charlie@example.com", role: "User", status: "Active"},
526+
%{id: 4, name: "Diana", email: "diana@example.com", role: "Manager", status: "Active"},
527+
%{id: 5, name: "Eve", email: "eve@example.com", role: "Admin", status: "Inactive"}
528+
]
529+
530+
@pattern_sort_columns [:id, :name, :email, :role, :status]
531+
532+
def mount(_params, _session, socket) do
533+
{:ok,
534+
socket
535+
|> assign(:pattern_full_rows, @pattern_users)
536+
|> Corex.DataTable.Sort.assign_for_sort(:pattern_full_rows,
537+
default_sort_by: :id,
538+
default_sort_order: :asc,
539+
sort_columns: @pattern_sort_columns
540+
)
541+
|> Corex.DataTable.Selection.assign_for_selection(:pattern_full_rows,
542+
table_id: "pattern-full-table",
543+
row_id: &"pfull-#{&1.id}"
544+
)}
545+
end
546+
394547
def handle_event("pattern_full_sort", params, socket) do
395548
{:noreply, Corex.DataTable.Sort.handle_sort(socket, params, :pattern_full_rows)}
396549
end
@@ -448,30 +601,39 @@ defmodule E2eWeb.Demos.DataTableDemo do
448601
|> assign(:pattern_db_rows, rows)
449602
|> assign(:pattern_db_page, 1)
450603
|> assign(:pattern_db_page_size, 5)
604+
|> assign(:pattern_db_sort_columns, [:name, :iata_code, :iata_country_code])
451605
|> assign(:pattern_db_sort_by, :name)
452606
|> assign(:pattern_db_sort_order, :asc)
453607
|> assign(:pattern_db_total, total)}
454608
end
455609
456-
def handle_event("pattern_db_sort", %{"sort_by" => sort_by}, socket) do
457-
sort_by = String.to_existing_atom(sort_by)
458-
order = if socket.assigns.pattern_db_sort_by == sort_by, do: toggle(socket.assigns.pattern_db_sort_order), else: :asc
459-
460-
{rows, total} =
461-
MyApp.Place.list_cities_table(
462-
page: 1,
463-
page_size: socket.assigns.pattern_db_page_size,
464-
order_by: sort_by,
465-
order_dir: order
466-
)
467-
468-
{:noreply,
469-
socket
470-
|> assign(:pattern_db_rows, rows)
471-
|> assign(:pattern_db_page, 1)
472-
|> assign(:pattern_db_sort_by, sort_by)
473-
|> assign(:pattern_db_sort_order, order)
474-
|> assign(:pattern_db_total, total)}
610+
def handle_event("pattern_db_sort", %{"sort_by" => sort_by_param}, socket) do
611+
case Corex.DataTable.Sort.parse_sort_by(sort_by_param, socket.assigns.pattern_db_sort_columns) do
612+
{:ok, sort_by} ->
613+
order =
614+
if socket.assigns.pattern_db_sort_by == sort_by,
615+
do: toggle(socket.assigns.pattern_db_sort_order),
616+
else: :asc
617+
618+
{rows, total} =
619+
MyApp.Place.list_cities_table(
620+
page: 1,
621+
page_size: socket.assigns.pattern_db_page_size,
622+
order_by: sort_by,
623+
order_dir: order
624+
)
625+
626+
{:noreply,
627+
socket
628+
|> assign(:pattern_db_rows, rows)
629+
|> assign(:pattern_db_page, 1)
630+
|> assign(:pattern_db_sort_by, sort_by)
631+
|> assign(:pattern_db_sort_order, order)
632+
|> assign(:pattern_db_total, total)}
633+
634+
:error ->
635+
{:noreply, socket}
636+
end
475637
end
476638
477639
def handle_event("pattern_db_page", %{"page" => page}, socket) do

0 commit comments

Comments
 (0)