Module: Plutonium::Wizard::Resume

Defined in:
lib/plutonium/wizard/resume.rb

Overview

Builds the "continue where you left off" listing (§4.5): for every in-progress Session row owned by a user (optionally narrowed to a tenant scope), an enriched Entry carrying the wizard's label/icon, the current step (+ its label), updated_at, and a resolved resume_url.

A host renders this on a dashboard:

Plutonium::Wizard.in_progress_for(view_context)

Resume URLs are built in the CURRENT portal (the one whose view_context is passed), so a run is only ever linked from the portal it belongs to:

  • A register_wizard (portal/public) wizard draws a NAMED route carrying a wizard_class route default; we find it and build the URL from its helper, threading the tenant scope segment and (for tokened runs) the :token.
  • A wizard-macro (resource-mounted) ANCHORED wizard's member URL is built by the same resource_url_for(record, wizard:, step:) machinery the launch button uses — portal- and scope-correct by construction — from the row's anchor + the registering definition's wizard name.

When a row's mount can't be resolved in this portal (e.g. a non-anchored resource-mounted wizard, whose resource identity isn't on the row, or a wizard not mounted here), the entry is returned with resume_url: nil and a resume_unresolved_reason, rather than guessing or raising.

Defined Under Namespace

Classes: Entry, ResumeUrl

Class Method Summary collapse

Class Method Details

.entries_for(view_context, anchor: nil, wizard: nil) ⇒ Array<Entry>

In-progress entries for the run owner and tenant scope derived from the current portal's view_context (the same object interactions take). A run belongs to exactly one portal context, so the scope MATCHES it: a scoped portal narrows to its tenant; a non-scoped portal narrows to runs with no scope (never another portal's entity-scoped runs). Resume URLs are built through that same view_context, so they land in THIS portal. Newest first.

Optional +anchor:+/+wizard:+ filters narrow IN THE QUERY (before enrichment) so discarded rows are never URL-resolved or anchor-loaded — cheaper than filtering the returned array. Both compose; the wizard + anchor pair is index-covered by [:wizard, :anchor_type, :anchor_id, :status].

Parameters:

  • view_context (ActionView::Base)

    the current view context

  • anchor (ActiveRecord::Base, nil) (defaults to: nil)

    narrow to runs anchored against this record

  • wizard (Class, nil) (defaults to: nil)

    narrow to runs of this wizard class

Returns:



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/plutonium/wizard/resume.rb', line 61

def entries_for(view_context, anchor: nil, wizard: nil)
  controller = view_context.controller
  owner = controller.helpers.current_user
  # A guest has no owner-tracked runs — anonymous runs are session-keyed and
  # ownerless (§4.5). The public surface stubs `current_user` to "Guest", so
  # bail rather than query `where(owner: "Guest")` (a non-record). And never
  # normalize "Guest" to nil: `where(owner: nil)` would match EVERY guest's
  # ownerless run — a cross-guest leak.
  return [] unless owner.present? && owner != "Guest"

  # `current_scoped_entity` is a helper_method — read it off the view context.
  scope = controller.scoped_to_entity? ? view_context.current_scoped_entity : nil
  # The portal pins the listing: a run is only shown by the portal it was
  # launched in. `scope` still isolates the tenant WITHIN a scoped portal —
  # `engine` alone can't (one engine serves every tenant via path scoping).
  engine = view_context.current_engine.name

  relation = Session.status_in_progress.where(owner: owner, engine: engine, scope: scope)
  relation = relation.where(anchor: anchor) if anchor
  relation = relation.where(wizard: wizard.name) if wizard

  relation
    .order(updated_at: :desc)
    .filter_map { |row| entry_for(row, view_context) }
end

.entry_for(row, view_context) ⇒ Entry?

Returns nil when the wizard class can't be loaded.

Returns:

  • (Entry, nil)

    nil when the wizard class can't be loaded



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/plutonium/wizard/resume.rb', line 88

def entry_for(row, view_context)
  wizard_class = row.wizard.to_s.safe_constantize
  return nil unless wizard_class

  step = resolve_step(wizard_class, row.current_step)
  resolved = ResumeUrl.new(row, wizard_class, view_context).resolve

  Entry.new(
    wizard_class: wizard_class,
    label: wizard_class.label,
    icon: wizard_class.icon,
    current_step: row.current_step,
    current_step_label: step&.label,
    updated_at: row.updated_at,
    resume_url: resolved[:url],
    resume_unresolved_reason: resolved[:reason],
    session: row
  )
end

.resolve_step(wizard_class, key) ⇒ Object



108
109
110
111
112
# File 'lib/plutonium/wizard/resume.rb', line 108

def resolve_step(wizard_class, key)
  return nil if key.blank?

  wizard_class.steps.find { |s| s.key.to_s == key.to_s }
end