28namespace morph::forms {
68namespace detail::engine {
73[[nodiscard]]
inline std::string collectionErrorText(
CollectionSession const& session) {
74 std::exception_ptr error = session.listError();
75 if (error ==
nullptr) {
76 error = session.actionError();
85[[nodiscard]]
inline std::vector<std::size_t> actionsOf(CollectionModel
const& model, CollectionActionScope scope) {
86 std::vector<std::size_t> indices;
87 for (std::size_t i = 0; i < model.actions().size(); ++i) {
88 if (elementAt(model.actions(), i).scope == scope) {
99[[nodiscard]]
inline ui::Node collectionHeader(CollectionSession& session, ui::Prop<std::string>
const& heading = {}) {
100 CollectionSession*
const view = &
session;
101 ui::Text title{.text = isSet(heading) ? heading : ui::Prop<std::string>{std::string{
session.model().title()}},
102 .role = ui::TextRole::Heading};
103 title.common.testId =
"collection_title";
104 std::vector<ui::Node> children{
105 ui::text(std::move(title)),
106 ui::busy(ui::Busy{.active = [view] {
return view->loading(); }, .label = std::string{}}),
108 for (std::size_t
const index : actionsOf(
session.model(), CollectionActionScope::Collection)) {
109 CollectionAction
const& action = elementAt(
session.model().actions(), index);
110 ui::Button
button{.label = action.label, .onClick = [view, index] { view->fire(index); }};
111 button.common.
enabled = [view] {
return !view->actionPending(); };
112 button.common.
testId =
"collectionAction_" + action.action;
113 children.push_back(ui::button(std::move(button)));
115 return ui::row(ui::Row{.children = std::move(children), .gap = 4});
123[[nodiscard]]
inline ui::Node rowButtons(CollectionSession& session, std::string
const& rowKey,
124 CollectionViewOptions
const& options) {
125 CollectionSession*
const view = &
session;
126 std::vector<ui::Node> buttons;
127 if (
session.model().rowAction().has_value()) {
128 ui::Button
open{.label = options.openLabel, .onClick = [view, rowKey] { view->open(rowKey); }};
129 open.common.enabled = [view] {
return view->editor() !=
nullptr; };
130 open.common.testId =
"rowOpen_" + rowKey;
131 buttons.push_back(ui::button(std::move(open)));
133 for (std::size_t
const index : actionsOf(
session.model(), CollectionActionScope::Row)) {
134 CollectionAction
const& action = elementAt(
session.model().actions(), index);
135 ui::Button
button{.label = action.label, .onClick = [view, index, rowKey] { view->fire(index, rowKey); }};
136 button.common.
enabled = [view] {
return !view->actionPending(); };
137 button.common.
testId =
"rowAction_" + action.action +
"_" + rowKey;
138 buttons.push_back(ui::button(std::move(button)));
140 return ui::row(ui::Row{.children = std::move(buttons), .gap = 4});
147[[nodiscard]]
inline ui::TableOptions rowsTableOptions(CollectionSession& session) {
148 CollectionSession*
const view = &
session;
149 ui::TableOptions
table{};
150 if (
session.model().rowAction().has_value()) {
151 table.onActivate = [view](ui::Key
const& key) {
152 if (
auto const*
const text = std::get_if<std::string>(&key)) {
157 if (
session.model().kind() == CollectionKind::MasterDetail) {
158 table.selectionMode = ui::SelectionMode::Single;
159 table.selection = [view] {
160 std::optional<std::string>
const&
open = view->openRow();
161 return open.has_value() ? std::vector<ui::Key>{ui::Key{*
open}} : std::vector<ui::Key>{};
163 table.onSelectionChange = [view](std::vector<ui::Key>
const& keys) {
164 auto const*
const text = keys.empty() ? nullptr : std::get_if<std::string>(&keys.front());
165 if (text ==
nullptr) {
181[[nodiscard]]
inline ui::Node collectionRows(CollectionSession& session,
182 std::shared_ptr<CollectionViewOptions const>
const& options) {
183 CollectionSession*
const view = &
session;
184 auto const rows = [view] {
return view->rows(); };
185 auto const keyOf = [](CollectionRow
const&
row) {
return ui::Key{
row.key}; };
186 if (options->chrome.row) {
187 ui::Node
list = ui::forEach<CollectionRow>(
189 [view, options](reactive::Signal<CollectionRow>
const& slot) {
return options->chrome.row(*view, slot); });
192 std::vector<ui::TableColumn> columns;
193 std::vector<std::string> fields;
194 for (CollectionColumn
const& column :
session.model().columns()) {
196 columns.push_back(ui::TableColumn{.label =
column.label});
197 fields.push_back(
column.field);
201 session.model().rowAction().has_value() || !actionsOf(
session.model(), CollectionActionScope::Row).empty();
203 columns.push_back(ui::TableColumn{.label = std::string{}});
205 ui::TableOptions
table = rowsTableOptions(session);
206 return ui::table<CollectionRow>(
207 std::move(columns), rows, keyOf,
208 [view, options, fields, buttons](reactive::Signal<CollectionRow>
const& slot) {
209 reactive::Signal<CollectionRow>
const*
const row = &
slot;
210 std::string
const key =
slot.peek().key;
211 std::vector<ui::Node>
cells;
212 for (std::size_t i = 0; i < fields.size(); ++i) {
213 ui::Text cell{.text = [
row, i] {
214 std::vector<std::string>
const& texts =
row->get().cells;
215 return i < texts.size() ? texts.at(i) : std::string{};
217 cell.common.testId =
"cell_" + fields.at(i) +
"_" + key;
218 cells.push_back(ui::text(std::move(cell)));
221 cells.push_back(rowButtons(*view, key, *options));
232[[nodiscard]]
inline ui::Node confirmDialog(CollectionSession& session, CollectionViewOptions
const& options) {
233 CollectionSession*
const view = &
session;
234 ui::Button accept{.label = options.acceptLabel, .onClick = [view] { view->confirm(); }};
235 accept.common.testId =
"confirm_accept";
236 ui::Button cancel{.label = options.cancelLabel, .onClick = [view] { view->cancelConfirm(); }};
237 cancel.common.testId =
"confirm_cancel";
238 ui::Text message{.text = options.confirmMessage};
239 message.common.testId =
"confirm_message";
241 .open = [view] {
return view->pendingConfirm().has_value(); },
242 .title = options.confirmTitle,
243 .child = ui::column(ui::Column{
244 .children = {ui::text(std::move(message)),
245 ui::row(ui::Row{.children = {ui::button(std::move(accept)), ui::button(std::move(cancel))},
248 .onDismiss = [view] { view->cancelConfirm(); },
251 return ui::dialog(std::move(dialog));
258[[nodiscard]]
inline ui::Node confirmChrome(CollectionSession& session,
259 std::function<ui::Node(CollectionSession&)>
const& chrome) {
260 CollectionSession
const*
const view = &
session;
261 return ui::switchOf(ui::Switch{
262 .selector = [view] {
return ui::Key{std::int64_t{view->pendingConfirm().has_value() ? 1 : 0}}; },
263 .cases = {ui::SwitchCase{.key = ui::Key{std::int64_t{1}}, .node = chrome(session)}},
283 auto const shared = std::make_shared<CollectionViewOptions const>(std::move(options));
284 CollectionSession*
const view = &session;
285 std::vector<ui::Node> list;
286 list.push_back(shared->chrome.header ? shared->chrome.header(session)
287 : detail::engine::collectionHeader(session, shared->title));
288 ui::Text error{.text = [view] {
return detail::engine::collectionErrorText(*view); }, .role = ui::TextRole::Error};
289 error.common.visible = [view] {
return view->listError() !=
nullptr || view->actionError() !=
nullptr; };
290 error.common.testId =
"collection_error";
291 list.push_back(ui::text(std::move(error)));
292 list.push_back(detail::engine::collectionRows(session, shared));
293 list.push_back(shared->chrome.confirm ? detail::engine::confirmChrome(session, shared->chrome.confirm)
294 : detail::engine::confirmDialog(session, *shared));
297 if (editor !=
nullptr && !masterDetail) {
299 if (shared->chrome.editor) {
300 list.push_back(shared->chrome.editor(session, std::move(form)));
303 .open = [view] {
return view->openRow().has_value(); },
305 .title =
session.model().rowAction().value().action,
306 .child = std::move(form),
307 .onDismiss = [view] { view->close(); },
310 list.push_back(ui::dialog(std::move(dialog)));
313 ui::Node
column = ui::column(ui::Column{.children = std::move(list), .gap = 4});
314 if (editor ==
nullptr || !masterDetail) {
317 ui::Column detail{.children = {
formView(*editor, shared->form)}};
318 detail.common.
visible = [view] {
return view->openRow().has_value(); };
319 detail.common.
testId =
"detailForm";
320 return ui::row(ui::Row{.children = {std::move(column), ui::column(std::move(detail))}, .gap = 5});
Node text(TextOptions const &options)
A text node.
AnyHandle list(std::vector< AnyHandle > const &items)
{"list": [...]}: a list built from expressions.
Node column(Nodes const &children, ContainerOptions const &options={})
A column node.
Node button(ButtonOptions const &options)
A button node.
Handle< T > row()
The innermost row, {"row": ""}.
AnyHandle session(std::string const &name)
A session attribute, {"session": name}.
std::vector< TableColumn > cells(std::vector< Node > const &cells)
Columns from cells alone, without ids or labels.
Node dialog(Element const &element, Nodes const &children, DialogOptions const &options={})
A dialog node, a scope of its own; open and close name it by its element.
Command open(Element const &dialog)
open: opens a dialog or drawer.
Node table(AnyHandle const &rows, TableOptions const &options)
A table over a list, with the columns in its options.
Node form(ModelHandle< Model > const &model, FormOptions< Action > const &options={})
A form node: a schema form of the forms engine over an action (spec 2); each field takes its label fr...
A source value. Reading it with get() inside a tracked run subscribes that run.
A node property: a constant, or a slot — a reactive read, with a stable id when it was given one.
Node testId(std::string const &name) const
A stable identifier for automated tests.
Node visible(AnyHandle const &condition) const
Whether the node shows.
Node enabled(AnyHandle const &condition) const
Whether the node is enabled.
Collections, headless: a v-* view document as a reactive list of rows, the table source the table eng...
@ MasterDetail
v-kind: master-detail: the editor sits beside the table.
ui::Node collectionView(CollectionSession &session, CollectionViewOptions options={})
Renders session: the header (title, busy indicator, collection actions), the error line,...
Declarative control: Query, Mutation and Subscription turn server interaction into reactive state,...
std::string errorMessage(std::exception_ptr const &error)
The one place an exception_ptr becomes display text.
Key keyOf(Value const &value)
A value as a view key: an int or a string as itself, anything else as its JSON text.
Signal<T>, Computed<T> and Effect: the nodes of a reactive graph.
The toolkit-agnostic view tree: props, node kinds and their builders.
std::shared_ptr< NodeData const > Node
A view-tree node: immutable, shared data.
SlotBinding< std::decay_t< F > > slot(SlotId slotId, F &&read)
A slot with an id.