alembic 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/javascripts/alembic/one_page.js +39 -0
- data/app/controllers/alembic/application_controller.rb +1 -0
- data/app/controllers/alembic/flows_controller.rb +31 -0
- data/app/controllers/alembic/manage/base_controller.rb +8 -0
- data/app/controllers/alembic/manage/flows_controller.rb +29 -0
- data/app/controllers/alembic/manage/page_blocks_controller.rb +1 -1
- data/app/controllers/alembic/manage/pages_controller.rb +7 -0
- data/app/controllers/concerns/alembic/summarizes.rb +13 -1
- data/app/helpers/alembic/application_helper.rb +15 -0
- data/app/helpers/alembic/content_blocks_helper.rb +31 -0
- data/app/helpers/alembic/pages_helper.rb +2 -2
- data/app/helpers/alembic/result_blocks_helper.rb +12 -0
- data/app/models/alembic/flow/definition_summary.rb +1 -0
- data/app/models/alembic/flow/output_form.rb +74 -0
- data/app/models/alembic/flow/preview_values.rb +47 -0
- data/app/models/alembic/flow/summaries.rb +31 -0
- data/app/models/alembic/outputs/outcome.rb +33 -0
- data/app/models/alembic/page/content_blocks.rb +20 -0
- data/app/models/alembic/page/preview.rb +15 -0
- data/app/models/alembic/result_blocks.rb +4 -0
- data/app/views/alembic/blocks/_code.html.erb +1 -0
- data/app/views/alembic/blocks/_facts.html.erb +5 -0
- data/app/views/alembic/blocks/_hero.html.erb +9 -0
- data/app/views/alembic/blocks/_outcome_entry.html.erb +44 -0
- data/app/views/alembic/blocks/_outcomes.html.erb +10 -0
- data/app/views/alembic/blocks/_table.html.erb +20 -0
- data/app/views/alembic/flows/_question.html.erb +24 -0
- data/app/views/alembic/flows/one_page.html.erb +35 -0
- data/app/views/alembic/flows/show.html.erb +20 -13
- data/app/views/alembic/flows/step.html.erb +47 -0
- data/app/views/alembic/flows/summary_page.html.erb +5 -1
- data/app/views/alembic/manage/flows/_outputs.html.erb +61 -0
- data/app/views/alembic/manage/flows/edit.html.erb +24 -0
- data/app/views/alembic/manage/pages/_block_content.html.erb +1 -1
- data/app/views/alembic/manage/pages/show.html.erb +10 -1
- data/config/routes.rb +1 -0
- data/db/migrate/20260930230000_add_shows_answer_values_to_alembic_flow_definition_summaries.rb +5 -0
- data/db/migrate/20260930233000_add_asks_on_one_page_to_alembic_flow_definition_summaries.rb +5 -0
- data/db/migrate/20261001120000_add_intro_page_to_alembic_flow_definition_summaries.rb +6 -0
- data/lib/alembic/engine.rb +7 -1
- data/lib/alembic/version.rb +1 -1
- metadata +23 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51cc656bc727f4e074fd445d526d35e81335f5c9194035a06d4c2eae5db8b58d
|
|
4
|
+
data.tar.gz: 4940fe7f9a3f9f1db30fd8bde8581894943bf6aa17f128caef25c86f53b6c053
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0145df853f77e3ca81a6498d82c02034d10c1941ce25589d62f22ff8e6c36be4711874eee41b2885e8e055ae80197b5d8a087da3f4060f3bb38ecfdd237f9d10
|
|
7
|
+
data.tar.gz: 802007ed37aebdff456e3d4e71f54b5e35e1b126704fcbd2c7dc9540657d9fbb1508c2de7d63721816104e3db7b8cc5a0582a7edf813636de2dbea9f274284ac
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const start = (form) => {
|
|
3
|
+
if (form.dataset.onePageStarted) return
|
|
4
|
+
form.dataset.onePageStarted = "true"
|
|
5
|
+
|
|
6
|
+
const questions = Array.from(form.querySelectorAll("[data-one-page-question]"))
|
|
7
|
+
const ready = form.querySelector("[data-one-page-ready]")
|
|
8
|
+
const show = (index) => {
|
|
9
|
+
questions.forEach((question, position) => { question.hidden = position !== index })
|
|
10
|
+
ready.hidden = index < questions.length
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
questions.forEach((question, position) => {
|
|
14
|
+
question.querySelectorAll("input[type=radio]").forEach((answer) => {
|
|
15
|
+
answer.addEventListener("change", () => show(position + 1))
|
|
16
|
+
})
|
|
17
|
+
const previous = question.querySelector("[data-one-page-previous]")
|
|
18
|
+
if (previous) {
|
|
19
|
+
previous.hidden = false
|
|
20
|
+
previous.addEventListener("click", () => show(position - 1))
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const back = ready.querySelector("[data-one-page-previous]")
|
|
25
|
+
back.hidden = false
|
|
26
|
+
back.addEventListener("click", () => show(questions.length - 1))
|
|
27
|
+
|
|
28
|
+
show(0)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const begin = () => document.querySelectorAll("form[data-one-page]").forEach(start)
|
|
32
|
+
|
|
33
|
+
document.addEventListener("turbo:load", begin)
|
|
34
|
+
if (document.readyState === "loading") {
|
|
35
|
+
document.addEventListener("DOMContentLoaded", begin)
|
|
36
|
+
} else {
|
|
37
|
+
begin()
|
|
38
|
+
}
|
|
39
|
+
})()
|
|
@@ -2,9 +2,40 @@ module Alembic
|
|
|
2
2
|
class FlowsController < EasyFlow::FlowsController
|
|
3
3
|
hosted_by FLOW_HOST
|
|
4
4
|
include Summarizes
|
|
5
|
+
helper_method :step_category, :shows_answer_values?
|
|
6
|
+
|
|
7
|
+
def step
|
|
8
|
+
return super unless asked_on_one_page?
|
|
9
|
+
|
|
10
|
+
@guide = runner_for(running_definition)
|
|
11
|
+
@questions = questions_in_order
|
|
12
|
+
render :one_page
|
|
13
|
+
end
|
|
5
14
|
|
|
6
15
|
private
|
|
7
16
|
|
|
17
|
+
def asked_on_one_page?
|
|
18
|
+
run.nil? && params[:answers].blank? && Flow::Summaries.new(flow).asks_on_one_page?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def questions_in_order
|
|
22
|
+
state = {}
|
|
23
|
+
questions = []
|
|
24
|
+
while (question = @guide.next_step(state))
|
|
25
|
+
questions << question
|
|
26
|
+
state[question.id.to_s] = "asked"
|
|
27
|
+
end
|
|
28
|
+
questions
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def shows_answer_values?
|
|
32
|
+
Flow::Summaries.new(run&.flow || flow).shows_answer_values?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def step_category(question)
|
|
36
|
+
EasyFlow::Steps::Question.category_of(@guide.step(question.id)&.config)
|
|
37
|
+
end
|
|
38
|
+
|
|
8
39
|
def run_location(run)
|
|
9
40
|
alembic.run_path(run)
|
|
10
41
|
end
|
|
@@ -13,6 +13,14 @@ module Alembic
|
|
|
13
13
|
helper KsBlocks::ContentHelper
|
|
14
14
|
helper Alembic::PagesHelper
|
|
15
15
|
helper Alembic::ResultBlocksHelper
|
|
16
|
+
helper Alembic::ContentBlocksHelper
|
|
17
|
+
helper_method :preview_values
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def preview_values(page)
|
|
22
|
+
@preview_values ||= Page::Preview.values_for(page, session.dig(:alembic_preview, page.id.to_s))
|
|
23
|
+
end
|
|
16
24
|
end
|
|
17
25
|
end
|
|
18
26
|
end
|
|
@@ -2,11 +2,19 @@ module Alembic
|
|
|
2
2
|
module Manage
|
|
3
3
|
class FlowsController < EasyFlow::Manage::FlowsController
|
|
4
4
|
hosted_by FLOW_HOST
|
|
5
|
+
helper Alembic::ApplicationHelper
|
|
5
6
|
def edit
|
|
6
7
|
super
|
|
7
8
|
summaries = Flow::Summaries.new(@flow)
|
|
8
9
|
@summary = summaries.text
|
|
9
10
|
@summary_page = summaries.summary_page
|
|
11
|
+
@intro_page = summaries.intro_page
|
|
12
|
+
@outputs = Array(summaries.document.to_h["outputs"])
|
|
13
|
+
@uncategorised = scored_by_category?(@outputs) ? questions_of(@flow).reject { |node| EasyFlow::Steps::Question.category_of(node).present? } : []
|
|
14
|
+
@categories = questions_of(@flow).filter_map { |node| EasyFlow::Steps::Question.category_of(node) }.uniq
|
|
15
|
+
@shows_answer_values = summaries.shows_answer_values?
|
|
16
|
+
@asks_on_one_page = summaries.asks_on_one_page?
|
|
17
|
+
@branches = summaries.branches?
|
|
10
18
|
@published_pages = Page.where(id: Page::Version.live.select(:page_id)).order(:name)
|
|
11
19
|
end
|
|
12
20
|
|
|
@@ -14,9 +22,30 @@ module Alembic
|
|
|
14
22
|
details = params.require(:flow)
|
|
15
23
|
summaries = Flow::Summaries.new(flow_host.flows.find(params[:id]))
|
|
16
24
|
summaries.describe(details[:summary]) unless details[:summary].nil?
|
|
25
|
+
summaries.start_on(Page.find_by(id: details[:intro_page_id])) unless details[:intro_page_id].nil?
|
|
17
26
|
summaries.finish_on(Page.find_by(id: details[:summary_page_id])) unless details[:summary_page_id].nil?
|
|
27
|
+
summaries.show_answer_values(details[:shows_answer_values] == "1") unless details[:shows_answer_values].nil?
|
|
28
|
+
summaries.ask_on_one_page(details[:asks_on_one_page] == "1") unless details[:asks_on_one_page].nil?
|
|
29
|
+
record_outputs(summaries, details[:outputs]) if details[:outputs]
|
|
18
30
|
|
|
19
31
|
super
|
|
32
|
+
rescue Summary::UnknownOutputType => refused
|
|
33
|
+
redirect_to easy_flow.edit_manage_flow_path(params[:id]), alert: refused.message
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def scored_by_category?(outputs)
|
|
39
|
+
outputs.any? { |output| %w[grouped lowest].include?(output["type"]) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def questions_of(flow)
|
|
43
|
+
Array(flow.definition.to_h["nodes"]).select { |node| node["type"] == "question" }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def record_outputs(summaries, submitted)
|
|
47
|
+
edited = Flow::OutputForm.edited(Array(summaries.document.to_h["outputs"]), submitted.permit!.to_h)
|
|
48
|
+
summaries.record(summaries.document.to_h.merge("outputs" => edited))
|
|
20
49
|
end
|
|
21
50
|
end
|
|
22
51
|
end
|
|
@@ -12,7 +12,7 @@ module Alembic
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def block_content(block)
|
|
15
|
-
render_to_string(partial: "alembic/manage/pages/block_content", locals: { block: block }, formats: [ :html ])
|
|
15
|
+
render_to_string(partial: "alembic/manage/pages/block_content", locals: { block: block, page: block_layout_record }, formats: [ :html ])
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -27,6 +27,13 @@ module Alembic
|
|
|
27
27
|
redirect_to manage_page_path(page)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def preview
|
|
31
|
+
page = Page.find(params[:id])
|
|
32
|
+
session[:alembic_preview] = session.fetch(:alembic_preview, {}).merge(page.id.to_s => params[:choice].presence)
|
|
33
|
+
|
|
34
|
+
redirect_to manage_page_path(page)
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
private
|
|
31
38
|
|
|
32
39
|
def payload(page)
|
|
@@ -7,8 +7,9 @@ module Alembic
|
|
|
7
7
|
helper ApplicationHelper
|
|
8
8
|
helper PagesHelper
|
|
9
9
|
helper ResultBlocksHelper
|
|
10
|
+
helper ContentBlocksHelper
|
|
10
11
|
helper KsBlocks::LayoutHelper
|
|
11
|
-
helper_method :flow_summary, :first_step_path, :summary_values
|
|
12
|
+
helper_method :flow_summary, :first_step_path, :summary_values, :intro_values
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
private
|
|
@@ -32,6 +33,17 @@ module Alembic
|
|
|
32
33
|
render template: @summary_page&.live_version ? "alembic/flows/summary_page" : "alembic/flows/complete"
|
|
33
34
|
end
|
|
34
35
|
|
|
36
|
+
def intro_values
|
|
37
|
+
starts_a_run = @flow.each_step? && !previewing?
|
|
38
|
+
{ "title" => @flow.title.presence || @flow.slug, "summary" => flow_summary(@flow),
|
|
39
|
+
"question_count" => question_count, "starts_a_run" => starts_a_run,
|
|
40
|
+
"start_path" => starts_a_run ? alembic.flow_runs_path(@flow.slug) : first_step_path(@flow.slug) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def question_count
|
|
44
|
+
Array(@flow.live_definition.to_h["nodes"]).count { |node| node["type"] == "question" }
|
|
45
|
+
end
|
|
46
|
+
|
|
35
47
|
def summary_values
|
|
36
48
|
@outputs.to_h { |output| [ output.id, output.value ] }
|
|
37
49
|
.merge("slug" => @guide.slug, "note" => result_note, "answers" => answers_given)
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
module Alembic
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
def alembic_rules_text(rules)
|
|
4
|
+
Array(rules).map do |rule|
|
|
5
|
+
conditions = Array(rule["when"]).map do |condition|
|
|
6
|
+
condition.key?("is_not") ? "#{condition['step']} is not #{condition['is_not']}" : "#{condition['step']} is #{condition['is']}"
|
|
7
|
+
end
|
|
8
|
+
"#{rule['entry']}: #{conditions.join(', ')}".strip
|
|
9
|
+
end.join("\n")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def alembic_entry_texts(entry)
|
|
13
|
+
{ "facts_text" => Array(entry["facts"]).map { |pair| pair.join(" | ") }.join("\n"),
|
|
14
|
+
"sections_text" => Array(entry["sections"]).map { |section| section.values_at("heading", "text", "note").compact.join(" | ") }.join("\n"),
|
|
15
|
+
"steps_text" => Array(entry["steps"]).map { |step| "#{step['title']}\n#{step['code']}" }.join("\n---\n") }
|
|
16
|
+
end
|
|
17
|
+
|
|
3
18
|
def alembic_output_lines(value)
|
|
4
19
|
case value
|
|
5
20
|
when Hash then value.key?("name") ? [ value["name"] ] : value.map { |name, share| "#{name}: #{share}" }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module ContentBlocksHelper
|
|
3
|
+
def alembic_hero_block(headline: nil, kicker: nil, text: nil)
|
|
4
|
+
render "alembic/blocks/hero", headline: headline, kicker: kicker, text: text
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def alembic_facts_block(question_count: nil)
|
|
8
|
+
facts = yield.to_s.lines.map(&:strip).compact_blank.map do |line|
|
|
9
|
+
line.gsub("{question_count}", question_count.to_s).split("|", 2).map(&:strip)
|
|
10
|
+
end
|
|
11
|
+
render "alembic/blocks/facts", facts: facts
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
START_LOOK = "inline-flex items-center justify-center font-semibold rounded-lg bg-accent-600 text-white hover:bg-accent-500 px-6 py-3 cursor-pointer".freeze
|
|
15
|
+
|
|
16
|
+
def alembic_start_block(start_path:, label: "Start →", starts_a_run: false)
|
|
17
|
+
return button_to(label, start_path, method: :post, class: START_LOOK) if starts_a_run
|
|
18
|
+
|
|
19
|
+
link_to label, start_path, class: START_LOOK
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def alembic_code_block
|
|
23
|
+
render "alembic/blocks/code", code: yield.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def alembic_table_block
|
|
27
|
+
heading, *rows = yield.to_s.lines.map(&:strip).compact_blank.map { |line| line.split("|").map(&:strip) }
|
|
28
|
+
render "alembic/blocks/table", heading: heading.to_a, rows: rows
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -31,10 +31,22 @@ module Alembic
|
|
|
31
31
|
when "band" then alembic_band_block(value: output.value, score: outputs.find { |other| other.type == "percentage" }&.value)
|
|
32
32
|
when "grouped" then alembic_categories_block(value: output.value)
|
|
33
33
|
when "lowest" then alembic_weakest_block(value: output.value)
|
|
34
|
+
when "outcome" then alembic_outcome_block(value: output.value)
|
|
34
35
|
else ui_stat_card(label: output.label, value: alembic_output_lines(output.value).join(" · "))
|
|
35
36
|
end
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
def alembic_outcome_block(value: nil)
|
|
40
|
+
chosen = value.to_h["chosen"]
|
|
41
|
+
return "" if chosen.nil?
|
|
42
|
+
|
|
43
|
+
render "alembic/blocks/outcome_entry", entry: chosen, open: true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def alembic_outcomes_block(value: nil, heading: nil)
|
|
47
|
+
render "alembic/blocks/outcomes", entries: Array(value.to_h["entries"]), heading: heading
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
def alembic_tone(percentage)
|
|
39
51
|
return "bg-red-500" if percentage.to_i < 40
|
|
40
52
|
return "bg-amber-500" if percentage.to_i < 70
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
module OutputForm
|
|
4
|
+
def self.edited(current, submitted)
|
|
5
|
+
edited = submitted.sort_by { |index, _| index.to_i }.map do |index, output|
|
|
6
|
+
current.fetch(index.to_i, {}).merge(output.except("rules_text")).merge(listed_bands(output)).merge(counted(output))
|
|
7
|
+
.merge(ruled(output)).merge(listed_entries(output))
|
|
8
|
+
end
|
|
9
|
+
edited.select { |output| output["id"].present? }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.listed_entries(output)
|
|
13
|
+
return {} unless output["entries"].is_a?(Hash)
|
|
14
|
+
|
|
15
|
+
entries = output["entries"].sort_by { |position, _| position.to_i }.map(&:last).select { |entry| entry["key"].present? }
|
|
16
|
+
{ "entries" => entries.map { |entry| entry_from(entry) } }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.entry_from(entry)
|
|
20
|
+
entry.except("facts_text", "sections_text", "steps_text")
|
|
21
|
+
.merge("facts" => pairs(entry["facts_text"]), "sections" => sections(entry["sections_text"]), "steps" => steps(entry["steps_text"]))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.steps(text)
|
|
25
|
+
text.to_s.split(/^---\s*$/).map(&:strip).compact_blank.map do |block|
|
|
26
|
+
title, code = block.split("\n", 2)
|
|
27
|
+
{ "title" => title.strip, "code" => code.to_s.strip }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.sections(text)
|
|
32
|
+
text.to_s.lines.map(&:strip).compact_blank.map do |line|
|
|
33
|
+
heading, body, note = line.split("|", 3).map(&:strip)
|
|
34
|
+
{ "heading" => heading, "text" => body, "note" => note }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.pairs(text)
|
|
39
|
+
text.to_s.lines.map(&:strip).compact_blank.map { |line| line.split("|", 2).map(&:strip) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.ruled(output)
|
|
43
|
+
return {} unless output.key?("rules_text")
|
|
44
|
+
|
|
45
|
+
rules = output["rules_text"].to_s.lines.map(&:strip).compact_blank.map do |line|
|
|
46
|
+
entry, conditions = line.split(":", 2)
|
|
47
|
+
{ "entry" => entry.strip, "when" => conditions.to_s.split(",").map(&:strip).compact_blank.map { |condition| condition_from(condition) } }
|
|
48
|
+
end
|
|
49
|
+
{ "rules" => rules }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.condition_from(text)
|
|
53
|
+
step, answer = text.split(/\s+is not\s+/, 2)
|
|
54
|
+
return { "step" => step, "is_not" => answer } if answer
|
|
55
|
+
|
|
56
|
+
step, answer = text.split(/\s+is\s+/, 2)
|
|
57
|
+
{ "step" => step, "is" => answer }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.counted(output)
|
|
61
|
+
output.key?("count") ? { "count" => output["count"].presence&.to_i } : {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.listed_bands(output)
|
|
65
|
+
return {} unless output["bands"].is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
named = output["bands"].sort_by { |position, _| position.to_i }.map(&:last).select { |band| band["name"].present? }
|
|
68
|
+
{ "bands" => named.map { |band| band.merge("ceiling" => band["ceiling"].presence&.to_i) } }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private_class_method :counted, :listed_bands, :ruled, :condition_from, :listed_entries, :pairs, :entry_from, :sections, :steps
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
module PreviewValues
|
|
4
|
+
def self.values(page, choice)
|
|
5
|
+
flow = flow_for(page)
|
|
6
|
+
return {} if flow.nil?
|
|
7
|
+
|
|
8
|
+
summaries = Summaries.new(flow)
|
|
9
|
+
state = finished_runs(flow).find { |run| run.id.to_s == choice.to_s }&.recorded || sample_state(flow)
|
|
10
|
+
introduced(flow, summaries).merge(summaries.of(state).to_h { |result| [ result.id, result.value ] })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.finished_runs(flow)
|
|
14
|
+
EasyFlow::Run.where(flow: flow).order(created_at: :desc).limit(20).select { |run| run.next_step(run.recorded).nil? }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.introduced(flow, summaries)
|
|
18
|
+
questions = Array(flow.live_definition.to_h["nodes"]).count { |node| node["type"] == "question" }
|
|
19
|
+
{ "title" => flow.title.presence || flow.slug, "summary" => summaries.text, "question_count" => questions,
|
|
20
|
+
"slug" => flow.slug, "start_path" => "#", "starts_a_run" => false }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.choices(page)
|
|
24
|
+
flow = flow_for(page)
|
|
25
|
+
return [] if flow.nil?
|
|
26
|
+
|
|
27
|
+
finished_runs(flow).map { |run| [ "Run of #{I18n.l(run.created_at, format: :short)}", run.id.to_s ] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.flow_for(page)
|
|
31
|
+
DefinitionSummary.where(summary_page: page).or(DefinitionSummary.where(intro_page: page)).first&.flow
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.sample_state(flow)
|
|
35
|
+
questions = Array(flow.live_definition.to_h["nodes"]).select { |node| node["type"] == "question" }
|
|
36
|
+
questions.each_with_index.filter_map do |question, index|
|
|
37
|
+
answers = EasyFlow::Steps::Question.answers_of(question)
|
|
38
|
+
next if answers.empty?
|
|
39
|
+
|
|
40
|
+
[ question["id"], answers[index % answers.size].then { |answer| answer.is_a?(Hash) ? answer["value"] : answer } ]
|
|
41
|
+
end.to_h
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private_class_method :flow_for, :introduced, :finished_runs
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -14,6 +14,7 @@ module Alembic
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def record(payload)
|
|
17
|
+
Array(payload.to_h["outputs"]).each { |output| Summary.registry.fetch(output["type"]) if output["type"].present? }
|
|
17
18
|
versions.create!(number: next_number, summary: payload)
|
|
18
19
|
.tap { |version| details.update!(summary_cursor: version.number) }
|
|
19
20
|
end
|
|
@@ -38,6 +39,36 @@ module Alembic
|
|
|
38
39
|
details.summary_page
|
|
39
40
|
end
|
|
40
41
|
|
|
42
|
+
def shows_answer_values?
|
|
43
|
+
details.shows_answer_values
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def show_answer_values(shown)
|
|
47
|
+
details.update!(shows_answer_values: shown)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def asks_on_one_page?
|
|
51
|
+
details.asks_on_one_page
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ask_on_one_page(asked)
|
|
55
|
+
details.update!(asks_on_one_page: asked && !branches?)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def branches?
|
|
59
|
+
Array(@flow.definition.to_h["nodes"]).any? do |node|
|
|
60
|
+
EasyFlow.registry.registered?(node["type"]) && EasyFlow.registry.fetch(node["type"]).routes?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def intro_page
|
|
65
|
+
details.intro_page
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def start_on(page)
|
|
69
|
+
details.update!(intro_page: page)
|
|
70
|
+
end
|
|
71
|
+
|
|
41
72
|
def finish_on(page)
|
|
42
73
|
details.update!(summary_page: page)
|
|
43
74
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Outputs
|
|
3
|
+
module Outcome
|
|
4
|
+
def self.output_type
|
|
5
|
+
Summary::OutputType.define(:outcome) do
|
|
6
|
+
label "Outcome"
|
|
7
|
+
compute { |config, run, _so_far| Outcome.placed(config, run.state) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.register(registry = Summary.registry)
|
|
12
|
+
registry.register(output_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.placed(config, state)
|
|
16
|
+
entries = Array(config["entries"])
|
|
17
|
+
rule = Array(config["rules"]).find { |candidate| holds?(candidate, state) }
|
|
18
|
+
|
|
19
|
+
{ "chosen" => entries.find { |entry| entry["key"] == rule&.dig("entry") }, "entries" => entries }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.holds?(rule, state)
|
|
23
|
+
Array(rule["when"]).all? { |condition| met?(condition, state[condition["step"]].to_s) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.met?(condition, answer)
|
|
27
|
+
return answer != condition["is_not"].to_s if condition.key?("is_not")
|
|
28
|
+
|
|
29
|
+
answer == condition["is"].to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
class Page < ApplicationRecord
|
|
3
|
+
module ContentBlocks
|
|
4
|
+
def self.register
|
|
5
|
+
Page.block(:alembic_hero, name: "Hero", width: 12, height: 3, drawn_by: :alembic_hero_block,
|
|
6
|
+
fields: [ { key: :kicker, label: "Small label" }, { key: :headline, label: "Headline" }, { key: :text, label: "Text" } ])
|
|
7
|
+
Page.block(:alembic_facts, name: "Facts", width: 12, height: 1, drawn_by: :alembic_facts_block,
|
|
8
|
+
fields: [ { key: :facts, label: "Facts, one per line" } ], body: :facts,
|
|
9
|
+
options: { question_count: { value: "question_count" } })
|
|
10
|
+
Page.block(:alembic_table, name: "Table", width: 12, height: 4, drawn_by: :alembic_table_block,
|
|
11
|
+
fields: [ { key: :rows, label: "Rows, one per line" } ], body: :rows)
|
|
12
|
+
Page.block(:alembic_code, name: "Code", width: 12, height: 3, drawn_by: :alembic_code_block,
|
|
13
|
+
fields: [ { key: :code, label: "Code" } ], body: :code)
|
|
14
|
+
Page.block(:alembic_start, name: "Start button", width: 4, height: 1, drawn_by: :alembic_start_block,
|
|
15
|
+
fields: [ { key: :label, label: "Label" } ],
|
|
16
|
+
options: { start_path: { value: "start_path" }, starts_a_run: { value: "starts_a_run" } })
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
class Page < ApplicationRecord
|
|
3
|
+
module Preview
|
|
4
|
+
mattr_accessor :provider, :chooser
|
|
5
|
+
|
|
6
|
+
def self.values_for(page, choice)
|
|
7
|
+
provider ? provider.call(page, choice).to_h : {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.choices_for(page)
|
|
11
|
+
chooser ? chooser.call(page).to_a : []
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -16,6 +16,10 @@ module Alembic
|
|
|
16
16
|
fields: [ OUTPUT, HEADING, { key: :cost_label, label: "Cost label" } ], options: { value: { value_of: :output } })
|
|
17
17
|
Page.block(:alembic_answers, name: "Answers given", width: 12, height: 4, drawn_by: :alembic_answers_block,
|
|
18
18
|
fields: [ HEADING ], options: { value: { value: "answers" } })
|
|
19
|
+
Page.block(:alembic_outcome, name: "Chosen outcome", width: 12, height: 4, drawn_by: :alembic_outcome_block,
|
|
20
|
+
fields: [ OUTPUT ], options: { value: { value_of: :output } })
|
|
21
|
+
Page.block(:alembic_outcomes, name: "Every outcome", width: 12, height: 6, drawn_by: :alembic_outcomes_block,
|
|
22
|
+
fields: [ OUTPUT, HEADING ], options: { value: { value_of: :output } })
|
|
19
23
|
register_lead if Alembic.lead_address
|
|
20
24
|
end
|
|
21
25
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<pre class="overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm text-gray-100"><code><%= code %></code></pre>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<div class="flex flex-wrap gap-2 mb-8">
|
|
2
|
+
<% facts.each do |value, label| %>
|
|
3
|
+
<span class="text-sm text-gray-500 bg-gray-50 border border-gray-200 rounded-md px-3 py-1.5"><% if label %><b class="text-gray-900"><%= value %></b> <%= label %><% else %><%= value %><% end %></span>
|
|
4
|
+
<% end %>
|
|
5
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<% if kicker.present? %>
|
|
3
|
+
<p class="text-xs font-semibold uppercase tracking-widest text-accent-600 mb-3"><%= kicker %></p>
|
|
4
|
+
<% end %>
|
|
5
|
+
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-gray-900 mb-3"><%= headline %></h1>
|
|
6
|
+
<% if text.present? %>
|
|
7
|
+
<p class="text-lg text-gray-600 mb-6"><%= text %></p>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<%= ui_panel(padding: :lg) do %>
|
|
2
|
+
<details id="outcome-<%= entry["key"] %>" class="group" <%= "open" if open %>>
|
|
3
|
+
<summary class="flex items-start gap-3 cursor-pointer list-none">
|
|
4
|
+
<% if entry["label"].present? %>
|
|
5
|
+
<span class="text-xs font-semibold uppercase tracking-wider text-accent-600 border border-accent-200 rounded px-2 py-1 shrink-0"><%= entry["label"] %></span>
|
|
6
|
+
<% end %>
|
|
7
|
+
<span class="flex-1">
|
|
8
|
+
<span class="block text-lg font-semibold text-gray-900"><%= entry["name"] %></span>
|
|
9
|
+
<span class="block text-sm text-gray-500"><%= entry["tagline"] %></span>
|
|
10
|
+
</span>
|
|
11
|
+
<span class="text-gray-400 transition group-open:rotate-90">▸</span>
|
|
12
|
+
</summary>
|
|
13
|
+
|
|
14
|
+
<div class="mt-5 space-y-5">
|
|
15
|
+
<% if Array(entry["facts"]).any? %>
|
|
16
|
+
<dl class="grid grid-cols-1 sm:grid-cols-3 gap-px overflow-hidden rounded-lg bg-gray-200">
|
|
17
|
+
<% Array(entry["facts"]).each do |label, value| %>
|
|
18
|
+
<div class="bg-gray-50 p-3"><dt class="text-xs uppercase tracking-wide text-gray-400 mb-1"><%= label %></dt><dd class="text-sm text-gray-900"><%= value %></dd></div>
|
|
19
|
+
<% end %>
|
|
20
|
+
</dl>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<% Array(entry["sections"]).each do |section| %>
|
|
24
|
+
<div>
|
|
25
|
+
<h4 class="text-xs font-semibold uppercase tracking-wider <%= section["note"].present? ? "text-red-600" : "text-accent-600" %> mb-1"><%= section["heading"] %></h4>
|
|
26
|
+
<p class="text-gray-700"><%= section["text"] %></p>
|
|
27
|
+
<% if section["note"].present? %>
|
|
28
|
+
<p class="mt-1 text-sm text-red-700"><span class="font-semibold">Result:</span> <%= section["note"] %></p>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
|
|
33
|
+
<% if Array(entry["steps"]).any? %>
|
|
34
|
+
<div>
|
|
35
|
+
<h4 class="text-xs font-semibold uppercase tracking-wider text-accent-600 mb-2">How to build it</h4>
|
|
36
|
+
<% Array(entry["steps"]).each_with_index do |step, index| %>
|
|
37
|
+
<p class="text-sm font-semibold text-gray-900 mt-3"><%= index + 1 %>. <%= step["title"] %></p>
|
|
38
|
+
<pre class="mt-1 overflow-x-auto rounded-lg bg-gray-900 p-4 text-xs leading-relaxed text-gray-100"><code><%= step["code"] %></code></pre>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
</details>
|
|
44
|
+
<% end %>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<% if heading.present? %>
|
|
3
|
+
<h2 class="text-2xl font-bold tracking-tight text-gray-900 mb-6"><%= heading %></h2>
|
|
4
|
+
<% end %>
|
|
5
|
+
<div class="space-y-3">
|
|
6
|
+
<% entries.each do |entry| %>
|
|
7
|
+
<%= render "alembic/blocks/outcome_entry", entry: entry, open: false %>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<div class="overflow-x-auto">
|
|
2
|
+
<table class="w-full text-sm text-left">
|
|
3
|
+
<thead>
|
|
4
|
+
<tr class="border-b border-gray-200">
|
|
5
|
+
<% heading.each do |cell| %>
|
|
6
|
+
<th class="py-2 pr-4 font-semibold text-gray-900"><%= cell %></th>
|
|
7
|
+
<% end %>
|
|
8
|
+
</tr>
|
|
9
|
+
</thead>
|
|
10
|
+
<tbody>
|
|
11
|
+
<% rows.each do |row| %>
|
|
12
|
+
<tr class="border-b border-gray-100">
|
|
13
|
+
<% row.each do |cell| %>
|
|
14
|
+
<td class="py-2 pr-4 text-gray-600"><%= cell %></td>
|
|
15
|
+
<% end %>
|
|
16
|
+
</tr>
|
|
17
|
+
<% end %>
|
|
18
|
+
</tbody>
|
|
19
|
+
</table>
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<fieldset>
|
|
2
|
+
<% if category.present? %>
|
|
3
|
+
<p class="text-xs font-semibold uppercase tracking-wider text-accent-600 mb-2"><%= category %></p>
|
|
4
|
+
<% end %>
|
|
5
|
+
<legend class="text-xl font-semibold text-gray-900 mb-5 leading-snug"><%= question.text %></legend>
|
|
6
|
+
<div class="space-y-3">
|
|
7
|
+
<% question.choices.each do |choice| %>
|
|
8
|
+
<label class="block cursor-pointer">
|
|
9
|
+
<%= radio_button_tag "answers[#{question.id}]", choice.value, false, class: "peer sr-only", onchange: (local_assigns.fetch(:sends_on_choice, true) ? "this.form.requestSubmit()" : nil) %>
|
|
10
|
+
<span class="flex items-start gap-3 rounded-lg border-2 border-gray-200 bg-white px-4 py-3 transition hover:border-gray-400 peer-checked:border-accent-500 peer-checked:bg-accent-50">
|
|
11
|
+
<% if shows_values %>
|
|
12
|
+
<span class="text-xs font-semibold uppercase tracking-wider text-accent-600 border border-accent-200 rounded px-2 py-0.5 shrink-0 mt-0.5"><%= choice.value %></span>
|
|
13
|
+
<% end %>
|
|
14
|
+
<span class="text-gray-900">
|
|
15
|
+
<%= choice.label.presence || choice.value %>
|
|
16
|
+
<% if choice.hint.present? %>
|
|
17
|
+
<small class="block text-gray-500"><%= choice.hint %></small>
|
|
18
|
+
<% end %>
|
|
19
|
+
</span>
|
|
20
|
+
</span>
|
|
21
|
+
</label>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</fieldset>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<% content_for(:title, @guide.headline) %>
|
|
2
|
+
<% content_for :head do %>
|
|
3
|
+
<%= javascript_include_tag "alembic/one_page", defer: true %>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<section class="py-20 md:py-28">
|
|
7
|
+
<div class="max-w-2xl mx-auto px-6">
|
|
8
|
+
<%= link_to "← Start over", flow_start_path(@guide.slug), class: "text-sm text-accent-600 hover:text-accent-700" %>
|
|
9
|
+
|
|
10
|
+
<%= form_with url: flow_step_path(@guide.slug), method: :get, data: { turbo: false, one_page: true }, class: "mt-6" do %>
|
|
11
|
+
<% @questions.each_with_index do |question, index| %>
|
|
12
|
+
<div data-one-page-question>
|
|
13
|
+
<div class="flex items-center justify-between text-sm text-gray-500 mb-2">
|
|
14
|
+
<span>Question <%= index + 1 %> of <%= @questions.size %></span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="h-1.5 bg-gray-100 rounded-full overflow-hidden mb-8">
|
|
17
|
+
<i class="block h-full bg-accent-600 rounded-full transition-all" style="width:<%= (index * 100.0 / @questions.size).round %>%"></i>
|
|
18
|
+
</div>
|
|
19
|
+
<%= render "alembic/flows/question", question: question, category: step_category(question), shows_values: shows_answer_values?, sends_on_choice: false %>
|
|
20
|
+
<% if index.positive? %>
|
|
21
|
+
<button type="button" data-one-page-previous hidden class="mt-6 cursor-pointer text-sm text-gray-500 underline underline-offset-2 hover:text-gray-900">← Previous</button>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<div data-one-page-ready class="mt-8">
|
|
27
|
+
<p class="text-gray-600 mb-5">That's every question.</p>
|
|
28
|
+
<div class="flex items-center gap-4">
|
|
29
|
+
<button type="submit" class="inline-flex items-center justify-center font-semibold rounded-lg bg-accent-600 text-white hover:bg-accent-500 px-6 py-3 cursor-pointer">See my results</button>
|
|
30
|
+
<button type="button" data-one-page-previous hidden class="cursor-pointer text-sm text-gray-500 underline underline-offset-2 hover:text-gray-900">← Previous</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
<%
|
|
1
|
+
<% intro_page = Alembic::Flow::Summaries.new(@flow).intro_page %>
|
|
2
|
+
<% if intro_page&.live_version %>
|
|
3
|
+
<% content_for(:title, @flow.title.presence || @flow.slug) %>
|
|
4
|
+
<% content_for :head do %>
|
|
5
|
+
<%= stylesheet_link_tag "ks_blocks" %>
|
|
6
|
+
<% end %>
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
8
|
+
<section class="py-20 md:py-28">
|
|
9
|
+
<div class="max-w-2xl mx-auto px-6">
|
|
10
|
+
<%= drawn_page(intro_page, values: intro_values) %>
|
|
11
|
+
</div>
|
|
12
|
+
</section>
|
|
13
|
+
<% else %>
|
|
14
|
+
<% content_for(:title, intro_values["title"]) %>
|
|
5
15
|
|
|
6
|
-
|
|
7
|
-
<div class="
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<% else %>
|
|
13
|
-
<%= ui_button(label: @flow.start_label.presence || "Start", href: first_step_path(@flow.slug)) %>
|
|
14
|
-
<% end %>
|
|
16
|
+
<section class="py-20 md:py-28">
|
|
17
|
+
<div class="max-w-2xl mx-auto px-6">
|
|
18
|
+
<%= alembic_hero_block(headline: intro_values["title"], text: intro_values["summary"]) %>
|
|
19
|
+
<%= alembic_facts_block(question_count: intro_values["question_count"]) { "{question_count} | questions" } %>
|
|
20
|
+
<%= alembic_start_block(label: @flow.start_label.presence || "Start →", start_path: intro_values["start_path"],
|
|
21
|
+
starts_a_run: intro_values["starts_a_run"]) %>
|
|
15
22
|
</div>
|
|
16
|
-
|
|
23
|
+
</section>
|
|
17
24
|
<% end %>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<% content_for(:title, @guide.headline) %>
|
|
2
|
+
|
|
3
|
+
<section class="py-20 md:py-28">
|
|
4
|
+
<div class="max-w-2xl mx-auto px-6">
|
|
5
|
+
<%= link_to "← Start over", flow_start_path(@guide.slug), class: "text-sm text-accent-600 hover:text-accent-700" %>
|
|
6
|
+
|
|
7
|
+
<% answered = @answers.size %>
|
|
8
|
+
<% questions = answered + @guide.questions_left(@answers) %>
|
|
9
|
+
<div class="mt-6 flex items-center justify-between text-sm text-gray-500 mb-2">
|
|
10
|
+
<span>Question <%= answered + 1 %> of <%= questions %></span>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="h-1.5 bg-gray-100 rounded-full overflow-hidden mb-8">
|
|
13
|
+
<i class="block h-full bg-accent-600 rounded-full transition-all" style="width:<%= (answered * 100.0 / [ questions, 1 ].max).round %>%"></i>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<% if flash[:alert] %>
|
|
17
|
+
<div class="mt-6"><%= ui_alert(message: flash[:alert], type: :error) %></div>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
<% if @waiting %>
|
|
21
|
+
<p class="mt-6 text-gray-700">Waiting for <%= @waiting %>.</p>
|
|
22
|
+
<% else %>
|
|
23
|
+
<%= form_with url: step_form[:url], method: step_form[:method], data: { turbo: false }, class: "mt-6" do %>
|
|
24
|
+
<%= hidden_field_tag :asked, @question.id %>
|
|
25
|
+
<% if carries_answers? %>
|
|
26
|
+
<% @answers.each do |key, value| %>
|
|
27
|
+
<%= hidden_field_tag "answers[#{key}]", value %>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<% if @question.respond_to?(:choices) %>
|
|
32
|
+
<%= render "alembic/flows/question", question: @question, category: step_category(@question),
|
|
33
|
+
shows_values: shows_answer_values? %>
|
|
34
|
+
<% else %>
|
|
35
|
+
<%= render @drawing, step: @question %>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
<div class="mt-8 flex items-center gap-4">
|
|
39
|
+
<%= ui_button(label: "Next", type: :submit) %>
|
|
40
|
+
<% if @answers.any? %>
|
|
41
|
+
<button type="submit" name="back" value="1" formnovalidate class="cursor-pointer text-sm text-gray-500 underline underline-offset-2 hover:text-gray-900">← Back</button>
|
|
42
|
+
<% end %>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% end %>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
@@ -2,4 +2,8 @@
|
|
|
2
2
|
<%= stylesheet_link_tag "ks_blocks" %>
|
|
3
3
|
<% end %>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<section class="py-20 md:py-28">
|
|
6
|
+
<div class="max-w-2xl mx-auto px-6">
|
|
7
|
+
<%= drawn_page(@summary_page, values: summary_values) %>
|
|
8
|
+
</div>
|
|
9
|
+
</section>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<%= ui_section(title: "What it works out") do %>
|
|
2
|
+
<% uncategorised.each do |question| %>
|
|
3
|
+
<div class="mb-4" data-problem>
|
|
4
|
+
<%= ui_alert(type: :warning, message: "#{EasyFlow::Steps::Question.asked(question)} has no category, so it counts toward no category's score.") %>
|
|
5
|
+
</div>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% outputs.each_with_index do |output, index| %>
|
|
8
|
+
<% field = "flow[outputs][#{index}]" %>
|
|
9
|
+
<div data-output-editor="<%= output["id"] %>" class="mb-6">
|
|
10
|
+
<%= hidden_field_tag "#{field}[id]", output["id"], id: nil %>
|
|
11
|
+
<%= hidden_field_tag "#{field}[type]", output["type"], id: nil %>
|
|
12
|
+
<p class="mb-1 text-sm ks-tone-muted"><%= output["type"] %></p>
|
|
13
|
+
<%= ui_input(name: "#{field}[label]", value: output["label"]) %>
|
|
14
|
+
<% if output["type"] == "band" %>
|
|
15
|
+
<% (Array(output["bands"]) + [ {} ]).each_with_index do |band, position| %>
|
|
16
|
+
<div class="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-3" data-band>
|
|
17
|
+
<%= ui_input(name: "#{field}[bands][#{position}][name]", value: band["name"], placeholder: "Name") %>
|
|
18
|
+
<%= ui_input(name: "#{field}[bands][#{position}][ceiling]", value: band["ceiling"], type: :number, placeholder: "Up to, below") %>
|
|
19
|
+
<%= ui_input(name: "#{field}[bands][#{position}][description]", value: band["description"], placeholder: "Description") %>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% if output["type"] == "outcome" %>
|
|
24
|
+
<label class="ks-label mb-1 mt-3">Rules, first that holds wins</label>
|
|
25
|
+
<%= ui_textarea(name: "#{field}[rules_text]", value: alembic_rules_text(output["rules"]), rows: 4) %>
|
|
26
|
+
<% (Array(output["entries"]) + [ {} ]).each_with_index do |entry, position| %>
|
|
27
|
+
<% place = "#{field}[entries][#{position}]" %>
|
|
28
|
+
<% texts = alembic_entry_texts(entry) %>
|
|
29
|
+
<div class="mt-4 space-y-2 border-t border-(color:--ks-color-divider) pt-4" data-outcome-entry>
|
|
30
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
31
|
+
<%= ui_input(name: "#{place}[key]", value: entry["key"], placeholder: "Key the rules name") %>
|
|
32
|
+
<%= ui_input(name: "#{place}[label]", value: entry["label"], placeholder: "Label, such as Tier 1") %>
|
|
33
|
+
<%= ui_input(name: "#{place}[name]", value: entry["name"], placeholder: "Name") %>
|
|
34
|
+
<%= ui_input(name: "#{place}[tagline]", value: entry["tagline"], placeholder: "Tagline") %>
|
|
35
|
+
</div>
|
|
36
|
+
<%= ui_textarea(name: "#{place}[facts_text]", value: texts["facts_text"], rows: 3, placeholder: "Facts, one per line: Label | value") %>
|
|
37
|
+
<%= ui_textarea(name: "#{place}[sections_text]", value: texts["sections_text"], rows: 4, placeholder: "Sections, one per line: Heading | text | result note") %>
|
|
38
|
+
<%= ui_textarea(name: "#{place}[steps_text]", value: texts["steps_text"], rows: 6, placeholder: "Steps: a title line, then code, with --- between steps") %>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% if output["type"] == "lowest" %>
|
|
43
|
+
<label class="ks-label mb-1 mt-3">How many to name</label>
|
|
44
|
+
<%= ui_input(name: "#{field}[count]", value: output["count"], type: :number) %>
|
|
45
|
+
<% categories.each do |category| %>
|
|
46
|
+
<div class="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-3" data-category-copy>
|
|
47
|
+
<p class="text-sm"><%= category %></p>
|
|
48
|
+
<%= ui_input(name: "#{field}[copy][#{category}][miss]", value: output.dig("copy", category, "miss"), placeholder: "What it misses") %>
|
|
49
|
+
<%= ui_input(name: "#{field}[copy][#{category}][cost]", value: output.dig("copy", category, "cost"), placeholder: "What it costs") %>
|
|
50
|
+
</div>
|
|
51
|
+
<% end %>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
<% field = "flow[outputs][#{outputs.size}]" %>
|
|
56
|
+
<div data-new-outcome class="mt-6">
|
|
57
|
+
<label class="ks-label mb-1">Add an outcome</label>
|
|
58
|
+
<%= hidden_field_tag "#{field}[type]", "outcome", id: nil %>
|
|
59
|
+
<%= ui_input(name: "#{field}[id]", placeholder: "The new outcome's id") %>
|
|
60
|
+
</div>
|
|
61
|
+
<% end %>
|
|
@@ -17,12 +17,36 @@
|
|
|
17
17
|
<%= ui_input(name: "flow[start_label]", value: @flow.start_label) %>
|
|
18
18
|
<% end %>
|
|
19
19
|
|
|
20
|
+
<%= ui_section(title: "How each question is asked") do %>
|
|
21
|
+
<%= hidden_field_tag "flow[shows_answer_values]", "0", id: nil %>
|
|
22
|
+
<label class="flex items-center gap-2">
|
|
23
|
+
<%= check_box_tag "flow[shows_answer_values]", "1", @shows_answer_values %>
|
|
24
|
+
<span>Show each answer's value beside its label</span>
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
<%= hidden_field_tag "flow[asks_on_one_page]", "0", id: nil %>
|
|
28
|
+
<label class="mt-3 flex items-center gap-2">
|
|
29
|
+
<%= check_box_tag "flow[asks_on_one_page]", "1", @asks_on_one_page, disabled: @branches %>
|
|
30
|
+
<span>Ask every question on one page</span>
|
|
31
|
+
</label>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<%= ui_section(title: "Where this starts") do %>
|
|
35
|
+
<p class="mb-3"><%= link_to "Preview the intro page", alembic.manage_flow_preview_path(@flow), class: "text-sm text-(color:--ks-color-link) hover:text-(color:--ks-color-link-hover) dark:text-(color:--ks-color-link-dark) dark:hover:text-(color:--ks-color-link-hover-dark)" %></p>
|
|
36
|
+
<%= ui_select(name: "flow[intro_page_id]", selected: @intro_page&.id,
|
|
37
|
+
include_blank: "Alembic's default intro page",
|
|
38
|
+
options: @published_pages.map { |page| [ page.name, page.id ] }) %>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
20
41
|
<%= ui_section(title: "Where this finishes") do %>
|
|
42
|
+
<p class="mb-3"><%= link_to "Preview the summary page with sample results", alembic.step_manage_flow_preview_path(@flow, answers: Alembic::Flow::PreviewValues.sample_state(@flow)), class: "text-sm text-(color:--ks-color-link) hover:text-(color:--ks-color-link-hover) dark:text-(color:--ks-color-link-dark) dark:hover:text-(color:--ks-color-link-hover-dark)" %></p>
|
|
21
43
|
<%= ui_select(name: "flow[summary_page_id]", selected: @summary_page&.id,
|
|
22
44
|
include_blank: "Alembic's default summary page",
|
|
23
45
|
options: @published_pages.map { |page| [ page.name, page.id ] }) %>
|
|
24
46
|
<% end %>
|
|
25
47
|
|
|
48
|
+
<%= render "alembic/manage/flows/outputs", outputs: @outputs, categories: @categories, uncategorised: @uncategorised %>
|
|
49
|
+
|
|
26
50
|
<%= ui_section(title: "What this keeps of a run") do %>
|
|
27
51
|
<%= ui_select(name: "flow[persists]", selected: @flow.persists,
|
|
28
52
|
options: [ [ "Nothing", "unsaved" ],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<%= shown_block(block) %>
|
|
1
|
+
<%= shown_block(block, values: preview_values(page)) %>
|
|
@@ -8,10 +8,19 @@
|
|
|
8
8
|
<%= ui_button(label: "Publish", type: :submit, variant: :secondary) %>
|
|
9
9
|
<% end %>
|
|
10
10
|
|
|
11
|
+
<% choices = Alembic::Page::Preview.choices_for(@page) %>
|
|
12
|
+
<% if choices.any? %>
|
|
13
|
+
<%= ui_form(action: alembic.preview_manage_page_path(@page), method: :patch) do %>
|
|
14
|
+
<label class="ks-label mb-1">Preview with</label>
|
|
15
|
+
<%= ui_select(name: "choice", options: choices, selected: session.dig(:alembic_preview, @page.id.to_s), include_blank: "Sample results") %>
|
|
16
|
+
<%= ui_button(label: "Show", type: :submit, variant: :secondary) %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
11
20
|
<%= ui_panel do %>
|
|
12
21
|
<%= react_ui("alembic/page-builder", @payload.merge(token: form_authenticity_token)) %>
|
|
13
22
|
<% end %>
|
|
14
23
|
|
|
15
24
|
<%= block_contents(@page.blocks) do |block| %>
|
|
16
|
-
<%= render "alembic/manage/pages/block_content", block: block %>
|
|
25
|
+
<%= render "alembic/manage/pages/block_content", block: block, page: @page %>
|
|
17
26
|
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -2,6 +2,7 @@ Alembic::Engine.routes.draw do
|
|
|
2
2
|
namespace :manage do
|
|
3
3
|
resources :pages, only: [ :index, :create, :show ] do
|
|
4
4
|
post "publish", on: :member
|
|
5
|
+
patch "preview", on: :member
|
|
5
6
|
get "layout", to: "page_blocks#layout", as: :layout
|
|
6
7
|
post "blocks", to: "page_blocks#add_block", as: :blocks
|
|
7
8
|
patch "blocks", to: "page_blocks#place_blocks"
|
data/lib/alembic/engine.rb
CHANGED
|
@@ -28,11 +28,17 @@ module Alembic
|
|
|
28
28
|
Alembic::Outputs::Lowest.register
|
|
29
29
|
Alembic::Outputs::Tally.register
|
|
30
30
|
Alembic::Outputs::Band.register
|
|
31
|
+
Alembic::Outputs::Outcome.register
|
|
31
32
|
end
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
initializer "alembic.result_blocks" do |app|
|
|
35
|
-
app.config.to_prepare
|
|
36
|
+
app.config.to_prepare do
|
|
37
|
+
Alembic::Page::ContentBlocks.register
|
|
38
|
+
Alembic::ResultBlocks.register
|
|
39
|
+
Alembic::Page::Preview.provider = ->(page, choice) { Alembic::Flow::PreviewValues.values(page, choice) }
|
|
40
|
+
Alembic::Page::Preview.chooser = ->(page) { Alembic::Flow::PreviewValues.choices(page) }
|
|
41
|
+
end
|
|
36
42
|
end
|
|
37
43
|
end
|
|
38
44
|
end
|
data/lib/alembic/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: alembic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -57,20 +57,14 @@ dependencies:
|
|
|
57
57
|
requirements:
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0.
|
|
61
|
-
- - ">="
|
|
62
|
-
- !ruby/object:Gem::Version
|
|
63
|
-
version: 0.4.1
|
|
60
|
+
version: '0.6'
|
|
64
61
|
type: :runtime
|
|
65
62
|
prerelease: false
|
|
66
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
67
64
|
requirements:
|
|
68
65
|
- - "~>"
|
|
69
66
|
- !ruby/object:Gem::Version
|
|
70
|
-
version: '0.
|
|
71
|
-
- - ">="
|
|
72
|
-
- !ruby/object:Gem::Version
|
|
73
|
-
version: 0.4.1
|
|
67
|
+
version: '0.6'
|
|
74
68
|
description: Alembic builds and runs data-driven interactive diagnostics — scored
|
|
75
69
|
assessments and branching guides — that place a visitor on an outcome and capture
|
|
76
70
|
the result. A runtime engine plus a UI builder.
|
|
@@ -85,6 +79,7 @@ files:
|
|
|
85
79
|
- Rakefile
|
|
86
80
|
- app/assets/builds/alembic/page_builder.css
|
|
87
81
|
- app/assets/builds/alembic/page_builder.js
|
|
82
|
+
- app/assets/javascripts/alembic/one_page.js
|
|
88
83
|
- app/assets/stylesheets/alembic/application.css
|
|
89
84
|
- app/controllers/alembic/application_controller.rb
|
|
90
85
|
- app/controllers/alembic/flows_controller.rb
|
|
@@ -97,6 +92,7 @@ files:
|
|
|
97
92
|
- app/controllers/concerns/alembic/manage/authenticates_admin.rb
|
|
98
93
|
- app/controllers/concerns/alembic/summarizes.rb
|
|
99
94
|
- app/helpers/alembic/application_helper.rb
|
|
95
|
+
- app/helpers/alembic/content_blocks_helper.rb
|
|
100
96
|
- app/helpers/alembic/pages_helper.rb
|
|
101
97
|
- app/helpers/alembic/result_blocks_helper.rb
|
|
102
98
|
- app/javascript/alembic/page_builder.jsx
|
|
@@ -105,18 +101,23 @@ files:
|
|
|
105
101
|
- app/mailers/alembic/application_mailer.rb
|
|
106
102
|
- app/models/alembic/application_record.rb
|
|
107
103
|
- app/models/alembic/flow/definition_summary.rb
|
|
104
|
+
- app/models/alembic/flow/output_form.rb
|
|
105
|
+
- app/models/alembic/flow/preview_values.rb
|
|
108
106
|
- app/models/alembic/flow/run_summary.rb
|
|
109
107
|
- app/models/alembic/flow/summaries.rb
|
|
110
108
|
- app/models/alembic/flow/summary_version.rb
|
|
111
109
|
- app/models/alembic/outputs/band.rb
|
|
112
110
|
- app/models/alembic/outputs/grouped.rb
|
|
113
111
|
- app/models/alembic/outputs/lowest.rb
|
|
112
|
+
- app/models/alembic/outputs/outcome.rb
|
|
114
113
|
- app/models/alembic/outputs/percentage.rb
|
|
115
114
|
- app/models/alembic/outputs/tally.rb
|
|
116
115
|
- app/models/alembic/outputs/weighted_sum.rb
|
|
117
116
|
- app/models/alembic/page.rb
|
|
117
|
+
- app/models/alembic/page/content_blocks.rb
|
|
118
118
|
- app/models/alembic/page/drawing.rb
|
|
119
119
|
- app/models/alembic/page/options.rb
|
|
120
|
+
- app/models/alembic/page/preview.rb
|
|
120
121
|
- app/models/alembic/page/version.rb
|
|
121
122
|
- app/models/alembic/result_blocks.rb
|
|
122
123
|
- app/models/alembic/summary.rb
|
|
@@ -128,12 +129,22 @@ files:
|
|
|
128
129
|
- app/views/alembic/blocks/_answers.html.erb
|
|
129
130
|
- app/views/alembic/blocks/_band.html.erb
|
|
130
131
|
- app/views/alembic/blocks/_categories.html.erb
|
|
132
|
+
- app/views/alembic/blocks/_code.html.erb
|
|
133
|
+
- app/views/alembic/blocks/_facts.html.erb
|
|
134
|
+
- app/views/alembic/blocks/_hero.html.erb
|
|
131
135
|
- app/views/alembic/blocks/_lead.html.erb
|
|
136
|
+
- app/views/alembic/blocks/_outcome_entry.html.erb
|
|
137
|
+
- app/views/alembic/blocks/_outcomes.html.erb
|
|
132
138
|
- app/views/alembic/blocks/_score.html.erb
|
|
139
|
+
- app/views/alembic/blocks/_table.html.erb
|
|
133
140
|
- app/views/alembic/blocks/_weakest.html.erb
|
|
141
|
+
- app/views/alembic/flows/_question.html.erb
|
|
134
142
|
- app/views/alembic/flows/complete.html.erb
|
|
143
|
+
- app/views/alembic/flows/one_page.html.erb
|
|
135
144
|
- app/views/alembic/flows/show.html.erb
|
|
145
|
+
- app/views/alembic/flows/step.html.erb
|
|
136
146
|
- app/views/alembic/flows/summary_page.html.erb
|
|
147
|
+
- app/views/alembic/manage/flows/_outputs.html.erb
|
|
137
148
|
- app/views/alembic/manage/flows/edit.html.erb
|
|
138
149
|
- app/views/alembic/manage/pages/_block_content.html.erb
|
|
139
150
|
- app/views/alembic/manage/pages/index.html.erb
|
|
@@ -195,6 +206,9 @@ files:
|
|
|
195
206
|
- db/migrate/20260922130000_create_alembic_page_versions.rb
|
|
196
207
|
- db/migrate/20260924220000_move_flows_onto_easy_flow.rb
|
|
197
208
|
- db/migrate/20260930120000_add_summary_page_to_alembic_flow_definition_summaries.rb
|
|
209
|
+
- db/migrate/20260930230000_add_shows_answer_values_to_alembic_flow_definition_summaries.rb
|
|
210
|
+
- db/migrate/20260930233000_add_asks_on_one_page_to_alembic_flow_definition_summaries.rb
|
|
211
|
+
- db/migrate/20261001120000_add_intro_page_to_alembic_flow_definition_summaries.rb
|
|
198
212
|
- lib/alembic.rb
|
|
199
213
|
- lib/alembic/engine.rb
|
|
200
214
|
- lib/alembic/leftover_stylesheet.rb
|