22#include "../attributes.hpp"
52 constexpr std::array<std::string_view, 5> kNames{
"query",
"mutation",
"form",
"collection",
"wizard"};
53 return kNames.at(
static_cast<std::size_t
>(kind));
68namespace detail::explorer {
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()) {
96 std::string_view alias, std::string action,
ActionUseKind kind, std::string pointer) {
97 std::string type =
typeOf(scopes, alias);
99 if (type.empty() && catalog.
action) {
100 if (std::optional<ActionInfo>
const info = catalog.
action(action)) {
101 type = info->modelType;
105 .actionId = std::move(action), .modelType = std::move(type), .kind = kind, .pointer = std::move(pointer)});
130 scopes.push_back(&*node.
scope);
133 static constexpr std::array<std::pair<std::string_view, ActionUseKind>, 3> kNamed{
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);
140 for (
auto const& [key, nodes] : node.
children) {
141 static_cast<void>(key);
143 nodeUses(uses, scopes, catalog, *child);
146 for (
auto const& [key, parts] : node.
parts) {
147 static_cast<void>(key);
149 nodeUses(uses, scopes, catalog, *part);
169 std::vector<ActionUse> uses;
170 detail::explorer::ScopeChain scopes{&document.
scope};
171 detail::explorer::scopeUses(uses, scopes, catalog);
173 detail::explorer::nodeUses(uses, scopes, catalog, *document.
view);
192namespace detail::explorer {
198[[nodiscard]]
inline std::string
joined(std::vector<std::string>
const& parts, std::string_view separator) {
200 for (std::string
const& part : parts) {
201 out += out.empty() ? part : std::string{separator} + part;
211 if (field.
kind == FieldKind::Enum) {
212 std::vector<std::string> labels;
215 labels.push_back(option.
label);
217 return "one of: " +
joined(labels,
", ");
219 if (field.
kind == FieldKind::Quantity) {
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);
225 return others.empty() ?
text :
text +
" (or " +
joined(others,
", ") +
")";
228 return "decimal, " + std::to_string(*field.
decimalPlaces) +
" places";
230 if (field.
kind == FieldKind::Choice && field.
choice) {
231 return "choice from " + field.
choice->optionsAction;
233 if (field.
kind == FieldKind::Array) {
236 if (field.
kind == FieldKind::ObjectArray) {
237 return "list of objects";
247[[nodiscard]]
inline std::expected<std::vector<ActionField>, std::string>
actionFields(std::string_view schemaJson) {
250 std::string
const& where = model.error().path;
251 return std::unexpected(where.empty() ? model.error().message : where +
": " + model.error().message);
253 std::vector<ActionField> fields;
256 if (
auto const& minimum = spec.exactMinimum ? spec.exactMinimum : spec.minimum) {
257 field.
rules.push_back(
"min " + *minimum);
259 if (
auto const& maximum = spec.exactMaximum ? spec.exactMaximum : spec.maximum) {
260 field.
rules.push_back(
"max " + *maximum);
262 if (spec.multipleOf) {
263 field.
rules.push_back(
"multiple of " + *spec.multipleOf);
266 field.
rules.emplace_back(
"read-only");
269 field.
rules.emplace_back(
"hidden");
272 if (rule.names(spec.name)) {
273 field.
rules.emplace_back(rule.kindName());
276 fields.push_back(std::move(field));
292 return "explorer:try:" + std::string{actionId};
307 auto const quoted = [](std::string_view
text) {
return glz::write_json(std::string{
text}).value_or(
"\"\""); };
308 std::string
const action = quoted(actionId);
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"}]},""]}})";
321 R
"(","testId":"tryit","model":"target","action":)" + action + "}";
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":[)" +
333using ModelSchemas = std::expected<std::string, std::string>;
336using SchemasByModel = std::map<std::string, ModelSchemas, std::less<>>;
341[[nodiscard]]
inline std::expected<std::map<std::string, std::string, std::less<>>, std::string>
schemasOf(
342 std::string_view document) {
345 return std::unexpected(std::move(parsed.error()));
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"});
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{}));
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) {
373 for (
auto& [action, schema] : *read) {
374 known->emplace(action, std::pair{type, std::move(schema)});
379 known](std::string_view actionId) -> std::optional<ActionInfo> {
381 if (std::optional<ActionInfo> info = inner(actionId)) {
385 auto const found = known->find(actionId);
386 if (found == known->end()) {
390 info.modelType = found->second.first;
394 known](std::string_view actionId) -> std::optional<std::string> {
396 if (std::optional<std::string> schema = inner(actionId)) {
400 auto const found = known->find(actionId);
401 if (found == known->end()) {
404 return found->second.second;
418 std::map<std::string, std::shared_ptr<Document const>, std::less<>>
documents;
424 std::function<void(std::string
const& screenId, std::shared_ptr<Document const> document)>
provide;
426 std::function<void(std::string
const& screenId)>
onOpen;
441class Explorer final :
public ScreenHandle {
450 : _backend{&backend},
451 _options{std::move(options)},
453 _title{_scope.computed([] {
return std::string{
"Explorer"}; })},
454 _dirty{_scope.computed([] {
return false; })},
455 _identity{_scope.computed([] {
return Value{}; })},
460 _detail{backend.createStack(_detailScroll.get(),
Axis::Vertical)} {
461 if (!_options.facts.action && !_options.facts.model) {
462 _options.facts = _options.catalog.facts();
464 _permitted.insert(_options.catalog.permitted.begin(), _options.catalog.permitted.end());
465 for (
auto const& [screenId, document] : _options.documents) {
467 std::string action = use.actionId;
468 _uses[std::move(action)].emplace_back(screenId, std::move(use));
472 _root->setTestId(
"explorer");
473 _root->setSizes({kListWidth, kDetailWidth});
480 _detail->setTestId(
"explorer_detail");
506 void requestClose(std::function<
void(
bool closed)> done)
override {
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);
531 [[nodiscard]] std::string
const&
selected() const noexcept {
return _selected; }
535 static constexpr int kListWidth = 32;
536 static constexpr int kDetailWidth = 96;
538 static constexpr std::size_t kShownBytes = 200;
543 std::unique_ptr<ButtonWidget> button;
549 std::unique_ptr<CollapsibleWidget> box;
550 std::unique_ptr<StackWidget> column;
551 std::unique_ptr<TextWidget> note;
552 std::vector<ActionEntry> actions;
557 std::unique_ptr<ButtonWidget> button;
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)));
568 [[nodiscard]]
static std::string titleOf(CatalogEntry
const& entry) {
569 if (std::string
const*
const text = std::get_if<std::string>(&entry.title.data)) {
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) {
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);
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);
600 for (
auto const& [type, document] : _options.schemas) {
601 static_cast<void>(document);
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);
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));
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,
635 .box = _backend->createCollapsible(_list.get()),
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));
658 model.refusal =
"not described: " + read.error();
660 if (!model.refusal.empty()) {
661 model.note = text(*model.column, model.refusal, TextRole::Muted);
663 _models.push_back(std::move(model));
665 _modelsHeading->setText(
"Models (" + std::to_string(_models.size()) +
")");
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);
675 for (ScreenItem& item : _screens) {
676 item.button->setVisible(matches(item.title));
678 for (Model& model : _models) {
679 bool const typeMatches = matches(model.type);
681 for (ActionEntry& action : model.actions) {
682 bool const shown = typeMatches || matches(action.id);
683 action.button->setVisible(shown);
686 model.box->setVisible(typeMatches || any);
687 if (!needle.empty() && any) {
688 model.box->setOpen(
true);
693 void open(std::string
const& screenId)
const {
694 if (_options.onOpen) {
695 _options.onOpen(screenId);
702 while (!_sections.empty()) {
703 _sections.pop_back();
709 W& keep(std::unique_ptr<W> widget) {
711 _sections.push_back(std::move(widget));
715 void showNothingChosen() {
718 keep(text(*_detail,
"Choose an action to see its facts, its fields, where it is used, and to try it.",
720 .setTestId(
"explorer_hint");
724 StackWidget& section(std::string_view
title, std::string
const& testId) {
725 PanelWidget&
panel = keep(_backend->createPanel(_detail.get()));
727 panel.setTestId(testId);
728 StackWidget&
column = keep(_backend->createStack(&panel, Axis::Vertical));
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);
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));
746 [[nodiscard]] std::string_view permission(std::string
const& actionId)
const {
747 if (_permitted.contains(actionId)) {
751 bool const named = _options.catalog.actions && _options.catalog.actions->contains(actionId);
752 return named ?
"not permitted" :
"decided when it runs";
756 [[nodiscard]] std::vector<std::string> catalogFacts(std::string
const& actionId)
const {
757 if (!_options.catalog.actions || !_options.catalog.actions->contains(actionId)) {
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,
", ");
764 std::vector<std::string>
cells{
"sends a credential",
765 info.credential ?
"yes" :
"no",
767 listed(info.calculatedFields),
769 listed(info.secretFields)};
770 if (info.listFields) {
771 cells.insert(
cells.end(), {
"list fields", listed(*info.listFields)});
773 cells.insert(
cells.end(), {
"effects", effectsText(info)});
774 if (!info.records.empty()) {
775 cells.insert(
cells.end(), {
"records", recordsText(info)});
781 [[nodiscard]]
static std::string effectsText(ActionInfo
const& info) {
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);
792 effects.push_back(effect.kind);
795 return detail::explorer::joined(effects,
", ");
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);
807 std::string key{
" keyless"};
808 if (
auto const& field = record.keyField) {
809 key =
" by " + *field;
811 records.push_back(record.record + key +
" at " + detail::explorer::joined(paths,
", "));
813 return detail::explorer::joined(records,
"; ");
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) {
824 for (
auto const& [key, value] : *
object) {
825 if (key !=
"x-payloadFingerprint" && key !=
"x-payloadShape") {
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{}));
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);
844 bool showFields(ActionEntry
const& action) {
852 if (fields->empty()) {
856 std::vector<std::string>
cells{
"name",
"type",
"required",
"rules"};
859 {field.name, field.type, field.required ?
"required" :
"optional",
862 grid(content, 4, cells,
true);
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));
873 std::size_t index = 0;
874 for (
auto const& [screenId, use] : found->second) {
875 StackWidget&
row = keep(_backend->createStack(&content, Axis::Horizontal));
877 keep(
text(row, screenId +
" " + use.pointer +
" (" + std::string{actionUseKindName(use.kind)} +
")"))
878 .setLayout({.width = Sizing::stretch(), .height = {}});
880 if (screenId !=
"app") {
881 ButtonWidget&
button = keep(_backend->createButton(&row));
883 button.setTestId(
"explorer_open_" + std::to_string(index));
884 button.setOnClick([
this, target = screenId] {
open(target); });
890 [[nodiscard]]
TryItKind tryItKind(std::string
const& actionId)
const {
891 if (_options.catalog.views && _options.catalog.views->contains(actionId)) {
892 return TryItKind::Collection;
894 if (_options.catalog.wizards && _options.catalog.wizards->contains(actionId)) {
895 return TryItKind::Wizard;
897 return TryItKind::Form;
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");
905 if (_options.host ==
nullptr || !_options.provide) {
906 status(
"Not offered here.");
910 status(
"Not offered: the schema cannot be read.");
913 if (permission(action.id) ==
"not permitted") {
914 status(
"not permitted");
919 LoadOptions{.catalog = tryItCatalog(_options.facts, _options.schemas)});
921 status(
"Cannot build it: " + loaded.error().message());
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);
929 status(
"Cannot mount it: " + mounted.error().reason);
932 _tryIt = std::move(*mounted);
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));
941 keep(
text(column, action.schema, TextRole::Muted));
942 for (
auto const*
const documents : {&_options.catalog.views, &_options.catalog.wizards}) {
946 if (
auto const found = (*documents)->find(actionId); found != (*documents)->end()) {
947 keep(
text(column, found->second, TextRole::Muted));
952 void showAction(Model
const& model, std::string
const& actionId) {
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);
960 showTryIt(model, action, readable);
961 showSchema(actionId, action);
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;
972 std::map<std::string, std::vector<std::pair<std::string, ActionUse>>, std::less<>> _uses;
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;
985 std::vector<std::unique_ptr<Widget>> _sections;
986 std::unique_ptr<ScreenHandle> _tryIt;
987 std::string _selected;
Node text(TextOptions const &options)
A text node.
Node panel(Nodes const &children, PanelOptions const &options={})
A panel node.
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 content()
The content sizing, "content": as much as the content needs.
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.
std::vector< TableColumn > cells(std::vector< Node > const &cells)
Columns from cells alone, without ids or labels.
Command open(Element const &dialog)
open: opens a dialog or drawer.
What the UI service's client and server share (spec 5 §12): content hashes, the bundle's manifest,...
A derived value: computed lazily on first read, cached, and recomputed only after a source it read re...
The owner of a signal graph: every node takes one, and every operation runs on its owner.
The framework's ScreenHost over UI documents: it mounts them through an Interpreter,...
std::string const & selected() const noexcept
The action shown.
reactive::Computed< bool > const & dirty() const override
Never dirty.
~Explorer() override
Unmounts the "Try it" screen first, then the widgets it sits in.
void requestClose(std::function< void(bool closed)> done) override
Closes when asked.
Explorer(reactive::Runtime &runtime, IViewBackend &backend, ContainerWidget &page, ExplorerOptions options)
The explorer, appended to page.
void forceClose() override
Closes the "Try it" screen.
reactive::Computed< std::string > const & title() const override
The title.
reactive::Computed< Value > const & identity() const override
No identity: one explorer per workspace.
bool select(std::string const &actionId)
Shows an action's sections, as choosing it in the list does.
Makes widgets. Each factory appends the new widget to parent, or makes a root when it is null,...
The document's typed value (spec 5 §3): what every expression evaluates to.
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.
std::vector< ScopeDecl const * > ScopeChain
The scopes around a node, innermost last: the aliases in its reach.
std::map< std::string, ModelSchemas, std::less<> > SchemasByModel
The schemas documents of the models, by model type.
std::string tryItScreenId(std::string_view actionId)
The screen id "Try it" mounts an action's document under.
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.
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.
std::string typeOf(ScopeChain const &scopes, std::string_view alias)
The model type an alias binds, from the innermost scope that declares it.
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,...
std::string joined(std::vector< std::string > const &parts, std::string_view separator)
Texts joined by a separator.
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.
TryItKind
Which widget "Try it" builds for an action.
@ Collection
A collection over the catalog's view document of the action.
@ Form
A form over the action's schema.
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.
Catalog tryItCatalog(Catalog catalog, SchemasByModel const &schemas)
The catalog a "Try it" document loads with: catalog, whose action lookup also answers an action only ...
void scopeUses(std::vector< ActionUse > &uses, ScopeChain const &scopes, Catalog const &catalog)
Records the queries and mutations of the innermost scope.
constexpr std::string_view actionUseKindName(ActionUseKind kind) noexcept
The name of an action use's kind.
std::string typeText(forms::FieldSpec const &field)
A field's type as the explorer lists it.
ActionUseKind
What names an action in a document.
@ Mutation
A mutation declaration.
@ Query
A query declaration.
@ Collection
A collection node.
std::string tryItDocument(std::string_view actionId, std::string_view modelType, TryItKind kind)
The one-node document "Try it" mounts for an action.
std::expected< std::string, std::string > ModelSchemas
A model's schemas document: {actionType: schema}, or why the server would not describe the model.
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 field of an action, as the explorer lists it.
std::string name
The wire name.
std::vector< std::string > rules
The bounds and rules: min …, max …, multiple of …, read-only, hidden, and the name of each x-rules ru...
bool required
Whether the action requires it.
std::string type
The type: the field kind's name, or for an enum "one of: …", a quantity "quantity in <unit>" with its...
What the catalog says about an action.
One place a document names an action.
std::string actionId
The action id.
ActionUseKind kind
What names it.
std::string pointer
The JSON Pointer of the declaration or node.
std::string modelType
The model type the alias it goes through binds; empty when neither the document nor the catalog says.
The body of a ui-catalog reply: the catalog filtered for the principal, and the facts a client's load...
The facts load-time validation needs from the application's catalog. Local mode answers them from the...
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...
std::function< std::optional< ActionInfo >(std::string_view actionId)> action
Looks up an action by its id.
A view node of the document (spec 5 §9), or a structured part of one (a table column,...
std::map< std::string, std::vector< DocNodePtr >, std::less<> > children
Child node lists, by key (children, header, loading, error, default, cell, fallback).
std::string path
The node's JSON Pointer.
std::optional< ScopeDecl > scope
The declarations of the scope the node creates (dialog, drawer, switch case, forEach or table row); e...
std::string_view name(std::string_view key) const
A plain string property.
std::map< std::string, std::vector< DocNodePtr >, std::less<> > parts
Structured parts, by key (columns, tabs, cases, items, a11y, action, fields, …).
std::string kind
The node kind (text, table, …), or for a part its part kind (column, tab, case, …).
A loaded and validated UI document: a screen, or the app document (id app, spec 6 §5).
DocNodePtr view
The view; nullptr for the app document.
ScopeDecl scope
The screen scope's declarations.
What an Explorer shows, and what it reports.
CatalogReply catalog
The catalog for the current principal: its entries, permitted, and the facts.
std::function< void(std::string const &screenId)> onOpen
Called with a screen id when the person asks to open it.
Catalog facts
The client's catalog facts, which "Try it" documents load with; empty uses catalog....
SchemasByModel schemas
The schemas document of every model the catalog names, by model type.
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...
std::map< std::string, std::shared_ptr< Document const >, std::less<> > documents
The loaded documents by screen id, the app document under app.
DocumentScreenHost * host
What "Try it" mounts its document through. Borrowed; null offers no "Try it".
std::string name
The alias.
std::string model
The model alias it goes through.
std::string action
The action id it runs.
std::string path
The declaration's JSON Pointer.
std::string model
The model alias it goes through.
std::string path
The declaration's JSON Pointer.
std::string action
The action id it runs, or evaluates.
The declarations of one scope, in declaration order.
std::vector< MutationDecl > mutations
Mutations.
std::vector< QueryDecl > queries
Queries.
static constexpr Sizing stretch(int weight=1) noexcept
A share of the leftover space.
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.
Node grid(Grid spec)
Cells in a fixed number of columns.
Node text(Text spec)
Static or bound text.
@ Horizontal
Left to right.
SlotBinding< std::decay_t< F > > slot(SlotId slotId, F &&read)
A slot with an id.