Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions lib/backpex/field.ex
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,21 @@ defmodule Backpex.Field do
def readonly?(%{readonly: readonly}, assigns) when is_function(readonly, 1), do: readonly.(assigns)
def readonly?(_field_options, _assigns), do: false

@doc """
Drops readonly field changes from the given change map.

Takes a map of string-keyed form params, a keyword list of field definitions,
and assigns. Returns the change map with readonly field keys removed.
"""
def drop_readonly_changes(change, fields, assigns) do
read_only =
fields
|> Enum.filter(fn {_name, options} -> readonly?(options, assigns) end)
|> Enum.map(fn {name, _options} -> Atom.to_string(name) end)

Map.drop(change, read_only)
end

def translate_error_fun(%{translate_error: translate_error}, _assigns) when is_function(translate_error, 1),
do: translate_error

Expand Down
7 changes: 1 addition & 6 deletions lib/backpex/live_components/form_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -406,12 +406,7 @@ defmodule Backpex.FormComponent do
end

defp drop_readonly_changes(change, fields, assigns) do
read_only =
fields
|> Enum.filter(&Backpex.Field.readonly?(&1, assigns))
|> Enum.map(&Atom.to_string(&1.name))

Map.drop(change, read_only)
Backpex.Field.drop_readonly_changes(change, fields, assigns)
end

defp drop_unused_changes(change) do
Expand Down
66 changes: 66 additions & 0 deletions test/field_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule Backpex.FieldTest do
use ExUnit.Case, async: true

alias Backpex.Field

# Simulates a LiveResource fields/0 callback structure
defmodule UpstreamPrices do
def fields do
[
name: %{module: Backpex.Fields.Text, label: "Name"},
upstream_price: %{module: Backpex.Fields.Number, label: "Upstream Price", readonly: true},
override_price: %{module: Backpex.Fields.Number, label: "Our Price"}
]
end
end

describe "drop_readonly_changes/3" do
test "readonly fields are correctly filtered from LiveResource fields" do
fields = UpstreamPrices.fields()
change = %{"name" => "Backpack", "upstream_price" => "100", "override_price" => "200"}

filtered = Field.drop_readonly_changes(change, fields, %{})

assert filtered == %{"name" => "Backpack", "override_price" => "200"}
refute Map.has_key?(filtered, "upstream_price")
end

test "readonly function field is filtered when condition is true" do
fields = [
name: %{module: Backpex.Fields.Text, label: "Name"},
secret: %{module: Backpex.Fields.Text, label: "Secret", readonly: fn assigns -> assigns[:role] == :viewer end}
]

change = %{"name" => "Test", "secret" => "hidden"}

# As viewer - secret should be filtered
filtered = Field.drop_readonly_changes(change, fields, %{role: :viewer})
assert filtered == %{"name" => "Test"}

# As admin - secret should remain
filtered = Field.drop_readonly_changes(change, fields, %{role: :admin})
assert filtered == %{"name" => "Test", "secret" => "hidden"}
end
end

describe "readonly?/2" do
test "returns true for boolean true" do
assert Field.readonly?(%{readonly: true}, %{}) == true
end

test "returns false for boolean false" do
assert Field.readonly?(%{readonly: false}, %{}) == false
end

test "returns false when readonly key is missing" do
assert Field.readonly?(%{}, %{}) == false
end

test "evaluates function against assigns" do
readonly_fn = fn assigns -> assigns[:role] == :viewer end

assert Field.readonly?(%{readonly: readonly_fn}, %{role: :viewer}) == true
assert Field.readonly?(%{readonly: readonly_fn}, %{role: :admin}) == false
end
end
end
Loading