alembic 0.3.0 → 0.4.0

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/alembic/one_page.js +39 -0
  3. data/app/controllers/alembic/application_controller.rb +1 -0
  4. data/app/controllers/alembic/flows_controller.rb +31 -0
  5. data/app/controllers/alembic/manage/base_controller.rb +8 -0
  6. data/app/controllers/alembic/manage/flows_controller.rb +29 -0
  7. data/app/controllers/alembic/manage/page_blocks_controller.rb +1 -1
  8. data/app/controllers/alembic/manage/pages_controller.rb +7 -0
  9. data/app/controllers/concerns/alembic/summarizes.rb +13 -1
  10. data/app/helpers/alembic/application_helper.rb +15 -0
  11. data/app/helpers/alembic/content_blocks_helper.rb +31 -0
  12. data/app/helpers/alembic/pages_helper.rb +2 -2
  13. data/app/helpers/alembic/result_blocks_helper.rb +12 -0
  14. data/app/models/alembic/flow/definition_summary.rb +1 -0
  15. data/app/models/alembic/flow/output_form.rb +74 -0
  16. data/app/models/alembic/flow/preview_values.rb +47 -0
  17. data/app/models/alembic/flow/summaries.rb +31 -0
  18. data/app/models/alembic/outputs/outcome.rb +33 -0
  19. data/app/models/alembic/page/content_blocks.rb +20 -0
  20. data/app/models/alembic/page/preview.rb +15 -0
  21. data/app/models/alembic/result_blocks.rb +4 -0
  22. data/app/views/alembic/blocks/_code.html.erb +1 -0
  23. data/app/views/alembic/blocks/_facts.html.erb +5 -0
  24. data/app/views/alembic/blocks/_hero.html.erb +9 -0
  25. data/app/views/alembic/blocks/_outcome_entry.html.erb +44 -0
  26. data/app/views/alembic/blocks/_outcomes.html.erb +10 -0
  27. data/app/views/alembic/blocks/_table.html.erb +20 -0
  28. data/app/views/alembic/flows/_question.html.erb +24 -0
  29. data/app/views/alembic/flows/one_page.html.erb +35 -0
  30. data/app/views/alembic/flows/show.html.erb +16 -13
  31. data/app/views/alembic/flows/step.html.erb +47 -0
  32. data/app/views/alembic/manage/flows/_outputs.html.erb +61 -0
  33. data/app/views/alembic/manage/flows/edit.html.erb +24 -0
  34. data/app/views/alembic/manage/pages/_block_content.html.erb +1 -1
  35. data/app/views/alembic/manage/pages/show.html.erb +10 -1
  36. data/config/routes.rb +1 -0
  37. data/db/migrate/20260930230000_add_shows_answer_values_to_alembic_flow_definition_summaries.rb +5 -0
  38. data/db/migrate/20260930233000_add_asks_on_one_page_to_alembic_flow_definition_summaries.rb +5 -0
  39. data/db/migrate/20261001120000_add_intro_page_to_alembic_flow_definition_summaries.rb +6 -0
  40. data/lib/alembic/engine.rb +7 -1
  41. data/lib/alembic/version.rb +1 -1
  42. metadata +23 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e0b25fc3373eb9220014c3c13e37b349b68b928ae417fa2f1b93081304bbb0d
4
- data.tar.gz: 0727dcc579e6a0cc0e38eff459ac31f29854e567a072c1ffb21f0eed687f70e2
3
+ metadata.gz: 199da5d98e867968625a2b5c7052a2b9be9be053dae21f2d9335d0985c821934
4
+ data.tar.gz: d267ef04676182a8973f0d69a50ddb774e26ca8ebe2f252325bb04be8dab62a7
5
5
  SHA512:
6
- metadata.gz: 88f5a0b2349fa2d04fcb5766651d645b031e706146360096e9e215c4bf19b2c5557c3e0e7e294e7700fabe1937da4511f3cbdecfadf4277d30e192d489bfe2f5
7
- data.tar.gz: 7f34afb77cc986dfdbe1df1f02a405d1f3a721117e8285be2854a90e5adfc10eb2eb3c1e9971887f5dafaa21817d6791c4eb35ea30df82d7915c75e3a63b5a6a
6
+ metadata.gz: c7e095677fe4cf0074adb09b2dc098da95b580c1ef5effc567425e712b6f7ccc9fd812fb17067c1b5971d136764e7e9de31580f241d921da76af18f9dd5e62c3
7
+ data.tar.gz: b68e766a8b94d801637d278111ceb983e97a7365b4ee8d66219c1cfce334c453f8b531b6db66d499ac76a3540cd181ee27435ac05b85514da5726c78b3295420
@@ -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
+ })()
@@ -3,6 +3,7 @@ module Alembic
3
3
  layout -> { Alembic.layout }
4
4
  helper KeystoneUiHelper
5
5
  helper ResultBlocksHelper
6
+ helper ContentBlocksHelper
6
7
 
7
8
  rescue_from NotPublished, NotPermitted, Withdrawn, with: :refuse
8
9
 
@@ -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
@@ -25,8 +25,8 @@ module Alembic
25
25
  nil
26
26
  end
27
27
 
28
- def shown_block(block)
29
- drawn_block(block) || undrawn_block(block)
28
+ def shown_block(block, values: {})
29
+ drawn_block(block, values: values) || undrawn_block(block)
30
30
  end
31
31
 
32
32
  private
@@ -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
@@ -5,6 +5,7 @@ module Alembic
5
5
 
6
6
  belongs_to :flow, class_name: "EasyFlow::Definition"
7
7
  belongs_to :summary_page, class_name: "Alembic::Page", optional: true
8
+ belongs_to :intro_page, class_name: "Alembic::Page", optional: true
8
9
  end
9
10
  end
10
11
  end
@@ -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">&#9656;</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,20 @@
1
- <% content_for(:title, @flow.title.presence || @flow.slug) %>
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
- <%= ui_page(max_width: :md) do %>
4
- <%= ui_page_header(title: @flow.title.presence || @flow.slug, subtitle: flow_summary(@flow)) %>
8
+ <%= drawn_page(intro_page, values: intro_values) %>
9
+ <% else %>
10
+ <% content_for(:title, intro_values["title"]) %>
5
11
 
6
- <%= ui_section do %>
7
- <div class="flex flex-wrap items-center gap-3">
8
- <% if @flow.each_step? && @flow.definition.present? && !previewing? %>
9
- <%= ui_form(action: alembic.flow_runs_path(@flow.slug), method: :post) do %>
10
- <%= ui_button(label: @flow.start_label.presence || "Start", type: :submit) %>
11
- <% end %>
12
- <% else %>
13
- <%= ui_button(label: @flow.start_label.presence || "Start", href: first_step_path(@flow.slug)) %>
14
- <% end %>
12
+ <section class="py-20 md:py-28">
13
+ <div class="max-w-2xl mx-auto px-6">
14
+ <%= alembic_hero_block(headline: intro_values["title"], text: intro_values["summary"]) %>
15
+ <%= alembic_facts_block(question_count: intro_values["question_count"]) { "{question_count} | questions" } %>
16
+ <%= alembic_start_block(label: @flow.start_label.presence || "Start →", start_path: intro_values["start_path"],
17
+ starts_a_run: intro_values["starts_a_run"]) %>
15
18
  </div>
16
- <% end %>
19
+ </section>
17
20
  <% 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>
@@ -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"
@@ -0,0 +1,5 @@
1
+ class AddShowsAnswerValuesToAlembicFlowDefinitionSummaries < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :alembic_flow_definition_summaries, :shows_answer_values, :boolean, default: false, null: false
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddAsksOnOnePageToAlembicFlowDefinitionSummaries < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :alembic_flow_definition_summaries, :asks_on_one_page, :boolean, default: false, null: false
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddIntroPageToAlembicFlowDefinitionSummaries < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_reference :alembic_flow_definition_summaries, :intro_page,
4
+ foreign_key: { to_table: :alembic_pages, on_delete: :nullify }
5
+ end
6
+ end
@@ -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 { Alembic::ResultBlocks.register }
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
@@ -1,3 +1,3 @@
1
1
  module Alembic
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
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.3.0
4
+ version: 0.4.0
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.4'
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.4'
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