26#include "../../attributes.hpp"
27#include "../../core/callback_scope.hpp"
28#include "../../core/completion.hpp"
39namespace morph::forms {
48 std::vector<std::pair<std::string, std::string>>
prefill;
59 [[nodiscard]]
static std::expected<WizardModel, SchemaError>
fromSchema(std::string_view wizardId,
60 std::string_view wizardJson) {
61 using detail::engine::Json;
62 auto const dom = detail::engine::parseJson(wizardJson);
63 if (!dom || !dom->is_object()) {
64 return std::unexpected(
SchemaError{.path =
"", .message =
"a wizard document must be a JSON object"});
68 auto const*
const title = detail::engine::stringAt(*dom,
"w-title");
69 model._title =
title !=
nullptr ? *
title : std::string{wizardId};
70 Json
const*
const steps = detail::engine::member(*dom,
"w-steps");
72 return std::unexpected(
73 SchemaError{.path =
"w-steps", .message = R
"(a wizard document needs a "w-steps" array)"});
75 std::size_t index = 0;
76 for (Json
const& entry :
steps->get_array()) {
77 std::string
const path =
"w-steps[" + std::to_string(index++) +
"]";
78 auto const*
const action = detail::engine::stringAt(entry,
"action");
79 if (action ==
nullptr) {
80 return std::unexpected(
SchemaError{.path = path, .message = R
"(a step needs an "action")"});
84 auto const*
const stepTitle = detail::engine::stringAt(entry,
"title");
85 step.
title = stepTitle !=
nullptr ? *stepTitle : step.
action;
86 if (Json
const*
const prefill = detail::engine::member(entry,
"prefill")) {
87 if (!prefill->is_object()) {
88 return std::unexpected(
89 SchemaError{.path = path +
".prefill", .message = R
"("prefill" must be an object)"});
91 for (
auto const& [field, source] : prefill->get_object()) {
92 auto const*
const text = source.get_if<std::string>();
93 if (text ==
nullptr || !text->contains(
'.')) {
94 std::string where = path;
97 return std::unexpected(
SchemaError{.path = std::move(where),
98 .message = R
"(a prefill path is "<Action>.<member>")"});
100 step.prefill.emplace_back(field, *text);
103 model._steps.push_back(std::move(step));
105 if (model._steps.empty()) {
106 return std::unexpected(
SchemaError{.path =
"w-steps", .message =
"a wizard needs at least one step"});
113 [[nodiscard]] std::string_view
id() const noexcept {
return _id; }
117 [[nodiscard]] std::string_view
title() const noexcept {
return _title; }
121 [[nodiscard]] std::span<WizardStepSpec const>
steps() const noexcept {
return _steps; }
128 std::vector<WizardStepSpec> _steps;
154 : _rt{&runtime}, _model{std::move(model)}, _submit{std::move(submit)}, _current{runtime, std::size_t{0}} {
156 throw std::invalid_argument{
"WizardSession: a Submitter is required"};
158 for (std::size_t i = 0; i < _model.
steps().size(); ++i) {
159 WizardStepSpec
const& spec = detail::engine::elementAt(_model.
steps(), i);
160 auto const schema = schemas ? schemas(spec.action) : std::nullopt;
162 throw std::invalid_argument{
"WizardSession: no schema for step '" + spec.action +
"'"};
166 throw std::invalid_argument{
"WizardSession: the schema of step '" + spec.action +
"' does not read: " +
167 stepModel.error().path +
": " + stepModel.error().message};
169 _succeeded.push_back(
170 std::make_unique<reactive::Signal<std::optional<std::string>>>(runtime, std::nullopt));
171 _steps.push_back(std::make_unique<FormSession>(
172 runtime, *std::move(stepModel),
173 [
this, i](std::string_view action, std::string body) {
return relay(i, action, std::move(body)); },
190 [[nodiscard]] std::size_t
current()
const {
return _current.get(); }
194 [[nodiscard]] std::size_t
stepCount() const noexcept {
return _steps.size(); }
200 [[nodiscard]]
FormSession&
step(std::size_t index)
const {
return *_steps.at(index); }
206 [[nodiscard]]
bool stepDone(std::size_t index)
const {
207 std::optional<std::string>
const& succeeded = _succeeded.at(index)->get();
208 return succeeded.has_value() && _steps.at(index)->body() == succeeded;
218 std::size_t
const here =
current();
219 if (here + 1 >= _steps.size()) {
223 return stepDone(here) || (session.ready() && !session.pending());
233 if (!_rt->untracked([&] { return canNext(); })) {
236 std::size_t
const here = _current.peek();
237 if (_rt->untracked([&] { return stepDone(here); })) {
242 _steps.at(here)->submit();
247 _advanceFrom.reset();
248 if (_current.peek() > 0) {
249 _current.set(_current.peek() - 1);
257 [[nodiscard]] std::optional<std::string>
resolved(std::string_view path)
const {
258 auto const found = _resolved.find(path);
259 return found == _resolved.end() ? std::nullopt : std::optional<std::string>{found->second};
264 [[nodiscard]] std::string
title()
const {
274 [[nodiscard]] std::string
stepTitle(std::size_t index)
const {
278 detail::engine::elementAt(_model.steps(), index).title);
282 void enter(std::size_t index) {
283 _advanceFrom.reset();
287 for (
auto const& [field, path] : detail::engine::elementAt(_model.
steps(), index).prefill) {
288 if (
auto const value =
resolved(path)) {
289 session.assign(field, *value);
293 bool const submitNow = _rt->untracked(
300 [[nodiscard]] async::Completion<std::string> relay(std::size_t index, std::string_view action, std::string body) {
302 auto const settle = std::make_shared<async::Completion<std::string>::Promise>(std::move(promise));
303 std::string submitted = body;
304 std::string type{action};
305 _submit(action, std::move(body))
307 [
this, index, settle, submitted = std::move(submitted),
308 type = std::move(type)](std::string
const& reply) {
310 record(type, submitted);
312 _succeeded.at(index)->set(submitted);
314 settle->resolve(reply);
315 if (_advanceFrom == index && _current.peek() == index && index + 1 < _steps.size()) {
319 .onError(_callbacks, [
this, index, settle](std::exception_ptr
const& error) {
320 _succeeded.at(index)->set(std::nullopt);
321 if (_advanceFrom == index) {
322 _advanceFrom.reset();
324 settle->reject(error);
326 return std::move(completion);
330 void record(std::string
const& action, std::string
const& json) {
331 auto const dom = detail::engine::parseJson(json);
332 if (!dom || !dom->is_object()) {
335 for (
auto const& [name, value] : dom->get_object()) {
336 std::string path = action;
339 _resolved.insert_or_assign(std::move(path), detail::engine::toJson(value));
343 reactive::Runtime* _rt;
346 reactive::Signal<std::size_t> _current;
347 std::vector<std::unique_ptr<reactive::Signal<std::optional<std::string>>>> _succeeded;
348 std::map<std::string, std::string, std::less<>> _resolved;
349 std::optional<std::size_t> _advanceFrom;
352 async::CallbackScope _callbacks;
353 std::vector<std::unique_ptr<FormSession>> _steps;
AnyHandle session(std::string const &name)
A session attribute, {"session": name}.
static std::pair< Completion< T >, Promise > makeSettleable(::morph::exec::IExecutor *execPtr)
Constructs a Completion<T>/Promise<T> pair sharing one settleable state.
The owner of a signal graph: every node takes one, and every operation runs on its owner.
decltype(auto) batch(F &&fn)
Runs fn as one batch: every write inside it is one flush.
void set(T value)
Replaces the value and notifies observers, unless it equals the current one and the policy is Equalit...
Collections, headless: a v-* view document as a reactive list of rows, the table source the table eng...
std::function< std::optional< std::string >(std::string_view actionType)> SchemaLookup
Finds an action's schema JSON by its type id (a registry, a map, a served catalog).
FormModel: the immutable runtime model of one action's form, read from the schema JSON schemaJson<A>(...
The renderer-side translation-catalog seam over morph's derived message keys (morph::forms::i18n,...
std::string resolveText(const TranslationProvider &provider, std::string_view bcp47Locale, const std::optional< std::string > &explicitKey, std::string_view derivedKey, std::string_view schemaLiteral)
Resolves one display slot's text: explicit key, then derived key, then the schema literal.
x-rules read into RuleExpr trees, translated into the UI document's expression language (expr/1) and ...
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
Signal<T>, Computed<T> and Effect: the nodes of a reactive graph.
Exact text helpers the forms engine shares: JSON quoting and reading, canonical decimal comparison an...