A write of a grid's rows: cells edited, new rows, rows deleted or brought back. Every change a person makes to the rows goes through one, whoever saves it.
The grid builds the write, and hands it to your function for its
operation (on_update, on_create, on_delete, on_restore on
Masu.Components.data_grid/1), which gives it back with a result for
each change. Without one, the grid saves it its own way
(Masu.Write.save/1: the repo of an Ecto source, or its copy of a
list), with changeset.
<.data_grid id="orders" source={Order} repo={Repo} scope={@current_scope}
on_update={&OrdersGrid.update/1}>
defmodule MyAppWeb.OrdersGrid do
alias MyApp.Orders
# A context function, row by row.
def update(write) do
Masu.Write.each(write, &Orders.update_order(write.scope, &1.row, &1.params))
end
endBy the time your function gets it, the grid has done its part: each
row is found again through the grid's own query (a person writes only
the rows they may see), the cells that do not edit, or that the row's
state makes read-only (row_editable, a column's editable), are
refused, and so are the cells someone changed meanwhile (see
"Two people, one cell" in the Editing guide). A change has only what
is left.
What a write has
op-:update,:create,:deleteor:restore;changes- theMasu.Write.Changes, one for each row;origin- what the person did::edit(typed in a cell),:paste,:fill,:clear,:undo,:redo(cells);:draft(a new row),:paste(rows of a paste);:rows(the rows of a range),:selection(the rows selected),:redo(a delete again);:undo(rows back);:testfromMasu.Test;scope- the grid'sscope: the current user, their organization, as your app has it (@current_scope);by- who makes it: on a grid in an Issho room (issho), and for an agent's tools, anIssho.Identity(a person, an agent and whom it acts for), the same as every component's callbacks get; else the grid'suser;grid- the id of the grid;ref- the write's reference, forasync/1andMasu.reply/2.
Results
Each change gets one, from the function that writes it:
each/2- a function of each change,{:ok, row}or{:error, reason}(:okfor a delete);all/2- one function of every change, for a transaction of the whole batch (Ecto.Multi):{:ok, rows}, in the order of the changes, or{:error, reason}, which refuses them all;ok/3anderror/3- a result for one change;save/1- the grid's own way, for the changes without a result;async/1- the changes without a result are saved later, by a job of yours: the grid shows them saving untilMasu.reply/2.
A reason is an Ecto.Changeset (its errors go on the cells of its
fields), a message (on every cell of the row), or [{key, message}]
of column keys. A change left without a result is a mistake, and the
grid raises.
The results compose. Your function may save the grid's way and do more, or save each row its own way and the rest the grid's:
def update(write) do
write
|> Masu.Write.save()
|> tap(&Audit.log(&1.scope, Masu.Write.saved(&1)))
end
def update(%Masu.Write{origin: :paste} = write) do
Masu.Write.all(write, &Orders.update_many(write.scope, &1))
end
def update(write), do: Masu.Write.save(write)Later: a job
A write that takes long (a paste of 5,000 cells into a system that
takes its time, an import of new rows through a queue) can be saved by
a job. async/1 says that the changes without a result will be
saved; ref reaches the grid from any process (an Oban job, on any
node of the cluster):
def update(write) do
%{ref: write.ref, changes: Enum.map(write.changes, &Map.take(&1, [:id, :params]))}
|> MyApp.Workers.SaveCells.new()
|> Oban.insert!()
Masu.Write.async(write)
end
# In the job:
Masu.reply(ref, :ok)
Masu.reply(ref, {:error, "The ledger is closed."})The cells show saving until the reply; then the grid reads its rows again, and an error refuses the changes, which show their old values. New rows and rows brought back need their ids at once: they cannot be saved later.
Summary
Functions
Gives the changes without a result one result, of fun of all of
them: {:ok, rows}, the rows as saved in the order of the changes (or
:ok for a delete), or {:error, reason}, which refuses every one
(the whole batch, as a transaction does). {:error, change, reason}
says which change it was: the others say that they were not saved.
The changes without a result are saved later, by a process of yours,
which says how it went with Masu.reply(write.ref, result): :ok,
{:ok, message} or {:error, message}. Not for :create and
:restore, whose rows need their ids at once.
Gives each change without a result the result of fun: {:ok, row},
with the row as saved, or {:error, reason}. For :delete, :ok or
{:ok, _} is the row deleted.
The result of one change: refused, for reason.
The changes refused, with their reasons.
The result of one change: saved, as row.
The changes without a result yet.
Records a change that your function saved, as row, in the grid's
history (Masu.History) now: in the transaction your function runs
it in, when the history is in the same repo. The grid does not record
it again after your function. Without a history, it does nothing.
The result of a change, or nil.
Saves the changes without a result the grid's own way
The rows saved: as saved, in the order of their changes.
Types
@type op() :: :update | :create | :delete | :restore
@type reason() :: Ecto.Changeset.t() | String.t() | [{String.t() | atom(), String.t()}]
@type t() :: %Masu.Write{ by: term(), changes: [Masu.Write.Change.t()], grid: String.t(), grid_config: map(), op: op(), origin: atom(), recorded: [non_neg_integer()], ref: String.t(), results: %{required(non_neg_integer()) => result()}, scope: term() }
Functions
@spec all(t(), ([Masu.Write.Change.t()] -> term())) :: t()
Gives the changes without a result one result, of fun of all of
them: {:ok, rows}, the rows as saved in the order of the changes (or
:ok for a delete), or {:error, reason}, which refuses every one
(the whole batch, as a transaction does). {:error, change, reason}
says which change it was: the others say that they were not saved.
The changes without a result are saved later, by a process of yours,
which says how it went with Masu.reply(write.ref, result): :ok,
{:ok, message} or {:error, message}. Not for :create and
:restore, whose rows need their ids at once.
@spec each(t(), (Masu.Write.Change.t() -> term())) :: t()
Gives each change without a result the result of fun: {:ok, row},
with the row as saved, or {:error, reason}. For :delete, :ok or
{:ok, _} is the row deleted.
@spec error(t(), Masu.Write.Change.t(), reason()) :: t()
The result of one change: refused, for reason.
@spec failed(t()) :: [{Masu.Write.Change.t(), reason()}]
The changes refused, with their reasons.
@spec ok(t(), Masu.Write.Change.t(), term()) :: t()
The result of one change: saved, as row.
@spec open(t()) :: [Masu.Write.Change.t()]
The changes without a result yet.
@spec record(t(), Masu.Write.Change.t(), term()) :: t()
Records a change that your function saved, as row, in the grid's
history (Masu.History) now: in the transaction your function runs
it in, when the history is in the same repo. The grid does not record
it again after your function. Without a history, it does nothing.
def update(write) do
Masu.Write.each(write, fn change ->
Repo.transaction(fn ->
order = Orders.update!(change.row, change.params)
Masu.Write.record(write, change, order)
order
end)
end)
endGives the write, which knows the change is recorded.
@spec result(t(), Masu.Write.Change.t()) :: result() | nil
The result of a change, or nil.
Saves the changes without a result the grid's own way:
:update-row |> changeset.(params), thenRepo.updatefor an Ecto source, each row read, compared again and saved in a transaction that locks it; for a list, the changeset applied;:create-changeset.(struct, params), inserted by the repo, or applied for a list; the grid'screate_withfields go on the struct first, and on the changeset after, over what it cast;:delete-Repo.delete, in a transaction that locks the row; a list's row taken out of its copy;:restore- the row inserted again; a list's put back.
An update and a new row need the grid's changeset (and struct).
The rows saved: as saved, in the order of their changes.