morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
explorer.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include <algorithm>
5#include <cctype>
6#include <cstddef>
7#include <cstdint>
8#include <expected>
9#include <functional>
10#include <iterator>
11#include <map>
12#include <memory>
13#include <optional>
14#include <ranges>
15#include <set>
16#include <string>
17#include <string_view>
18#include <utility>
19#include <variant>
20#include <vector>
21
22#include "../attributes.hpp"
24#include "backend.hpp"
25#include "bundle.hpp"
26#include "document.hpp"
27#include "host.hpp"
28#include "value.hpp"
29
30/// @file
31/// @brief The app explorer: what a morph application is made of — its screens, the models they use with every action,
32/// and for one action its facts, its fields, the places that use it and the widget the framework builds for
33/// it, mounted live.
34///
35/// Specified in `docs/spec/ui/explorer.md`.
36
37namespace morph::ui {
38
39/// @brief What names an action in a document.
40enum class ActionUseKind : std::uint8_t {
41 Query, ///< A query declaration.
42 Mutation, ///< A mutation declaration.
43 Form, ///< A `form` node.
44 Collection, ///< A `collection` node.
45 Wizard, ///< A `wizard` node.
46};
47
48/// @brief The name of an action use's kind.
49/// @param kind The kind.
50/// @return `"query"`, `"mutation"`, `"form"`, `"collection"` or `"wizard"`.
51[[nodiscard]] constexpr std::string_view actionUseKindName(ActionUseKind kind) noexcept {
52 constexpr std::array<std::string_view, 5> kNames{"query", "mutation", "form", "collection", "wizard"};
53 return kNames.at(static_cast<std::size_t>(kind));
54}
55
56/// @brief One place a document names an action.
57struct ActionUse {
58 /// @brief The action id.
59 std::string actionId;
60 /// @brief The model type the alias it goes through binds; empty when neither the document nor the catalog says.
61 std::string modelType;
62 /// @brief What names it.
64 /// @brief The JSON Pointer of the declaration or node.
65 std::string pointer;
66};
67
68namespace detail::explorer {
69
70/// @brief The scopes around a node, innermost last: the aliases in its reach.
71using ScopeChain = std::vector<ScopeDecl const*>;
72
73/// @brief The model type an alias binds, from the innermost scope that declares it.
74/// @param scopes The scopes around the use.
75/// @param alias The alias.
76/// @return The type; empty when no scope declares the alias.
77[[nodiscard]] inline std::string typeOf(ScopeChain const& scopes, std::string_view alias) {
78 for (ScopeDecl const* const scope : scopes | std::views::reverse) {
79 auto const found = std::ranges::find(scope->models, alias, &ModelDecl::name);
80 if (found != scope->models.end()) {
81 return found->type;
82 }
83 }
84 return {};
85}
86
87/// @brief Records one use, its model type the alias's, else the catalog's for the action.
88/// @param uses Where it is recorded.
89/// @param scopes The scopes around the use.
90/// @param catalog The facts a borrowed alias's type is read from.
91/// @param alias The alias the use goes through.
92/// @param action The action id.
93/// @param kind What names it.
94/// @param pointer Its JSON Pointer.
95inline void addUse(std::vector<ActionUse>& uses, ScopeChain const& scopes, Catalog const& catalog,
96 std::string_view alias, std::string action, ActionUseKind kind, std::string pointer) {
97 std::string type = typeOf(scopes, alias);
98 // A borrowed alias has no type of its own in the document: the catalog says which model has the action.
99 if (type.empty() && catalog.action) {
100 if (std::optional<ActionInfo> const info = catalog.action(action)) {
101 type = info->modelType;
102 }
103 }
104 uses.push_back(ActionUse{
105 .actionId = std::move(action), .modelType = std::move(type), .kind = kind, .pointer = std::move(pointer)});
106}
107
108/// @brief Records the queries and mutations of the innermost scope.
109/// @param uses Where they are recorded.
110/// @param scopes The scopes, the one read last.
111/// @param catalog The facts a borrowed alias's type is read from.
112inline void scopeUses(std::vector<ActionUse>& uses, ScopeChain const& scopes, Catalog const& catalog) {
113 ScopeDecl const& scope = *scopes.back();
114 for (QueryDecl const& query : scope.queries) {
115 addUse(uses, scopes, catalog, query.model, query.action, ActionUseKind::Query, query.path);
116 }
117 for (MutationDecl const& mutation : scope.mutations) {
118 addUse(uses, scopes, catalog, mutation.model, mutation.action, ActionUseKind::Mutation, mutation.path);
119 }
120}
121
122/// @brief Records the uses of a node and everything under it, with the scope it creates.
123/// @param uses Where they are recorded.
124/// @param scopes The scopes around the node; restored on return.
125/// @param catalog The facts a borrowed alias's type is read from.
126/// @param node The node.
127// NOLINTNEXTLINE(misc-no-recursion): a node's children are nodes.
128inline void nodeUses(std::vector<ActionUse>& uses, ScopeChain& scopes, Catalog const& catalog, DocNode const& node) {
129 if (node.scope) {
130 scopes.push_back(&*node.scope);
131 scopeUses(uses, scopes, catalog);
132 }
133 static constexpr std::array<std::pair<std::string_view, ActionUseKind>, 3> kNamed{
134 {{"form", ActionUseKind::Form}, {"collection", ActionUseKind::Collection}, {"wizard", ActionUseKind::Wizard}}};
135 for (auto const& [kind, use] : kNamed) {
136 if (node.kind == kind) {
137 addUse(uses, scopes, catalog, node.name("model"), std::string{node.name("action")}, use, node.path);
138 }
139 }
140 for (auto const& [key, nodes] : node.children) {
141 static_cast<void>(key);
142 for (DocNodePtr const& child : nodes) {
143 nodeUses(uses, scopes, catalog, *child);
144 }
145 }
146 for (auto const& [key, parts] : node.parts) {
147 static_cast<void>(key);
148 for (DocNodePtr const& part : parts) {
149 nodeUses(uses, scopes, catalog, *part);
150 }
151 }
152 if (node.scope) {
153 scopes.pop_back();
154 }
155}
156
157} // namespace detail::explorer
158
159/// @brief Every place a loaded document names an action: its queries and mutations at any scope, and its `form`,
160/// `collection` and `wizard` nodes, in document order.
161///
162/// A model type is the alias's, from the nearest enclosing scope that declares it. An alias that borrows another has
163/// no type in the document, and takes the catalog's `ActionInfo::modelType`. A `run` command names a mutation, not an
164/// action, so it is no use of its own.
165/// @param document The document: a screen, or the app document.
166/// @param catalog The facts a borrowed alias's model type is read from; empty leaves such a type empty.
167/// @return The uses.
168[[nodiscard]] inline std::vector<ActionUse> actionUses(Document const& document, Catalog const& catalog = {}) {
169 std::vector<ActionUse> uses;
170 detail::explorer::ScopeChain scopes{&document.scope};
171 detail::explorer::scopeUses(uses, scopes, catalog);
172 if (document.view) {
173 detail::explorer::nodeUses(uses, scopes, catalog, *document.view);
174 }
175 return uses;
176}
177
178/// @brief One field of an action, as the explorer lists it.
180 /// @brief The wire name.
181 std::string name;
182 /// @brief The type: the field kind's name, or for an enum "one of: …", a quantity "quantity in <unit>" with its
183 /// alternatives, a choice the action its options come from.
184 std::string type;
185 /// @brief Whether the action requires it.
186 bool required = false;
187 /// @brief The bounds and rules: `min …`, `max …`, `multiple of …`, `read-only`, `hidden`, and the name of each
188 /// `x-rules` rule that names the field.
189 std::vector<std::string> rules;
190};
191
192namespace detail::explorer {
193
194/// @brief Texts joined by a separator.
195/// @param parts The texts.
196/// @param separator What goes between two.
197/// @return The joined text.
198[[nodiscard]] inline std::string joined(std::vector<std::string> const& parts, std::string_view separator) {
199 std::string out;
200 for (std::string const& part : parts) {
201 out += out.empty() ? part : std::string{separator} + part;
202 }
203 return out;
204}
205
206/// @brief A field's type as the explorer lists it.
207/// @param field The field, as the forms engine reads it.
208/// @return The type.
209[[nodiscard]] inline std::string typeText(forms::FieldSpec const& field) {
210 using forms::FieldKind;
211 if (field.kind == FieldKind::Enum) {
212 std::vector<std::string> labels;
213 labels.reserve(field.enumOptions.size());
214 for (forms::ChoiceOption const& option : field.enumOptions) {
215 labels.push_back(option.label);
216 }
217 return "one of: " + joined(labels, ", ");
218 }
219 if (field.kind == FieldKind::Quantity) {
220 std::string const text = "quantity in " + (field.unit.empty() ? field.unitAscii : field.unit);
221 std::vector<std::string> others;
222 for (std::size_t index = 1; index < field.units.size(); ++index) {
223 others.push_back(field.units.at(index).display);
224 }
225 return others.empty() ? text : text + " (or " + joined(others, ", ") + ")";
226 }
227 if (field.kind == FieldKind::Decimal && field.decimalPlaces) {
228 return "decimal, " + std::to_string(*field.decimalPlaces) + " places";
229 }
230 if (field.kind == FieldKind::Choice && field.choice) {
231 return "choice from " + field.choice->optionsAction;
232 }
233 if (field.kind == FieldKind::Array) {
234 return "list of " + field.itemType;
235 }
236 if (field.kind == FieldKind::ObjectArray) {
237 return "list of objects";
238 }
239 return std::string{forms::fieldKindName(field.kind)};
240}
241
242} // namespace detail::explorer
243
244/// @brief The fields of an action, in `x-order`, as the forms engine reads its schema.
245/// @param schemaJson The action's schema, as `forms::schemaJson<A>()` emits it.
246/// @return The rows; none for a schema without properties. Or why the schema cannot be read.
247[[nodiscard]] inline std::expected<std::vector<ActionField>, std::string> actionFields(std::string_view schemaJson) {
248 auto model = forms::FormModel::fromSchema("", schemaJson);
249 if (!model) {
250 std::string const& where = model.error().path;
251 return std::unexpected(where.empty() ? model.error().message : where + ": " + model.error().message);
252 }
253 std::vector<ActionField> fields;
254 for (forms::FieldSpec const& spec : model->fields()) {
255 ActionField field{.name = spec.name, .type = detail::explorer::typeText(spec), .required = spec.required};
256 if (auto const& minimum = spec.exactMinimum ? spec.exactMinimum : spec.minimum) {
257 field.rules.push_back("min " + *minimum);
258 }
259 if (auto const& maximum = spec.exactMaximum ? spec.exactMaximum : spec.maximum) {
260 field.rules.push_back("max " + *maximum);
261 }
262 if (spec.multipleOf) {
263 field.rules.push_back("multiple of " + *spec.multipleOf);
264 }
265 if (spec.readOnly) {
266 field.rules.emplace_back("read-only");
267 }
268 if (spec.hidden) {
269 field.rules.emplace_back("hidden");
270 }
271 for (forms::RuleExpr const& rule : model->rules()) {
272 if (rule.names(spec.name)) {
273 field.rules.emplace_back(rule.kindName());
274 }
275 }
276 fields.push_back(std::move(field));
277 }
278 return fields;
279}
280
281/// @brief Which widget "Try it" builds for an action.
282enum class TryItKind : std::uint8_t {
283 Form, ///< A `form` over the action's schema.
284 Collection, ///< A `collection` over the catalog's view document of the action.
285 Wizard, ///< A `wizard` over the catalog's wizard document of the action.
286};
287
288/// @brief The screen id "Try it" mounts an action's document under.
289/// @param actionId The action id.
290/// @return `explorer:try:<action>`.
291[[nodiscard]] inline std::string tryItScreenId(std::string_view actionId) {
292 return "explorer:try:" + std::string{actionId};
293}
294
295/// @brief The one-node document "Try it" mounts for an action.
296///
297/// A private instance of the model under the alias `target`, the states `reply` and `failure`, and a column holding
298/// the node over the action (test id `tryit`) and, for a form, two texts showing the states (`tryit_result`,
299/// `tryit_error`). A form submits from its button only, and its `onSuccess` and `onError` set the states; a
300/// collection and a wizard show their own results and errors.
301/// @param actionId The action id.
302/// @param modelType The model type the action belongs to.
303/// @param kind The node.
304/// @return The document's JSON text, with the id `tryItScreenId(actionId)`.
305// NOLINTNEXTLINE(bugprone-easily-swappable-parameters): an action id and a model type read alike, and neither reads.
306[[nodiscard]] inline std::string tryItDocument(std::string_view actionId, std::string_view modelType, TryItKind kind) {
307 auto const quoted = [](std::string_view text) { return glz::write_json(std::string{text}).value_or("\"\""); };
308 std::string const action = quoted(actionId);
309 std::string node;
310 if (kind == TryItKind::Form) {
311 node = R"({"kind":"form","testId":"tryit","model":"target","action":)" + action +
312 R"(,"submit":"explicit",
313 "onSuccess":[{"set":"reply","value":{"ref":"event"}},{"set":"failure","value":null}],
314 "onError":[{"set":"failure","value":{"ref":"event"}},{"set":"reply","value":null}]},
315 {"kind":"text","testId":"tryit_result","role":"success","text":{"ref":"reply"}},
316 {"kind":"text","testId":"tryit_error","role":"error","text":{"if":[
317 {"fn":"engaged","args":[{"ref":"failure"}]},
318 {"fn":"format","args":["Failed: {0}",{"ref":"failure"}]},""]}})";
319 } else {
320 node = std::string{R"({"kind":")"} + (kind == TryItKind::Collection ? "collection" : "wizard") +
321 R"(","testId":"tryit","model":"target","action":)" + action + "}";
322 }
323 return R"({"vocab":"ui/1","id":)" + quoted(tryItScreenId(actionId)) + R"(,"title":)" +
324 quoted("Try " + std::string{actionId}) + R"(,
325 "models":{"target":{"type":)" +
326 quoted(modelType) + R"(,"instance":"private"}},
327 "state":{"reply":{"init":null},"failure":{"init":null}},
328 "view":{"kind":"column","gap":3,"children":[)" +
329 node + "]}}";
330}
331
332/// @brief A model's `schemas` document: `{actionType: schema}`, or why the server would not describe the model.
333using ModelSchemas = std::expected<std::string, std::string>;
334
335/// @brief The schemas documents of the models, by model type.
336using SchemasByModel = std::map<std::string, ModelSchemas, std::less<>>;
337
338/// @brief The actions a model's `schemas` document lists, with each one's schema.
339/// @param document The document's JSON text.
340/// @return The schemas by action id, or why the document cannot be read.
341[[nodiscard]] inline std::expected<std::map<std::string, std::string, std::less<>>, std::string> schemasOf(
342 std::string_view document) {
343 auto parsed = parseJson(document);
344 if (!parsed) {
345 return std::unexpected(std::move(parsed.error()));
346 }
347 auto const* const actions = std::get_if<Json::object_t>(&parsed->data);
348 if (actions == nullptr) {
349 return std::unexpected(std::string{"the schemas document is not an object"});
350 }
351 std::map<std::string, std::string, std::less<>> schemas;
352 for (auto const& [action, schema] : *actions) {
353 schemas.emplace(action, glz::write_json(schema).value_or(std::string{}));
354 }
355 return schemas;
356}
358/// @brief The catalog a "Try it" document loads with: @p catalog, whose `action` lookup also answers an action
359/// only a
360/// model's `schemas` document lists (load-time validation refuses an action the catalog does not know, and
361/// the catalog knows only those a served document names), and whose `schema` lookup also answers from the
362/// documents.
363/// @param catalog The client's catalog.
364/// @param schemas The models' `schemas` documents.
365/// @return The catalog.
366[[nodiscard]] inline Catalog tryItCatalog(Catalog catalog, SchemasByModel const& schemas) {
367 auto const known = std::make_shared<std::map<std::string, std::pair<std::string, std::string>, std::less<>>>();
368 for (auto const& [type, document] : schemas) {
369 if (!document) {
370 continue;
371 }
372 if (auto read = schemasOf(*document)) {
373 for (auto& [action, schema] : *read) {
374 known->emplace(action, std::pair{type, std::move(schema)});
375 }
376 }
377 }
378 catalog.action = [inner = std::move(catalog.action),
379 known](std::string_view actionId) -> std::optional<ActionInfo> {
380 if (inner) {
381 if (std::optional<ActionInfo> info = inner(actionId)) {
382 return info;
383 }
384 }
385 auto const found = known->find(actionId);
386 if (found == known->end()) {
387 return std::nullopt;
388 }
389 ActionInfo info;
390 info.modelType = found->second.first;
391 return info;
392 };
393 catalog.schema = [inner = std::move(catalog.schema),
394 known](std::string_view actionId) -> std::optional<std::string> {
395 if (inner) {
396 if (std::optional<std::string> schema = inner(actionId)) {
397 return schema;
398 }
399 }
400 auto const found = known->find(actionId);
401 if (found == known->end()) {
402 return std::nullopt;
404 return found->second.second;
405 };
406 return catalog;
408
409/// @brief What an `Explorer` shows, and what it reports.
410struct ExplorerOptions {
411 /// @brief The catalog for the current principal: its entries, `permitted`, and the facts.
413 /// @brief The client's catalog facts, which "Try it" documents load with; empty uses `catalog.facts()`.
415 /// @brief The `schemas` document of every model the catalog names, by model type.
417 /// @brief The loaded documents by screen id, the app document under `app`.
418 std::map<std::string, std::shared_ptr<Document const>, std::less<>> documents;
419 /// @brief What "Try it" mounts its document through. Borrowed; null offers no "Try it".
420 DocumentScreenHost* host = nullptr;
421 /// @brief Called with a "Try it" document before it is mounted, for the host's `documents` lookup to answer
422 /// its
423 /// screen id (`tryItScreenId`) with; null offers no "Try it".
424 std::function<void(std::string const& screenId, std::shared_ptr<Document const> document)> provide;
425 /// @brief Called with a screen id when the person asks to open it.
426 std::function<void(std::string const& screenId)> onOpen;
427};
428
429/// @brief The app explorer, as a host screen (spec 6 §15): a splitter with the list — a search, the catalog's
430/// screens,
431/// and every model the catalog names with every action of it — and the chosen action's sections: its facts,
432/// its fields, the places that use it, and "Try it", the widget the framework builds for it, mounted live.
433///
434/// Built from backend widgets, so every frontend shows it. Its title is "Explorer"; it is never dirty, and closes
435/// when asked. Neither copyable nor movable; owned by the owner thread of its backend.
436///
437/// Test ids: `explorer`, `explorer_search`, `explorer_screen_<id>`, `explorer_model_<type>`,
438/// `explorer_action_<id>`, `explorer_title`, `explorer_facts`, `explorer_fields`, `explorer_uses`,
439/// `explorer_open_<n>`, `explorer_tryit`, `explorer_tryit_status`, `explorer_schema`, and inside "Try it" the
440/// generated document's own (`tryit`, …).
441class Explorer final : public ScreenHandle {
442public:
443 /// @brief The explorer, appended to @p page.
444 /// @param runtime The reactive runtime its title is computed in. Borrowed.
445 /// @param backend The backend its widgets are made with. Borrowed.
446 /// @param page The container it is appended to; it outlives the explorer.
447 /// @param options What it shows and reports.
448 Explorer(reactive::Runtime& runtime MORPH_LIFETIMEBOUND, IViewBackend& backend MORPH_LIFETIMEBOUND,
449 ContainerWidget& page MORPH_LIFETIMEBOUND, ExplorerOptions options)
450 : _backend{&backend},
451 _options{std::move(options)},
452 _scope{runtime},
453 _title{_scope.computed([] { return std::string{"Explorer"}; })},
454 _dirty{_scope.computed([] { return false; })},
455 _identity{_scope.computed([] { return Value{}; })},
456 _root{backend.createSplitter(&page, Axis::Horizontal)},
457 _listScroll{backend.createScroll(_root.get(), Axis::Vertical)},
458 _list{backend.createStack(_listScroll.get(), Axis::Vertical)},
459 _detailScroll{backend.createScroll(_root.get(), Axis::Vertical)},
460 _detail{backend.createStack(_detailScroll.get(), Axis::Vertical)} {
461 if (!_options.facts.action && !_options.facts.model) {
462 _options.facts = _options.catalog.facts();
463 }
464 _permitted.insert(_options.catalog.permitted.begin(), _options.catalog.permitted.end());
465 for (auto const& [screenId, document] : _options.documents) {
466 for (ActionUse& use : actionUses(*document, _options.facts)) {
467 std::string action = use.actionId;
468 _uses[std::move(action)].emplace_back(screenId, std::move(use));
469 }
470 }
471 _root->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
472 _root->setTestId("explorer");
473 _root->setSizes({kListWidth, kDetailWidth});
474 _listScroll->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
475 _detailScroll->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
476 _list->setGap(2);
477 _list->setLayout({.width = Sizing::stretch(), .height = {}});
478 _detail->setGap(4);
479 _detail->setLayout({.width = Sizing::stretch(), .height = {}});
480 _detail->setTestId("explorer_detail");
481 listAll();
482 showNothingChosen();
483 }
484
485 Explorer(Explorer const&) = delete;
486 Explorer& operator=(Explorer const&) = delete;
487 Explorer(Explorer&&) = delete;
488 Explorer& operator=(Explorer&&) = delete;
489 /// @brief Unmounts the "Try it" screen first, then the widgets it sits in.
490 ~Explorer() override { clearDetail(); }
491
492 /// @brief The title.
493 /// @return "Explorer".
494 [[nodiscard]] reactive::Computed<std::string> const& title() const override { return _title; }
495
496 /// @brief Never dirty.
497 /// @return `false`.
498 [[nodiscard]] reactive::Computed<bool> const& dirty() const override { return _dirty; }
499
500 /// @brief No identity: one explorer per workspace.
501 /// @return `null`.
502 [[nodiscard]] reactive::Computed<Value> const& identity() const override { return _identity; }
503
504 /// @brief Closes when asked.
505 /// @param done Called with `true`.
506 void requestClose(std::function<void(bool closed)> done) override {
507 clearDetail();
508 if (done) {
509 done(true);
511 }
512
513 /// @brief Closes the "Try it" screen.
514 void forceClose() override { clearDetail(); }
515
516 /// @brief Shows an action's sections, as choosing it in the list does.
517 /// @param actionId The action id.
518 /// @return Whether a listed model has the action.
519 bool select(std::string const& actionId) {
520 for (Model const& model : _models) {
521 if (std::ranges::contains(model.actions, actionId, &ActionEntry::id)) {
522 showAction(model, actionId);
523 return true;
524 }
525 }
526 return false;
527 }
528
529 /// @brief The action shown.
530 /// @return Its id; empty with none.
531 [[nodiscard]] std::string const& selected() const noexcept { return _selected; }
532
533private:
534 // The panes' sizes, in units: the list as wide as the shell's sidebar plus a margin, the detail the rest.
535 static constexpr int kListWidth = 32;
536 static constexpr int kDetailWidth = 96;
537 // How much of an unreadable schema is shown.
538 static constexpr std::size_t kShownBytes = 200;
539
540 struct ActionEntry {
541 std::string id;
542 std::string schema;
543 std::unique_ptr<ButtonWidget> button;
544 };
545
546 struct Model {
547 std::string type;
548 std::string refusal;
549 std::unique_ptr<CollapsibleWidget> box;
550 std::unique_ptr<StackWidget> column;
551 std::unique_ptr<TextWidget> note;
552 std::vector<ActionEntry> actions;
553 };
554
555 struct ScreenItem {
556 std::string title;
557 std::unique_ptr<ButtonWidget> button;
558 };
559
560 [[nodiscard]] static std::string lower(std::string_view text) {
561 std::string out{text};
562 std::ranges::transform(out, out.begin(), [](char byte) {
563 return static_cast<char>(std::tolower(static_cast<unsigned char>(byte)));
564 });
565 return out;
566 }
567
568 [[nodiscard]] static std::string titleOf(CatalogEntry const& entry) {
569 if (std::string const* const text = std::get_if<std::string>(&entry.title.data)) {
570 return *text;
571 }
572 if (auto const* const object = std::get_if<Json::object_t>(&entry.title.data)) {
573 for (auto const& [key, value] : *object) {
574 std::string const* const text = std::get_if<std::string>(&value.data);
575 if (key == "fallback" && text != nullptr) {
576 return *text;
577 }
578 }
579 }
580 return entry.id;
581 }
582
583 // The model types the catalog names: its models and every action's model, in name order.
584 [[nodiscard]] std::set<std::string, std::less<>> modelTypes() const {
585 std::set<std::string, std::less<>> types;
586 if (_options.catalog.models) {
587 for (auto const& [type, info] : *_options.catalog.models) {
588 static_cast<void>(info);
589 types.insert(type);
590 }
591 }
592 if (_options.catalog.actions) {
593 for (auto const& [action, info] : *_options.catalog.actions) {
594 static_cast<void>(action);
595 if (!info.modelType.empty()) {
596 types.insert(info.modelType);
597 }
598 }
599 }
600 for (auto const& [type, document] : _options.schemas) {
601 static_cast<void>(document);
602 types.insert(type);
603 }
604 return types;
605 }
606
607 [[nodiscard]] std::unique_ptr<TextWidget> text(ContainerWidget& parent, std::string_view content,
608 TextRole role = TextRole::Normal) const {
609 auto made = _backend->createText(&parent);
610 made->setText(content);
611 made->setRole(role);
612 return made;
613 }
614
615 void listAll() {
616 _search = _backend->createTextInput(_list.get(), TextInputMode::SingleLine);
617 _search->setTestId("explorer_search");
618 _search->setPlaceholder("Search");
619 _search->setAccessibleName("Search screens and actions");
620 _search->setOnChange([this](std::string const& wanted) { filter(wanted); });
621 _screensHeading = text(*_list, {}, TextRole::Heading);
622 for (CatalogEntry const& entry : _options.catalog.entries) {
623 ScreenItem item{.title = titleOf(entry), .button = _backend->createButton(_list.get())};
624 item.button->setLabel(item.title);
625 item.button->setTestId("explorer_screen_" + entry.id);
626 item.button->setEnabled(entry.available || entry.isPublic);
627 item.button->setOnClick([this, screenId = entry.id] { open(screenId); });
628 _screens.push_back(std::move(item));
629 }
630 _screensHeading->setText("Screens (" + std::to_string(_screens.size()) + ")");
631 _modelsHeading = text(*_list, {}, TextRole::Heading);
632 for (std::string const& type : modelTypes()) {
633 Model model{.type = type,
634 .refusal = {},
635 .box = _backend->createCollapsible(_list.get()),
636 .column = {},
637 .note = {},
638 .actions = {}};
639 model.box->setTitle(type);
640 model.box->setTestId("explorer_model_" + type);
641 model.column = _backend->createStack(model.box.get(), Axis::Vertical);
642 model.column->setGap(1);
643 auto const found = _options.schemas.find(type);
644 if (found == _options.schemas.end()) {
645 model.refusal = "not described: the schemas did not arrive";
646 } else if (!found->second) {
647 model.refusal = "not described: " + found->second.error();
648 } else if (auto read = schemasOf(*found->second)) {
649 for (auto& [action, schema] : *read) {
650 ActionEntry entry{.id = action, .schema = std::move(schema), .button = {}};
651 entry.button = _backend->createButton(model.column.get());
652 entry.button->setLabel(action);
653 entry.button->setTestId("explorer_action_" + action);
654 entry.button->setOnClick([this, id = action] { select(id); });
655 model.actions.push_back(std::move(entry));
656 }
657 } else {
658 model.refusal = "not described: " + read.error();
659 }
660 if (!model.refusal.empty()) {
661 model.note = text(*model.column, model.refusal, TextRole::Muted);
662 }
663 _models.push_back(std::move(model));
664 }
665 _modelsHeading->setText("Models (" + std::to_string(_models.size()) + ")");
666 }
667
668 // Shows what a search matches: a screen by its title, an action by its id, a model by its type or any of its
669 // actions; a model with a matching action opens.
670 void filter(std::string const& wanted) {
671 std::string const needle = lower(wanted);
672 auto const matches = [&needle](std::string_view text) {
673 return needle.empty() || lower(text).contains(needle);
674 };
675 for (ScreenItem& item : _screens) {
676 item.button->setVisible(matches(item.title));
677 }
678 for (Model& model : _models) {
679 bool const typeMatches = matches(model.type);
680 bool any = false;
681 for (ActionEntry& action : model.actions) {
682 bool const shown = typeMatches || matches(action.id);
683 action.button->setVisible(shown);
684 any = any || shown;
685 }
686 model.box->setVisible(typeMatches || any);
687 if (!needle.empty() && any) {
688 model.box->setOpen(true);
689 }
690 }
691 }
692
693 void open(std::string const& screenId) const {
694 if (_options.onOpen) {
695 _options.onOpen(screenId);
696 }
697 }
698
699 // Takes the detail's widgets in reverse, so each child goes before its parent; the "Try it" screen goes first.
700 void clearDetail() {
701 _tryIt.reset();
702 while (!_sections.empty()) {
703 _sections.pop_back();
704 }
705 }
706
707 // Keeps a widget of the detail until the next choice.
708 template <class W>
709 W& keep(std::unique_ptr<W> widget) {
710 W& kept = *widget;
711 _sections.push_back(std::move(widget));
712 return kept;
713 }
714
715 void showNothingChosen() {
716 clearDetail();
717 _selected.clear();
718 keep(text(*_detail, "Choose an action to see its facts, its fields, where it is used, and to try it.",
719 TextRole::Muted))
720 .setTestId("explorer_hint");
721 }
722
723 // A card with a title, its content in a column the caller fills.
724 StackWidget& section(std::string_view title, std::string const& testId) {
725 PanelWidget& panel = keep(_backend->createPanel(_detail.get()));
726 panel.setTitle(title);
727 panel.setTestId(testId);
728 StackWidget& column = keep(_backend->createStack(&panel, Axis::Vertical));
729 column.setGap(2);
730 return column;
731 }
732
733 // A grid of texts, @p columns wide: the first row a heading when @p headed, else the first column labels.
734 void grid(StackWidget& parent, int columns, std::vector<std::string> const& cells, bool headed) {
735 GridWidget& made = keep(_backend->createGrid(&parent));
736 made.setColumns(columns);
737 made.setGap(2);
738 made.setLayout({.width = Sizing::stretch(), .height = {}});
739 auto const width = static_cast<std::size_t>(columns);
740 for (std::size_t index = 0; index < cells.size(); ++index) {
741 bool const muted = headed ? index < width : index % width == 0;
742 keep(text(made, cells.at(index), muted ? TextRole::Muted : TextRole::Normal));
743 }
744 }
745
746 [[nodiscard]] std::string_view permission(std::string const& actionId) const {
747 if (_permitted.contains(actionId)) {
748 return "permitted";
749 }
750 // The catalog's `permitted` covers only actions a served document names.
751 bool const named = _options.catalog.actions && _options.catalog.actions->contains(actionId);
752 return named ? "not permitted" : "decided when it runs";
753 }
754
755 // The catalog's facts of an action, as label and value cells; none when the catalog has none for it.
756 [[nodiscard]] std::vector<std::string> catalogFacts(std::string const& actionId) const {
757 if (!_options.catalog.actions || !_options.catalog.actions->contains(actionId)) {
758 return {};
759 }
760 ActionInfo const& info = _options.catalog.actions->at(actionId);
761 auto const listed = [](std::vector<std::string> const& names) {
762 return names.empty() ? std::string{"—"} : detail::explorer::joined(names, ", ");
763 };
764 std::vector<std::string> cells{"sends a credential",
765 info.credential ? "yes" : "no",
766 "server-calculated",
767 listed(info.calculatedFields),
768 "secret",
769 listed(info.secretFields)};
770 if (info.listFields) {
771 cells.insert(cells.end(), {"list fields", listed(*info.listFields)});
772 }
773 cells.insert(cells.end(), {"effects", effectsText(info)});
774 if (!info.records.empty()) {
775 cells.insert(cells.end(), {"records", recordsText(info)});
776 }
777 return cells;
778 }
779
780 // What an action declares it changes (spec 8 §5): why a screen did or did not update after it ran.
781 [[nodiscard]] static std::string effectsText(ActionInfo const& info) {
782 if (!info.effects) {
783 return "reads";
784 }
785 std::vector<std::string> effects;
786 for (ActionEffect const& effect : *info.effects) {
787 if (effect.kind == "changesInstance") {
788 effects.emplace_back("changes its instance");
789 } else if (effect.record) {
790 effects.push_back(effect.kind + " " + *effect.record);
791 } else {
792 effects.push_back(effect.kind);
793 }
794 }
795 return detail::explorer::joined(effects, ", ");
796 }
797
798 // The records an action's result holds, each with its key field and where the result holds it.
799 [[nodiscard]] static std::string recordsText(ActionInfo const& info) {
800 std::vector<std::string> records;
801 for (ActionRecordInfo const& record : info.records) {
802 std::vector<std::string> paths;
803 paths.reserve(record.paths.size());
804 for (std::string const& path : record.paths) {
805 paths.push_back(path.empty() ? std::string{"the result"} : path);
806 }
807 std::string key{" keyless"};
808 if (auto const& field = record.keyField) {
809 key = " by " + *field;
810 }
811 records.push_back(record.record + key + " at " + detail::explorer::joined(paths, ", "));
812 }
813 return detail::explorer::joined(records, "; ");
814 }
815
816 // The payload fingerprint and shape a schema carries, as label and value cells.
817 [[nodiscard]] static std::vector<std::string> payloadFacts(std::string const& schema) {
818 std::vector<std::string> cells;
819 auto parsed = parseJson(schema);
820 auto const* const object = parsed ? std::get_if<Json::object_t>(&parsed->data) : nullptr;
821 if (object == nullptr) {
822 return cells;
823 }
824 for (auto const& [key, value] : *object) {
825 if (key != "x-payloadFingerprint" && key != "x-payloadShape") {
826 continue;
827 }
828 std::string const* const plain = std::get_if<std::string>(&value.data);
829 cells.emplace_back(key == "x-payloadFingerprint" ? "payload fingerprint" : "payload shape");
830 cells.push_back(plain != nullptr ? *plain : glz::write_json(value).value_or(std::string{}));
831 }
832 return cells;
833 }
834
835 void showFacts(Model const& model, ActionEntry const& action) {
836 std::vector<std::string> cells{"model", model.type};
837 std::ranges::move(catalogFacts(action.id), std::back_inserter(cells));
838 std::ranges::move(payloadFacts(action.schema), std::back_inserter(cells));
839 cells.insert(cells.end(), {"may run it", std::string{permission(action.id)}});
840 grid(section("Facts", "explorer_facts"), 2, cells, false);
841 }
842
843 // Returns whether the schema reads.
844 bool showFields(ActionEntry const& action) {
845 StackWidget& content = section("Fields", "explorer_fields");
846 auto const fields = actionFields(action.schema);
847 if (!fields) {
848 keep(text(content, "unreadable schema: " + fields.error(), TextRole::Error));
849 keep(text(content, action.schema.substr(0, kShownBytes), TextRole::Muted));
850 return false;
851 }
852 if (fields->empty()) {
853 keep(text(content, "No fields.", TextRole::Muted));
854 return true;
855 }
856 std::vector<std::string> cells{"name", "type", "required", "rules"};
857 for (ActionField const& field : *fields) {
858 cells.insert(cells.end(),
859 {field.name, field.type, field.required ? "required" : "optional",
860 field.rules.empty() ? std::string{"—"} : detail::explorer::joined(field.rules, ", ")});
861 }
862 grid(content, 4, cells, true);
863 return true;
864 }
865
866 void showUses(std::string const& actionId) {
867 StackWidget& content = section("Used by", "explorer_uses");
868 auto const found = _uses.find(actionId);
869 if (found == _uses.end()) {
870 keep(text(content, "No loaded document names it.", TextRole::Muted));
871 return;
872 }
873 std::size_t index = 0;
874 for (auto const& [screenId, use] : found->second) {
875 StackWidget& row = keep(_backend->createStack(&content, Axis::Horizontal));
876 row.setGap(3);
877 keep(text(row, screenId + " " + use.pointer + " (" + std::string{actionUseKindName(use.kind)} + ")"))
878 .setLayout({.width = Sizing::stretch(), .height = {}});
879 // The app document is no screen to open.
880 if (screenId != "app") {
881 ButtonWidget& button = keep(_backend->createButton(&row));
882 button.setLabel("Open");
883 button.setTestId("explorer_open_" + std::to_string(index));
884 button.setOnClick([this, target = screenId] { open(target); });
885 }
886 ++index;
887 }
888 }
889
890 [[nodiscard]] TryItKind tryItKind(std::string const& actionId) const {
891 if (_options.catalog.views && _options.catalog.views->contains(actionId)) {
892 return TryItKind::Collection;
893 }
894 if (_options.catalog.wizards && _options.catalog.wizards->contains(actionId)) {
895 return TryItKind::Wizard;
896 }
897 return TryItKind::Form;
898 }
899
900 void showTryIt(Model const& model, ActionEntry const& action, bool readable) {
901 StackWidget& content = section("Try it", "explorer_tryit");
902 auto const status = [this, &content](std::string const& reason) {
903 keep(text(content, reason, TextRole::Muted)).setTestId("explorer_tryit_status");
904 };
905 if (_options.host == nullptr || !_options.provide) {
906 status("Not offered here.");
907 return;
908 }
909 if (!readable) {
910 status("Not offered: the schema cannot be read.");
911 return;
912 }
913 if (permission(action.id) == "not permitted") {
914 status("not permitted");
915 return;
916 }
917 std::string const screenId = tryItScreenId(action.id);
918 auto loaded = loadDocumentText(tryItDocument(action.id, model.type, tryItKind(action.id)),
919 LoadOptions{.catalog = tryItCatalog(_options.facts, _options.schemas)});
920 if (!loaded) {
921 status("Cannot build it: " + loaded.error().message());
922 return;
923 }
924 _options.provide(screenId, std::make_shared<Document const>(std::move(*loaded)));
925 SlotWidget& slot = keep(_backend->createSlot(&content));
926 slot.setLayout({.width = Sizing::stretch(), .height = {}});
927 auto mounted = _options.host->mountInto(screenId, {}, slot);
928 if (!mounted) {
929 status("Cannot mount it: " + mounted.error().reason);
930 return;
931 }
932 _tryIt = std::move(*mounted);
933 }
934
935 void showSchema(std::string const& actionId, ActionEntry const& action) {
936 CollapsibleWidget& box = keep(_backend->createCollapsible(_detail.get()));
937 box.setTitle("Schema JSON");
938 box.setTestId("explorer_schema");
939 StackWidget& column = keep(_backend->createStack(&box, Axis::Vertical));
940 column.setGap(2);
941 keep(text(column, action.schema, TextRole::Muted));
942 for (auto const* const documents : {&_options.catalog.views, &_options.catalog.wizards}) {
943 if (!*documents) {
944 continue;
945 }
946 if (auto const found = (*documents)->find(actionId); found != (*documents)->end()) {
947 keep(text(column, found->second, TextRole::Muted));
948 }
949 }
950 }
951
952 void showAction(Model const& model, std::string const& actionId) {
953 clearDetail();
954 _selected = actionId;
955 auto const& action = *std::ranges::find(model.actions, actionId, &ActionEntry::id);
956 keep(text(*_detail, model.type + " / " + actionId, TextRole::Heading)).setTestId("explorer_title");
957 showFacts(model, action);
958 bool const readable = showFields(action);
959 showUses(actionId);
960 showTryIt(model, action, readable);
961 showSchema(actionId, action);
962 }
963
964 IViewBackend* _backend;
965 ExplorerOptions _options;
966 reactive::Scope _scope;
967 reactive::Computed<std::string>& _title;
968 reactive::Computed<bool>& _dirty;
969 reactive::Computed<Value>& _identity;
970 std::set<std::string, std::less<>> _permitted;
971 // Every use of each action, with the screen whose document names it.
972 std::map<std::string, std::vector<std::pair<std::string, ActionUse>>, std::less<>> _uses;
973 // Declared parent before child, so destruction takes the children first.
974 std::unique_ptr<SplitterWidget> _root;
975 std::unique_ptr<ScrollWidget> _listScroll;
976 std::unique_ptr<StackWidget> _list;
977 std::unique_ptr<TextInputWidget> _search;
978 std::unique_ptr<TextWidget> _screensHeading;
979 std::vector<ScreenItem> _screens;
980 std::unique_ptr<TextWidget> _modelsHeading;
981 std::vector<Model> _models;
982 std::unique_ptr<ScrollWidget> _detailScroll;
983 std::unique_ptr<StackWidget> _detail;
984 // The detail's widgets, each after its parent, so clearing them in reverse takes the children first.
985 std::vector<std::unique_ptr<Widget>> _sections;
986 std::unique_ptr<ScreenHandle> _tryIt;
987 std::string _selected;
988};
989
990} // namespace morph::ui
Node text(TextOptions const &options)
A text node.
Definition author.hpp:3126
Node panel(Nodes const &children, PanelOptions const &options={})
A panel node.
Definition author.hpp:3446
Node column(Nodes const &children, ContainerOptions const &options={})
A column node.
Definition author.hpp:3394
Node button(ButtonOptions const &options)
A button node.
Definition author.hpp:3156
Handle< T > row()
The innermost row, {"row": ""}.
Definition author.hpp:1145
AnyHandle content()
The content sizing, "content": as much as the content needs.
Definition author.hpp:1104
bool names(DocNode const &node, std::string_view key)
Whether a node has a property that names it: present, and not the constant "" or null.
Definition author.hpp:4373
std::vector< TableColumn > cells(std::vector< Node > const &cells)
Columns from cells alone, without ids or labels.
Definition author.hpp:3686
Command open(Element const &dialog)
open: opens a dialog or drawer.
Definition author.hpp:2779
What the UI service's client and server share (spec 5 §12): content hashes, the bundle's manifest,...
static std::expected< FormModel, SchemaError > fromSchema(std::string_view actionType, std::string_view schemaJson, FieldOverlays const &overlays={})
Reads a schema document, with the overlays a document or an application states for its fields.
One x-rules entry, or a condition inside one, and its translation into expr/1.
Definition rules.hpp:191
A derived value: computed lazily on first read, cached, and recomputed only after a source it read re...
Definition signal.hpp:168
The owner of a signal graph: every node takes one, and every operation runs on its owner.
Definition runtime.hpp:70
A widget with children. A factory given this container appends the new widget as its last child.
Definition backend.hpp:103
The framework's ScreenHost over UI documents: it mounts them through an Interpreter,...
Definition host.hpp:344
std::string const & selected() const noexcept
The action shown.
Definition explorer.hpp:522
reactive::Computed< bool > const & dirty() const override
Never dirty.
Definition explorer.hpp:489
~Explorer() override
Unmounts the "Try it" screen first, then the widgets it sits in.
Definition explorer.hpp:481
void requestClose(std::function< void(bool closed)> done) override
Closes when asked.
Definition explorer.hpp:497
Explorer(reactive::Runtime &runtime, IViewBackend &backend, ContainerWidget &page, ExplorerOptions options)
The explorer, appended to page.
Definition explorer.hpp:439
void forceClose() override
Closes the "Try it" screen.
Definition explorer.hpp:505
reactive::Computed< std::string > const & title() const override
The title.
Definition explorer.hpp:485
reactive::Computed< Value > const & identity() const override
No identity: one explorer per workspace.
Definition explorer.hpp:493
bool select(std::string const &actionId)
Shows an action's sections, as choosing it in the list does.
Definition explorer.hpp:510
Makes widgets. Each factory appends the new widget to parent, or makes a root when it is null,...
Definition backend.hpp:810
Children stacked along one axis, fixed at creation.
Definition backend.hpp:310
The document's typed value (spec 5 §3): what every expression evaluates to.
Definition value.hpp:157
The UI document (spec 5 §3): its model — declarations, commands, view nodes, components and customiza...
std::expected< Document, Refusal > loadDocumentText(std::string_view text, LoadOptions const &options={})
Parses JSON text as a UI document and runs its load-time validation.
bool listed(std::span< std::string_view const > names, std::string_view name)
Whether a list of names holds one.
std::shared_ptr< DocNode const > DocNodePtr
A shared, immutable view node.
Definition document.hpp:387
std::vector< ScopeDecl const * > ScopeChain
The scopes around a node, innermost last: the aliases in its reach.
Definition explorer.hpp:71
std::map< std::string, ModelSchemas, std::less<> > SchemasByModel
The schemas documents of the models, by model type.
Definition explorer.hpp:327
std::string tryItScreenId(std::string_view actionId)
The screen id "Try it" mounts an action's document under.
Definition explorer.hpp:291
std::expected< std::vector< ActionField >, std::string > actionFields(std::string_view schemaJson)
The fields of an action, in x-order, as the forms engine reads its schema.
Definition explorer.hpp:247
void addUse(std::vector< ActionUse > &uses, ScopeChain const &scopes, Catalog const &catalog, std::string_view alias, std::string action, ActionUseKind kind, std::string pointer)
Records one use, its model type the alias's, else the catalog's for the action.
Definition explorer.hpp:95
std::string typeOf(ScopeChain const &scopes, std::string_view alias)
The model type an alias binds, from the innermost scope that declares it.
Definition explorer.hpp:77
std::vector< ActionUse > actionUses(Document const &document, Catalog const &catalog={})
Every place a loaded document names an action: its queries and mutations at any scope,...
Definition explorer.hpp:168
std::string joined(std::vector< std::string > const &parts, std::string_view separator)
Texts joined by a separator.
Definition explorer.hpp:198
std::expected< std::map< std::string, std::string, std::less<> >, std::string > schemasOf(std::string_view document)
The actions a model's schemas document lists, with each one's schema.
Definition explorer.hpp:332
TryItKind
Which widget "Try it" builds for an action.
Definition explorer.hpp:282
@ Collection
A collection over the catalog's view document of the action.
Definition explorer.hpp:284
@ Form
A form over the action's schema.
Definition explorer.hpp:283
void nodeUses(std::vector< ActionUse > &uses, ScopeChain &scopes, Catalog const &catalog, DocNode const &node)
Records the uses of a node and everything under it, with the scope it creates.
Definition explorer.hpp:128
Catalog tryItCatalog(Catalog catalog, SchemasByModel const &schemas)
The catalog a "Try it" document loads with: catalog, whose action lookup also answers an action only ...
Definition explorer.hpp:357
void scopeUses(std::vector< ActionUse > &uses, ScopeChain const &scopes, Catalog const &catalog)
Records the queries and mutations of the innermost scope.
Definition explorer.hpp:112
constexpr std::string_view actionUseKindName(ActionUseKind kind) noexcept
The name of an action use's kind.
Definition explorer.hpp:51
std::string typeText(forms::FieldSpec const &field)
A field's type as the explorer lists it.
Definition explorer.hpp:209
ActionUseKind
What names an action in a document.
Definition explorer.hpp:40
@ Mutation
A mutation declaration.
Definition explorer.hpp:42
@ Wizard
A wizard node.
Definition explorer.hpp:45
@ Query
A query declaration.
Definition explorer.hpp:41
@ Collection
A collection node.
Definition explorer.hpp:44
std::string tryItDocument(std::string_view actionId, std::string_view modelType, TryItKind kind)
The one-node document "Try it" mounts for an action.
Definition explorer.hpp:306
std::expected< std::string, std::string > ModelSchemas
A model's schemas document: {actionType: schema}, or why the server would not describe the model.
Definition explorer.hpp:324
FormModel: the immutable runtime model of one action's form, read from the schema JSON schemaJson<A>(...
FieldKind
The control a field gets, which the JSON type alone does not say.
constexpr std::string_view fieldKindName(FieldKind kind) noexcept
The stable name of a field kind (what Overrides::byKind documents and tests print).
The host contract (spec 6 §4): how a host mounts UI documents and closes them, whether it owns its ow...
One selectable value of an Enum or a Choice.
std::string label
What the user sees.
Everything the engine knows about one field, from its schema property.
std::string unitAscii
ExtUnits.unitAscii.
std::vector< ChoiceOption > enumOptions
An Enum's options, in schema order.
std::string itemType
An Array's item JSON type ("string" when undeclared).
std::optional< std::uint32_t > decimalPlaces
x-decimalPlaces: the declared precision of a Decimal or Quantity.
std::optional< ChoiceSpec > choice
A Choice's options source.
FieldKind kind
The control and encoder.
std::vector< UnitOption > units
A Decimal's or Quantity's entry units: the canonical one first, then x-unitAlternatives.
std::string unit
The display unit (ExtUnits.unitUnicode, else unitAscii), empty when none.
One field of an action, as the explorer lists it.
Definition explorer.hpp:179
std::string name
The wire name.
Definition explorer.hpp:181
std::vector< std::string > rules
The bounds and rules: min …, max …, multiple of …, read-only, hidden, and the name of each x-rules ru...
Definition explorer.hpp:189
bool required
Whether the action requires it.
Definition explorer.hpp:186
std::string type
The type: the field kind's name, or for an enum "one of: …", a quantity "quantity in <unit>" with its...
Definition explorer.hpp:184
What the catalog says about an action.
Definition document.hpp:78
One place a document names an action.
Definition explorer.hpp:57
std::string actionId
The action id.
Definition explorer.hpp:59
ActionUseKind kind
What names it.
Definition explorer.hpp:63
std::string pointer
The JSON Pointer of the declaration or node.
Definition explorer.hpp:65
std::string modelType
The model type the alias it goes through binds; empty when neither the document nor the catalog says.
Definition explorer.hpp:61
The body of a ui-catalog reply: the catalog filtered for the principal, and the facts a client's load...
Definition bundle.hpp:280
The facts load-time validation needs from the application's catalog. Local mode answers them from the...
Definition document.hpp:105
std::function< std::optional< std::string >(std::string_view actionId)> schema
The schema JSON of an action, as forms::schemaJson<A>() emits it: what a form node builds its form fr...
Definition document.hpp:113
std::function< std::optional< ActionInfo >(std::string_view actionId)> action
Looks up an action by its id.
Definition document.hpp:109
A view node of the document (spec 5 §9), or a structured part of one (a table column,...
Definition document.hpp:394
std::map< std::string, std::vector< DocNodePtr >, std::less<> > children
Child node lists, by key (children, header, loading, error, default, cell, fallback).
Definition document.hpp:406
std::string path
The node's JSON Pointer.
Definition document.hpp:400
std::optional< ScopeDecl > scope
The declarations of the scope the node creates (dialog, drawer, switch case, forEach or table row); e...
Definition document.hpp:418
std::string_view name(std::string_view key) const
A plain string property.
Definition document.hpp:455
std::map< std::string, std::vector< DocNodePtr >, std::less<> > parts
Structured parts, by key (columns, tabs, cases, items, a11y, action, fields, …).
Definition document.hpp:408
std::string kind
The node kind (text, table, …), or for a part its part kind (column, tab, case, …).
Definition document.hpp:396
A loaded and validated UI document: a screen, or the app document (id app, spec 6 §5).
Definition document.hpp:536
DocNodePtr view
The view; nullptr for the app document.
Definition document.hpp:570
ScopeDecl scope
The screen scope's declarations.
Definition document.hpp:556
What an Explorer shows, and what it reports.
Definition explorer.hpp:401
CatalogReply catalog
The catalog for the current principal: its entries, permitted, and the facts.
Definition explorer.hpp:403
std::function< void(std::string const &screenId)> onOpen
Called with a screen id when the person asks to open it.
Definition explorer.hpp:417
Catalog facts
The client's catalog facts, which "Try it" documents load with; empty uses catalog....
Definition explorer.hpp:405
SchemasByModel schemas
The schemas document of every model the catalog names, by model type.
Definition explorer.hpp:407
std::function< void(std::string const &screenId, std::shared_ptr< Document const > document)> provide
Called with a "Try it" document before it is mounted, for the host's documents lookup to answer its s...
Definition explorer.hpp:415
std::map< std::string, std::shared_ptr< Document const >, std::less<> > documents
The loaded documents by screen id, the app document under app.
Definition explorer.hpp:409
DocumentScreenHost * host
What "Try it" mounts its document through. Borrowed; null offers no "Try it".
Definition explorer.hpp:411
std::string name
The alias.
Definition document.hpp:157
A mutation (spec 5 §5).
Definition document.hpp:307
std::string model
The model alias it goes through.
Definition document.hpp:313
std::string action
The action id it runs.
Definition document.hpp:315
std::string path
The declaration's JSON Pointer.
Definition document.hpp:311
A query (spec 5 §5).
Definition document.hpp:193
std::string model
The model alias it goes through.
Definition document.hpp:199
std::string path
The declaration's JSON Pointer.
Definition document.hpp:197
std::string action
The action id it runs, or evaluates.
Definition document.hpp:201
The declarations of one scope, in declaration order.
Definition document.hpp:367
std::vector< MutationDecl > mutations
Mutations.
Definition document.hpp:377
std::vector< QueryDecl > queries
Queries.
Definition document.hpp:375
static constexpr Sizing stretch(int weight=1) noexcept
A share of the leftover space.
Definition view.hpp:294
The backend contract: the retained widgets a mount drives, and the factory that makes them.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
std::expected< Json, std::string > parseJson(std::string_view text)
Parses JSON text into the document layer's JSON tree.
Definition value.hpp:752
@ Muted
Secondary text.
Definition view.hpp:201
@ Error
An error message.
Definition view.hpp:203
Node grid(Grid spec)
Cells in a fixed number of columns.
Definition view.hpp:1234
Node text(Text spec)
Static or bound text.
Definition view.hpp:1194
@ Vertical
Top to bottom.
Definition view.hpp:223
@ Horizontal
Left to right.
Definition view.hpp:224
SlotBinding< std::decay_t< F > > slot(SlotId slotId, F &&read)
A slot with an id.
Definition view.hpp:63