alembic 0.1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +105 -0
- data/Rakefile +6 -0
- data/app/assets/builds/alembic/canvas.js +1 -0
- data/app/assets/builds/alembic/page_builder.css +1 -0
- data/app/assets/builds/alembic/page_builder.js +5 -0
- data/app/assets/stylesheets/alembic/application.css +15 -0
- data/app/controllers/alembic/application_controller.rb +32 -0
- data/app/controllers/alembic/flows_controller.rb +104 -0
- data/app/controllers/alembic/manage/base_controller.rb +16 -0
- data/app/controllers/alembic/manage/canvas_controller.rb +183 -0
- data/app/controllers/alembic/manage/definitions_controller.rb +15 -0
- data/app/controllers/alembic/manage/flows_controller.rb +55 -0
- data/app/controllers/alembic/manage/page_blocks_controller.rb +19 -0
- data/app/controllers/alembic/manage/pages_controller.rb +38 -0
- data/app/controllers/alembic/manage/previews_controller.rb +38 -0
- data/app/controllers/alembic/manage/versions_controller.rb +22 -0
- data/app/controllers/concerns/alembic/manage/authenticates_admin.rb +19 -0
- data/app/controllers/concerns/alembic/manage/draws_canvas.rb +35 -0
- data/app/helpers/alembic/application_helper.rb +11 -0
- data/app/javascript/alembic/canvas/Canvas.jsx +197 -0
- data/app/javascript/alembic/canvas/Connector.jsx +30 -0
- data/app/javascript/alembic/canvas/ConnectorLayer.jsx +31 -0
- data/app/javascript/alembic/canvas/Control.jsx +42 -0
- data/app/javascript/alembic/canvas/Inspector.jsx +44 -0
- data/app/javascript/alembic/canvas/Panel.jsx +67 -0
- data/app/javascript/alembic/canvas/Placeholder.jsx +24 -0
- data/app/javascript/alembic/canvas/Port.jsx +16 -0
- data/app/javascript/alembic/canvas/Records.jsx +34 -0
- data/app/javascript/alembic/canvas/StepCard.jsx +57 -0
- data/app/javascript/alembic/canvas/Toolbar.jsx +19 -0
- data/app/javascript/alembic/canvas/TypePicker.jsx +21 -0
- data/app/javascript/alembic/canvas/changes.js +5 -0
- data/app/javascript/alembic/canvas/choices.js +4 -0
- data/app/javascript/alembic/canvas/flow.js +7 -0
- data/app/javascript/alembic/canvas/flowSender.js +22 -0
- data/app/javascript/alembic/canvas/ids.js +7 -0
- data/app/javascript/alembic/canvas/rows.js +2 -0
- data/app/javascript/alembic/canvas/styles.js +3 -0
- data/app/javascript/alembic/canvas/useConnectors.js +75 -0
- data/app/javascript/alembic/canvas/useFlow.js +17 -0
- data/app/javascript/alembic/canvas.jsx +13 -0
- data/app/javascript/alembic/page_builder/PageBuilder.jsx +18 -0
- data/app/javascript/alembic/page_builder.jsx +10 -0
- data/app/jobs/alembic/application_job.rb +4 -0
- data/app/mailers/alembic/application_mailer.rb +6 -0
- data/app/models/alembic/application_record.rb +5 -0
- data/app/models/alembic/asked.rb +3 -0
- data/app/models/alembic/choice.rb +7 -0
- data/app/models/alembic/flow/admission.rb +19 -0
- data/app/models/alembic/flow/canvas.rb +167 -0
- data/app/models/alembic/flow/change.rb +18 -0
- data/app/models/alembic/flow/condition.rb +24 -0
- data/app/models/alembic/flow/definition.rb +132 -0
- data/app/models/alembic/flow/digest.rb +116 -0
- data/app/models/alembic/flow/document.rb +131 -0
- data/app/models/alembic/flow/drawing.rb +15 -0
- data/app/models/alembic/flow/edge.rb +9 -0
- data/app/models/alembic/flow/edit_history.rb +75 -0
- data/app/models/alembic/flow/layout.rb +88 -0
- data/app/models/alembic/flow/name.rb +15 -0
- data/app/models/alembic/flow/node.rb +5 -0
- data/app/models/alembic/flow/output.rb +23 -0
- data/app/models/alembic/flow/progress/kept.rb +38 -0
- data/app/models/alembic/flow/progress/loose.rb +43 -0
- data/app/models/alembic/flow/progress.rb +11 -0
- data/app/models/alembic/flow/registry.rb +25 -0
- data/app/models/alembic/flow/run.rb +54 -0
- data/app/models/alembic/flow/runner.rb +65 -0
- data/app/models/alembic/flow/settings.rb +80 -0
- data/app/models/alembic/flow/start.rb +13 -0
- data/app/models/alembic/flow/step.rb +42 -0
- data/app/models/alembic/flow/step_type/declaration.rb +118 -0
- data/app/models/alembic/flow/step_type.rb +69 -0
- data/app/models/alembic/flow/summaries.rb +44 -0
- data/app/models/alembic/flow/summary_version.rb +13 -0
- data/app/models/alembic/flow/switch.rb +17 -0
- data/app/models/alembic/flow/terminal.rb +13 -0
- data/app/models/alembic/flow/validator.rb +171 -0
- data/app/models/alembic/flow/version.rb +26 -0
- data/app/models/alembic/flow/violation.rb +9 -0
- data/app/models/alembic/flow.rb +44 -0
- data/app/models/alembic/outputs/band.rb +22 -0
- data/app/models/alembic/outputs/grouped.rb +26 -0
- data/app/models/alembic/outputs/lowest.rb +20 -0
- data/app/models/alembic/outputs/percentage.rb +31 -0
- data/app/models/alembic/outputs/tally.rb +23 -0
- data/app/models/alembic/outputs/weighted_sum.rb +26 -0
- data/app/models/alembic/page.rb +11 -0
- data/app/models/alembic/runner.rb +13 -0
- data/app/models/alembic/steps/question.rb +52 -0
- data/app/models/alembic/summary/output_type.rb +40 -0
- data/app/models/alembic/summary/registry.rb +25 -0
- data/app/models/alembic/summary/report.rb +31 -0
- data/app/models/alembic/summary/result.rb +5 -0
- data/app/models/alembic/summary/run.rb +13 -0
- data/app/models/alembic/summary.rb +15 -0
- data/app/views/alembic/flow/steps/_choosing.html.erb +9 -0
- data/app/views/alembic/flows/complete.html.erb +34 -0
- data/app/views/alembic/flows/show.html.erb +17 -0
- data/app/views/alembic/flows/step.html.erb +27 -0
- data/app/views/alembic/manage/definitions/edit.html.erb +13 -0
- data/app/views/alembic/manage/flows/edit.html.erb +29 -0
- data/app/views/alembic/manage/flows/index.html.erb +32 -0
- data/app/views/alembic/manage/flows/show.html.erb +26 -0
- data/app/views/alembic/manage/pages/_block_content.html.erb +1 -0
- data/app/views/alembic/manage/pages/index.html.erb +20 -0
- data/app/views/alembic/manage/pages/show.html.erb +11 -0
- data/app/views/alembic/manage/versions/index.html.erb +41 -0
- data/app/views/layouts/alembic/application.html.erb +17 -0
- data/config/routes.rb +42 -0
- data/db/migrate/20260604215031_create_alembic_diagnostics.rb +14 -0
- data/db/migrate/20260605015051_create_alembic_questions.rb +12 -0
- data/db/migrate/20260605021526_create_alembic_options.rb +14 -0
- data/db/migrate/20260605023308_create_alembic_conditions.rb +11 -0
- data/db/migrate/20260605025623_create_alembic_bands.rb +12 -0
- data/db/migrate/20260605032859_create_alembic_condition_options.rb +10 -0
- data/db/migrate/20260605032900_reshape_conditions_for_placement.rb +11 -0
- data/db/migrate/20260605125119_create_alembic_results.rb +14 -0
- data/db/migrate/20260605130338_create_alembic_rules.rb +10 -0
- data/db/migrate/20260605130339_create_alembic_rule_results.rb +10 -0
- data/db/migrate/20260605210714_add_definition_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260606000855_add_guide_copy_to_alembic_diagnostics.rb +9 -0
- data/db/migrate/20260606005430_create_alembic_nodes.rb +22 -0
- data/db/migrate/20260606010312_create_alembic_build_steps.rb +12 -0
- data/db/migrate/20260606011110_create_alembic_warnings.rb +11 -0
- data/db/migrate/20260804180000_create_alembic_domains.rb +14 -0
- data/db/migrate/20260804180100_add_domain_to_alembic_questions.rb +5 -0
- data/db/migrate/20260822120000_create_alembic_definition_versions.rb +14 -0
- data/db/migrate/20260822120100_backfill_alembic_definition_versions.rb +17 -0
- data/db/migrate/20260822130000_remove_definition_from_alembic_diagnostics.rb +28 -0
- data/db/migrate/20260822140000_create_alembic_responses.rb +14 -0
- data/db/migrate/20260824120000_create_alembic_transitions.rb +10 -0
- data/db/migrate/20260824130000_drop_alembic_builder_tables.rb +17 -0
- data/db/migrate/20260824140000_add_definition_cursor_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260824160000_add_summary_definition_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260824170000_drop_alembic_guide_copy_columns.rb +8 -0
- data/db/migrate/20260825120000_create_alembic_summary_versions.rb +14 -0
- data/db/migrate/20260825130000_add_summary_cursor_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825140000_move_summary_definitions_into_versions.rb +26 -0
- data/db/migrate/20260825150000_drop_summary_definition_from_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825160000_add_summary_version_to_alembic_responses.rb +6 -0
- data/db/migrate/20260825170000_rename_question_settings_in_current_definitions.rb +51 -0
- data/db/migrate/20260825180000_add_document_to_alembic_diagnostics.rb +6 -0
- data/db/migrate/20260825190000_seed_documents_from_current_definitions.rb +29 -0
- data/db/migrate/20260825200000_add_published_version_to_alembic_diagnostics.rb +6 -0
- data/db/migrate/20260825210000_publish_current_definitions.rb +29 -0
- data/db/migrate/20260825220000_add_undone_changes_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825223322_add_status_to_alembic_definition_versions.rb +8 -0
- data/db/migrate/20260825224538_remove_published_version_from_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825225015_change_alembic_diagnostic_status_to_availability.rb +16 -0
- data/db/migrate/20260825230000_add_undo_history_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825230500_add_changes_to_alembic_definition_versions.rb +5 -0
- data/db/migrate/20260826230000_rename_alembic_responses_to_flow_runs.rb +5 -0
- data/db/migrate/20260826234500_rename_answers_to_recorded_on_flow_runs.rb +5 -0
- data/db/migrate/20260826235000_rename_definition_versions_to_flow_versions.rb +5 -0
- data/db/migrate/20260826235500_rename_summary_versions_to_flow_summaries.rb +5 -0
- data/db/migrate/20260827000000_rename_diagnostics_to_flows.rb +8 -0
- data/db/migrate/20260831000000_add_persists_to_alembic_flows.rb +5 -0
- data/db/migrate/20260914120000_create_alembic_pages.rb +9 -0
- data/db/migrate/20260915120000_add_blocks_to_alembic_pages.rb +5 -0
- data/lib/alembic/engine.rb +43 -0
- data/lib/alembic/leftover_stylesheet.rb +14 -0
- data/lib/alembic/version.rb +3 -0
- data/lib/alembic.rb +34 -0
- data/lib/tasks/alembic_tasks.rake +4 -0
- metadata +252 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import Connector from "./Connector"
|
|
3
|
+
|
|
4
|
+
const ConnectorLayer = ({ links, extent, dragging, onInsert, onRemove, onDrop }) => (
|
|
5
|
+
<>
|
|
6
|
+
<svg width={extent.width} height={extent.height}
|
|
7
|
+
style={{ position: "absolute", top: 0, left: 0, pointerEvents: "none", zIndex: 1 }}>
|
|
8
|
+
<defs>
|
|
9
|
+
<marker id="alembic-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto">
|
|
10
|
+
<path d="M 0 0 L 10 5 L 0 10 z" className="fill-gray-400 dark:fill-zinc-500" />
|
|
11
|
+
</marker>
|
|
12
|
+
</defs>
|
|
13
|
+
{links.map((link) => (
|
|
14
|
+
<path key={link.id} data-link={link.id} className="stroke-gray-400 dark:stroke-zinc-500" fill="none" strokeWidth="1.5" markerEnd="url(#alembic-arrow)" d={link.path} />
|
|
15
|
+
))}
|
|
16
|
+
</svg>
|
|
17
|
+
|
|
18
|
+
{links.filter((link) => link.label).map((link) => (
|
|
19
|
+
<div key={`${link.id}-label`} data-link-label={link.id} className="bg-surface-50 text-gray-500 dark:bg-surface-950 dark:text-gray-400"
|
|
20
|
+
style={{ position: "absolute", left: link.midX - 10, top: link.midY - 18, zIndex: 3,
|
|
21
|
+
fontSize: 11, padding: "0 3px" }}>{link.label}</div>
|
|
22
|
+
))}
|
|
23
|
+
|
|
24
|
+
{links.filter((link) => !link.placeholder).map((link) => (
|
|
25
|
+
<Connector key={link.id} link={link} dragging={Boolean(dragging)}
|
|
26
|
+
onInsert={() => onInsert(link)} onRemove={() => onRemove(link)} onDrop={() => onDrop(link)} />
|
|
27
|
+
))}
|
|
28
|
+
</>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export default ConnectorLayer
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { toggled } from "./choices"
|
|
3
|
+
import Input from "keystone_ui-react/src/Input.jsx"
|
|
4
|
+
import Select from "keystone_ui-react/src/Select.jsx"
|
|
5
|
+
import Checkbox from "keystone_ui-react/src/Checkbox.jsx"
|
|
6
|
+
|
|
7
|
+
const offered = (choices) =>
|
|
8
|
+
(choices || []).map((choice) => (typeof choice === "object" ? choice : { value: choice, label: choice }))
|
|
9
|
+
|
|
10
|
+
const Control = ({ type, value, choices, onChange, onSettle }) => {
|
|
11
|
+
if (type === "boolean") return <Checkbox checked={Boolean(value)} onChange={(e) => onSettle(e.target.checked)} />
|
|
12
|
+
|
|
13
|
+
if (type === "select" || type === "previous_step" || type === "from_step") {
|
|
14
|
+
return <Select className="mb-3" value={value ?? ""} onChange={(e) => onSettle(e.target.value)}>
|
|
15
|
+
<option value=""></option>
|
|
16
|
+
{offered(choices).map((choice) => <option key={choice.value} value={choice.value}>{choice.label}</option>)}
|
|
17
|
+
</Select>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (type === "multi_select") {
|
|
21
|
+
const chosen = Array.isArray(value) ? value : []
|
|
22
|
+
const toggle = (choice) => onSettle(toggled(chosen, choice))
|
|
23
|
+
return <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
|
|
24
|
+
{(choices || []).map((choice) => (
|
|
25
|
+
<label key={choice} style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
|
|
26
|
+
<Checkbox checked={chosen.includes(choice)} onChange={() => toggle(choice)} />
|
|
27
|
+
<span>{choice}</span>
|
|
28
|
+
</label>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (type === "integer" || type === "float") {
|
|
34
|
+
return <Input className="mb-3" type="number" step={type === "integer" ? "1" : "any"} value={value ?? ""}
|
|
35
|
+
onChange={(e) => onChange(e.target.value)} onBlur={(e) => onSettle(e.target.value)} />
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return <Input className="mb-3" type="text" value={value ?? ""}
|
|
39
|
+
onChange={(e) => onChange(e.target.value)} onBlur={(e) => onSettle(e.target.value)} />
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default Control
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react"
|
|
2
|
+
import Control from "./Control"
|
|
3
|
+
import Records from "./Records"
|
|
4
|
+
import Panel from "keystone_ui-react/src/Panel.jsx"
|
|
5
|
+
import Button from "keystone_ui-react/src/Button.jsx"
|
|
6
|
+
import { Label } from "keystone_ui-react/src/FieldText.jsx"
|
|
7
|
+
|
|
8
|
+
const panel = { width: 280, padding: 20, overflowY: "auto" }
|
|
9
|
+
|
|
10
|
+
const Inspector = ({ node, fields, holds, labels, recordLabels, choices, onSave, onDelete, onClose }) => {
|
|
11
|
+
const [ draft, setDraft ] = useState(node.config)
|
|
12
|
+
useEffect(() => setDraft(node.config), [ node.id, node.config ])
|
|
13
|
+
|
|
14
|
+
const settle = (next) => {
|
|
15
|
+
setDraft(next)
|
|
16
|
+
if (JSON.stringify(next) !== JSON.stringify(node.config)) onSave(next)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Panel as="aside" style={panel} data-inspector>
|
|
21
|
+
<div style={{ display: "flex", alignItems: "start", justifyContent: "space-between", gap: 8 }}>
|
|
22
|
+
<h2 style={{ fontWeight: 600, marginBottom: 2 }}>{node.label}</h2>
|
|
23
|
+
<button title="Close" onClick={onClose} className="text-gray-500 dark:text-gray-400"
|
|
24
|
+
style={{ border: "none", background: "none", cursor: "pointer", fontSize: 18, lineHeight: 1 }}>×</button>
|
|
25
|
+
</div>
|
|
26
|
+
<p className="text-gray-500 dark:text-gray-400" style={{ fontSize: 11, marginBottom: 16 }}>{node.id} · {node.type}</p>
|
|
27
|
+
{Object.entries(fields).map(([ name, type ]) => (
|
|
28
|
+
<Label key={name}>
|
|
29
|
+
<span style={{ display: "block", marginBottom: 3 }}>{labels[name] || name}</span>
|
|
30
|
+
{type === "list"
|
|
31
|
+
? <Records holds={holds[name] || {}} labels={recordLabels[name] || {}} rows={draft[name]}
|
|
32
|
+
onChange={(next) => setDraft({ ...draft, [name]: next })}
|
|
33
|
+
onSettle={(next) => settle({ ...draft, [name]: next })} />
|
|
34
|
+
: <Control type={type} value={draft[name]} choices={choices[name]}
|
|
35
|
+
onChange={(next) => setDraft({ ...draft, [name]: next })}
|
|
36
|
+
onSettle={(next) => settle({ ...draft, [name]: next })} />}
|
|
37
|
+
</Label>
|
|
38
|
+
))}
|
|
39
|
+
<Button variant="danger" size="sm" className="w-full" onClick={onDelete}>Delete step</Button>
|
|
40
|
+
</Panel>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default Inspector
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import KeystonePanel from "keystone_ui-react/src/Panel.jsx"
|
|
3
|
+
import Button from "keystone_ui-react/src/Button.jsx"
|
|
4
|
+
import Input from "keystone_ui-react/src/Input.jsx"
|
|
5
|
+
import Textarea from "keystone_ui-react/src/Textarea.jsx"
|
|
6
|
+
import { Label } from "keystone_ui-react/src/FieldText.jsx"
|
|
7
|
+
import { worded } from "./changes"
|
|
8
|
+
import { standing } from "./flow"
|
|
9
|
+
|
|
10
|
+
const sheet = { width: 280, padding: 20, overflowY: "auto" }
|
|
11
|
+
const heading = { fontWeight: 600, marginBottom: 6, marginTop: 14 }
|
|
12
|
+
const item = { fontSize: 12, marginBottom: 4, lineHeight: 1.4 }
|
|
13
|
+
const caption = { display: "block", marginBottom: 3 }
|
|
14
|
+
|
|
15
|
+
const settling = (flow, name, onSaveDetails) => (event) => {
|
|
16
|
+
if (event.target.value !== (flow[name] ?? "")) onSaveDetails({ [name]: event.target.value })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Panel = ({ flow, changes, problems, refusal, notice, onCreate, onPublish, onSaveDetails, onClose }) => (
|
|
20
|
+
<KeystonePanel as="aside" style={sheet} data-builder-panel>
|
|
21
|
+
<div style={{ display: "flex", alignItems: "start", justifyContent: "space-between", gap: 8 }}>
|
|
22
|
+
<h2 style={{ fontWeight: 600 }} data-flow-name>{flow.title || flow.slug}</h2>
|
|
23
|
+
<button title="Close" onClick={onClose} className="text-gray-500 dark:text-gray-400"
|
|
24
|
+
style={{ border: "none", background: "none", cursor: "pointer", fontSize: 18, lineHeight: 1 }}>×</button>
|
|
25
|
+
</div>
|
|
26
|
+
<a className="text-accent-600 dark:text-accent-400" style={{ fontSize: 12, display: "block" }} href={flow.history_url} data-history>{standing(flow)}</a>
|
|
27
|
+
|
|
28
|
+
<h2 style={heading}>Problems</h2>
|
|
29
|
+
{refusal && <p className="text-red-800 dark:text-red-400" style={{ ...item, fontWeight: 600 }} data-refusal>{refusal}</p>}
|
|
30
|
+
{notice && !refusal && <p className="text-emerald-800 dark:text-emerald-400" style={{ ...item, fontWeight: 600 }} data-notice>{notice}</p>}
|
|
31
|
+
{problems.length === 0 && !refusal
|
|
32
|
+
? <p className="text-gray-500 dark:text-gray-400" style={{ fontSize: 12 }}>Nothing wrong with this flow.</p>
|
|
33
|
+
: problems.map((problem) => (
|
|
34
|
+
<p key={`${problem.node}-${problem.problem}`} className="text-amber-700 dark:text-amber-400" style={item} data-problem>⚠ {worded(problem)}</p>
|
|
35
|
+
))}
|
|
36
|
+
|
|
37
|
+
<h2 style={heading}>Changes since the last version</h2>
|
|
38
|
+
{changes.length === 0
|
|
39
|
+
? <p className="text-gray-500 dark:text-gray-400" style={{ fontSize: 12 }}>Nothing has changed.</p>
|
|
40
|
+
: changes.map((change, at) => <p key={at} style={item} data-change>{change}</p>)}
|
|
41
|
+
|
|
42
|
+
<h2 style={heading}>Details</h2>
|
|
43
|
+
<Label>
|
|
44
|
+
<span style={caption}>Title</span>
|
|
45
|
+
<Input className="mb-3" defaultValue={flow.title ?? ""} onBlur={settling(flow, "title", onSaveDetails)} data-flow-title />
|
|
46
|
+
</Label>
|
|
47
|
+
|
|
48
|
+
<Label>
|
|
49
|
+
<span style={caption}>Summary</span>
|
|
50
|
+
<Textarea className="mb-3" rows={4} defaultValue={flow.summary ?? ""} onBlur={settling(flow, "summary", onSaveDetails)} data-flow-summary />
|
|
51
|
+
</Label>
|
|
52
|
+
|
|
53
|
+
<Label>
|
|
54
|
+
<span style={caption}>Start label</span>
|
|
55
|
+
<Input className="mb-3" defaultValue={flow.start_label ?? ""} onBlur={settling(flow, "start_label", onSaveDetails)} data-flow-start-label />
|
|
56
|
+
</Label>
|
|
57
|
+
|
|
58
|
+
<div style={{ marginTop: 16 }}>
|
|
59
|
+
<Button variant="secondary" size="sm" className="w-full mb-1.5" onClick={onCreate} data-create-version>Create version</Button>
|
|
60
|
+
<Button size="sm" className="w-full mb-1.5" onClick={onPublish} data-publish>Publish</Button>
|
|
61
|
+
<Button variant="secondary" size="sm" className="w-full mb-1.5" href={flow.definition_url} data-definition>Definition</Button>
|
|
62
|
+
<Button variant="secondary" size="sm" className="w-full mb-1.5" href={flow.details_url} data-details>Edit details</Button>
|
|
63
|
+
</div>
|
|
64
|
+
</KeystonePanel>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
export default Panel
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { CARD } from "./styles"
|
|
3
|
+
|
|
4
|
+
const waiting = {
|
|
5
|
+
width: CARD, boxSizing: "border-box", padding: "10px 14px", borderRadius: 8, textAlign: "center", fontSize: 12, cursor: "pointer"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const open = "border-2 border-dashed border-amber-500 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300"
|
|
9
|
+
const receiving = "border-2 border-dashed border-accent-600 bg-accent-50 text-accent-800 dark:bg-accent-950 dark:text-accent-200"
|
|
10
|
+
|
|
11
|
+
const Placeholder = ({ node, dragging, onFill, onDrop }) => (
|
|
12
|
+
<div ref={node.ref} data-placeholder={node.id}
|
|
13
|
+
title={`Choose the step “${node.label}” should lead to`}
|
|
14
|
+
onClick={onFill}
|
|
15
|
+
onDragOver={(event) => { if (dragging) { event.preventDefault(); event.dataTransfer.dropEffect = "move" } }}
|
|
16
|
+
onDrop={(event) => { event.preventDefault(); onDrop() }}
|
|
17
|
+
className={dragging ? receiving : open}
|
|
18
|
+
style={waiting}>
|
|
19
|
+
<div style={{ fontWeight: 600 }}>{node.label}</div>
|
|
20
|
+
<div style={{ fontSize: 11 }}>{dragging ? "drop a step here" : "leads nowhere yet — click to choose"}</div>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
export default Placeholder
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
const open = "border border-amber-500 bg-white text-amber-700 dark:bg-zinc-900 dark:text-amber-400"
|
|
4
|
+
const joined = "border border-gray-300 bg-white text-gray-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-gray-400"
|
|
5
|
+
const choosing = "border border-accent-600 bg-accent-600 text-white"
|
|
6
|
+
|
|
7
|
+
const Port = ({ name, connected, armed, onArm, connecting }) => (
|
|
8
|
+
<button onClick={(event) => { if (connecting) return; event.stopPropagation(); onArm(event) }}
|
|
9
|
+
title={armed ? "Now choose a step to connect to" : "Connect this branch"}
|
|
10
|
+
className={armed ? choosing : connected ? joined : open}
|
|
11
|
+
style={{
|
|
12
|
+
padding: "1px 8px", marginRight: 4, borderRadius: 999, fontSize: 11, cursor: "pointer"
|
|
13
|
+
}}>{name || "next"}</button>
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
export default Port
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import Control from "./Control"
|
|
3
|
+
import { amended } from "./rows"
|
|
4
|
+
import Button from "keystone_ui-react/src/Button.jsx"
|
|
5
|
+
|
|
6
|
+
const Records = ({ holds, labels, rows, onChange, onSettle }) => {
|
|
7
|
+
const kept = Array.isArray(rows) ? rows : []
|
|
8
|
+
const amend = (index, name, next, settle) => {
|
|
9
|
+
const updated = amended(kept, index, name, next)
|
|
10
|
+
settle ? onSettle(updated) : onChange(updated)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div style={{ marginBottom: 12 }}>
|
|
15
|
+
{kept.map((row, index) => (
|
|
16
|
+
<div key={index} className="rounded-md border border-gray-200 dark:border-zinc-700" style={{ padding: "8px 8px 2px", marginBottom: 6 }}>
|
|
17
|
+
{Object.entries(holds).map(([ name, type ]) => (
|
|
18
|
+
<label key={name} style={{ display: "block" }}>
|
|
19
|
+
<span className="text-gray-500 dark:text-gray-400" style={{ display: "block", marginBottom: 2, fontSize: 11 }}>{(labels || {})[name] || name}</span>
|
|
20
|
+
<Control type={type} value={row[name]}
|
|
21
|
+
onChange={(next) => amend(index, name, next, false)}
|
|
22
|
+
onSettle={(next) => amend(index, name, next, true)} />
|
|
23
|
+
</label>
|
|
24
|
+
))}
|
|
25
|
+
<Button variant="secondary" size="sm" className="mb-1.5"
|
|
26
|
+
onClick={() => onSettle(kept.filter((_, at) => at !== index))}>Remove</Button>
|
|
27
|
+
</div>
|
|
28
|
+
))}
|
|
29
|
+
<Button variant="secondary" size="sm" className="w-full" onClick={() => onSettle([ ...kept, {} ])}>Add</Button>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default Records
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import Port from "./Port"
|
|
3
|
+
import { CARD } from "./styles"
|
|
4
|
+
|
|
5
|
+
const card = { width: CARD, boxSizing: "border-box", borderRadius: 8 }
|
|
6
|
+
|
|
7
|
+
const bookendCard = { padding: "8px 14px", textAlign: "center", fontWeight: 600 }
|
|
8
|
+
|
|
9
|
+
const named = { fontWeight: 600, lineHeight: 1.3 }
|
|
10
|
+
|
|
11
|
+
const edge = (target, troubled, selected) => {
|
|
12
|
+
const lift = selected ? "ring-4 ring-accent-600/15" : "shadow-sm"
|
|
13
|
+
if (target || (selected && !troubled)) return `border-accent-600 ${lift}`
|
|
14
|
+
if (troubled) return `border-red-600 ${lift}`
|
|
15
|
+
return "border-gray-300 shadow-sm dark:border-zinc-700"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const StepCard = ({ node, selected, armed, connecting, onSelect, onArm, onDragEnd, onDragStart }) => {
|
|
19
|
+
const bookend = node.begins_here || node.ends_here
|
|
20
|
+
const pinned = node.begins_here
|
|
21
|
+
const ports = node.ends_here ? [] : node.ports
|
|
22
|
+
const target = connecting && !pinned
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div ref={node.ref}
|
|
26
|
+
data-step={node.id}
|
|
27
|
+
draggable={!pinned}
|
|
28
|
+
onDragStart={(event) => { event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", node.id); onDragStart() }}
|
|
29
|
+
onDragEnd={onDragEnd}
|
|
30
|
+
onClick={onSelect}
|
|
31
|
+
className={`bg-white border-2 dark:bg-zinc-900 ${edge(target, node.violations.length > 0, selected)}`}
|
|
32
|
+
style={{
|
|
33
|
+
...card,
|
|
34
|
+
padding: bookend ? 0 : "12px 14px",
|
|
35
|
+
cursor: pinned ? "default" : connecting ? "crosshair" : "grab"
|
|
36
|
+
}}>
|
|
37
|
+
{bookend && <div className="text-gray-500 dark:text-gray-400" style={bookendCard}>{node.label}</div>}
|
|
38
|
+
{!bookend && <div style={named}>{node.label}</div>}
|
|
39
|
+
{!bookend && <div className="text-gray-500 dark:text-gray-400" style={{ fontSize: 11, marginTop: 2 }}>{node.type}</div>}
|
|
40
|
+
{node.violations.map((violation) => (
|
|
41
|
+
<div key={violation.problem + violation.detail} className="text-red-600 dark:text-red-400" style={{ fontSize: 11, padding: "0 14px 6px" }}>
|
|
42
|
+
{violation.problem.replace(/_/g, " ")}{violation.detail ? `: ${violation.detail}` : ""}
|
|
43
|
+
</div>
|
|
44
|
+
))}
|
|
45
|
+
{ports.length > 0 && (
|
|
46
|
+
<div style={{ padding: node.begins_here ? "0 14px 8px" : "10px 0 0" }}>
|
|
47
|
+
{ports.map((port) => (
|
|
48
|
+
<Port key={port} name={port} connected={node.connected.includes(port)}
|
|
49
|
+
connecting={connecting} armed={armed === port} onArm={(event) => onArm(port, event)} />
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default StepCard
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import Button from "keystone_ui-react/src/Button.jsx"
|
|
3
|
+
|
|
4
|
+
const round = { width: 24, height: 24, padding: 0, fontSize: 15, lineHeight: "15px" }
|
|
5
|
+
|
|
6
|
+
const Stepper = ({ label, title, idle, enabled, onUse }) => (
|
|
7
|
+
<Button variant="secondary" size="sm" title={enabled ? title : idle} disabled={!enabled} onClick={onUse}
|
|
8
|
+
style={{ opacity: enabled ? 1 : 0.4, cursor: enabled ? "pointer" : "default" }}>{label}</Button>
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const Toolbar = ({ undoable, redoable, onAdd, onUndo, onRedo }) => (
|
|
12
|
+
<div style={{ position: "absolute", top: 16, left: 16, zIndex: 5, display: "flex", gap: 8 }}>
|
|
13
|
+
<Button variant="secondary" className="rounded-full" style={round} title="Add a step" onClick={onAdd}>+</Button>
|
|
14
|
+
<Stepper label="↶ Undo" title="Undo the last change" idle="Nothing to undo" enabled={undoable} onUse={onUndo} />
|
|
15
|
+
<Stepper label="↷ Redo" title="Redo the change you undid" idle="Nothing to redo" enabled={redoable} onUse={onRedo} />
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export default Toolbar
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import Panel from "keystone_ui-react/src/Panel.jsx"
|
|
3
|
+
import Button from "keystone_ui-react/src/Button.jsx"
|
|
4
|
+
|
|
5
|
+
const TypePicker = ({ entries, at, onPick, onConnect, onDismiss }) => (
|
|
6
|
+
<Panel className="shadow-lg" style={{ position: "absolute", zIndex: 9, top: at.y, left: at.x, width: 210, padding: 8, borderRadius: 8 }}>
|
|
7
|
+
<p className="text-gray-500 dark:text-gray-400" style={{ margin: "0 0 6px", fontSize: 11 }}>Add a step</p>
|
|
8
|
+
{entries.map((entry) => (
|
|
9
|
+
<Button key={entry.type} variant="secondary" size="sm" className="w-full mb-1.5 flex-col" onClick={() => onPick(entry)}>
|
|
10
|
+
{entry.label}
|
|
11
|
+
<span className="opacity-75" style={{ display: "block", fontSize: 11 }}>{entry.type}</span>
|
|
12
|
+
</Button>
|
|
13
|
+
))}
|
|
14
|
+
{onConnect && (
|
|
15
|
+
<Button variant="secondary" size="sm" className="w-full mb-1.5" onClick={onConnect}>Connect to a step already here</Button>
|
|
16
|
+
)}
|
|
17
|
+
<Button variant="secondary" size="sm" className="w-full" onClick={onDismiss}>Cancel</Button>
|
|
18
|
+
</Panel>
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export default TypePicker
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const standing = ({ version, published }) => {
|
|
2
|
+
if (!version) return "No version yet"
|
|
3
|
+
if (!published) return `Version ${version} · never published`
|
|
4
|
+
if (published === version) return `Version ${version} · live`
|
|
5
|
+
|
|
6
|
+
return `Version ${version} · visitors still on ${published}`
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const UNREACHABLE = "Your change was not saved because the server could not be reached."
|
|
2
|
+
|
|
3
|
+
export const createFlowSender = ({ base, token, fetch, onFlow, onError, onNotice }) => async (path, method, body) => {
|
|
4
|
+
const response = await fetch(base + path, {
|
|
5
|
+
method, headers: { "Content-Type": "application/json", "X-CSRF-Token": token }, body: body && JSON.stringify(body)
|
|
6
|
+
}).catch(() => null)
|
|
7
|
+
if (!response) {
|
|
8
|
+
onNotice(null)
|
|
9
|
+
return onError(UNREACHABLE)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const answered = await response.json().catch(() => ({}))
|
|
13
|
+
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
onNotice(null)
|
|
16
|
+
return onError(answered.error || "That change was refused")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
onError(null)
|
|
20
|
+
onNotice(answered.notice || null)
|
|
21
|
+
onFlow(await (await fetch(base + ".json", { headers: { Accept: "application/json" } })).json())
|
|
22
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
export const anchor = (box, side, frame, surface) => {
|
|
4
|
+
const left = box.left - frame.left + surface.scrollLeft
|
|
5
|
+
const top = box.top - frame.top + surface.scrollTop
|
|
6
|
+
|
|
7
|
+
if (side === "left") return { x: left, y: top + box.height / 2 }
|
|
8
|
+
if (side === "right") return { x: left + box.width, y: top + box.height / 2 }
|
|
9
|
+
if (side === "top") return { x: left + box.width / 2, y: top }
|
|
10
|
+
return { x: left + box.width / 2, y: top + box.height }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const shy = (point, side) => {
|
|
14
|
+
if (side === "top") return { x: point.x, y: point.y - 3 }
|
|
15
|
+
if (side === "left") return { x: point.x - 3, y: point.y }
|
|
16
|
+
if (side === "right") return { x: point.x + 3, y: point.y }
|
|
17
|
+
return { x: point.x, y: point.y + 3 }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const drawn = (edge, a, b, lane, detour) => {
|
|
21
|
+
const path = {
|
|
22
|
+
straight: `M ${a.x} ${a.y} L ${b.x} ${b.y}`,
|
|
23
|
+
turn: `M ${a.x} ${a.y} H ${b.x} V ${b.y}`,
|
|
24
|
+
lane: `M ${a.x} ${a.y} V ${lane} H ${b.x} V ${b.y}`,
|
|
25
|
+
detour: `M ${a.x} ${a.y} H ${detour} V ${b.y} H ${b.x}`
|
|
26
|
+
}[edge.route]
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...edge, path,
|
|
30
|
+
midX: edge.route === "detour" ? detour : (a.x + b.x) / 2,
|
|
31
|
+
midY: edge.route === "lane" ? lane : (a.y + b.y) / 2
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const useConnectors = (flow, surface, cards, watching) => {
|
|
36
|
+
const [ links, setLinks ] = useState([])
|
|
37
|
+
const [ extent, setExtent ] = useState({ width: "100%", height: "100%" })
|
|
38
|
+
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const measure = () => {
|
|
41
|
+
const frame = surface.current?.getBoundingClientRect()
|
|
42
|
+
if (!frame) return
|
|
43
|
+
|
|
44
|
+
setExtent({ width: surface.current.scrollWidth, height: surface.current.scrollHeight })
|
|
45
|
+
|
|
46
|
+
setLinks(flow.edges.flatMap((edge) => {
|
|
47
|
+
const fromBox = cards.current[edge.source]?.getBoundingClientRect()
|
|
48
|
+
const toBox = cards.current[edge.target]?.getBoundingClientRect()
|
|
49
|
+
if (!fromBox || !toBox) return []
|
|
50
|
+
|
|
51
|
+
const a = anchor(fromBox, edge.leaves, frame, surface.current)
|
|
52
|
+
const b = shy(anchor(toBox, edge.enters, frame, surface.current), edge.enters)
|
|
53
|
+
const lane = (anchor(fromBox, "bottom", frame, surface.current).y + anchor(toBox, "top", frame, surface.current).y) / 2
|
|
54
|
+
const detour = edge.leaves === "left" ? Math.min(a.x, b.x) - 28 : Math.max(a.x, b.x) + 28
|
|
55
|
+
|
|
56
|
+
return [ drawn(edge, a, b, lane, detour) ]
|
|
57
|
+
}))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
measure()
|
|
61
|
+
window.addEventListener("resize", measure)
|
|
62
|
+
|
|
63
|
+
const watcher = new ResizeObserver(measure)
|
|
64
|
+
if (surface.current) watcher.observe(surface.current)
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
window.removeEventListener("resize", measure)
|
|
68
|
+
watcher.disconnect()
|
|
69
|
+
}
|
|
70
|
+
}, watching)
|
|
71
|
+
|
|
72
|
+
return { links, extent }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default useConnectors
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useMemo, useState } from "react"
|
|
2
|
+
import { createFlowSender } from "./flowSender"
|
|
3
|
+
|
|
4
|
+
const useFlow = (base, token, initial) => {
|
|
5
|
+
const [ flow, setFlow ] = useState(initial)
|
|
6
|
+
const [ error, setError ] = useState(null)
|
|
7
|
+
const [ notice, setNotice ] = useState(null)
|
|
8
|
+
|
|
9
|
+
const send = useMemo(
|
|
10
|
+
() => createFlowSender({ base, token, fetch: (...request) => window.fetch(...request), onFlow: setFlow, onError: setError, onNotice: setNotice }),
|
|
11
|
+
[ base, token ]
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
return { flow, error, notice, send }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default useFlow
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createRoot } from "react-dom/client"
|
|
2
|
+
import { register } from "keystone_ui-react/src/registry.js"
|
|
3
|
+
import { startMounting } from "keystone_ui-react/src/mounting.js"
|
|
4
|
+
import Canvas from "./canvas/Canvas"
|
|
5
|
+
|
|
6
|
+
register("alembic/flow-editor", Canvas)
|
|
7
|
+
|
|
8
|
+
document.addEventListener("alembic:flow-named", (event) => {
|
|
9
|
+
const heading = document.querySelector("[data-flow-heading]")
|
|
10
|
+
if (heading) heading.textContent = event.detail
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
startMounting(document, createRoot)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import Button from "keystone_ui-react/src/Button.jsx"
|
|
3
|
+
import Page from "keystone_ui-react/src/Page.jsx"
|
|
4
|
+
import PageHeader from "keystone_ui-react/src/PageHeader.jsx"
|
|
5
|
+
import BlockGrid from "keystone_ui-blocks/src/BlockGrid.jsx"
|
|
6
|
+
|
|
7
|
+
export default function PageBuilder({ base, token, name, pages, block_types, blocks, version }) {
|
|
8
|
+
return (
|
|
9
|
+
<Page>
|
|
10
|
+
<div className="sm:hidden mb-4 flex items-center justify-between gap-3">
|
|
11
|
+
<h1 className="ks-page-header-title">{name}</h1>
|
|
12
|
+
<Button href={pages} size="sm">All pages</Button>
|
|
13
|
+
</div>
|
|
14
|
+
<PageHeader title={name} actions={<Button href={pages} data-all-pages>All pages</Button>} />
|
|
15
|
+
<BlockGrid base={base} token={token} block_types={block_types} blocks={blocks} version={version} emptyMessage="This page has no blocks yet." />
|
|
16
|
+
</Page>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createRoot } from "react-dom/client"
|
|
2
|
+
import "react-grid-layout/css/styles.css"
|
|
3
|
+
import "keystone_ui-blocks/src/block_grid.css"
|
|
4
|
+
import { register } from "keystone_ui-react/src/registry.js"
|
|
5
|
+
import { startMounting } from "keystone_ui-react/src/mounting.js"
|
|
6
|
+
import PageBuilder from "./page_builder/PageBuilder"
|
|
7
|
+
|
|
8
|
+
register("alembic/page-builder", PageBuilder)
|
|
9
|
+
|
|
10
|
+
startMounting(document, createRoot)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
module Admission
|
|
4
|
+
def self.of(diagnostic, permitted:)
|
|
5
|
+
raise NotPublished if diagnostic.nil? || diagnostic.live_definition.blank?
|
|
6
|
+
raise NotPermitted unless permitted && diagnostic.available?
|
|
7
|
+
|
|
8
|
+
diagnostic
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.of_run(run, permitted: true)
|
|
12
|
+
raise Withdrawn if run.definition_version.withdrawn?
|
|
13
|
+
raise NotPermitted unless permitted && run.flow.available?
|
|
14
|
+
|
|
15
|
+
run
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|