acts_as_calculator_editor 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 +107 -0
- data/Rakefile +8 -0
- data/app/assets/javascripts/acts_as_calculator_editor/application.js +26 -0
- data/app/assets/javascripts/acts_as_calculator_editor/dentaku_tokenizer.js +139 -0
- data/app/assets/javascripts/acts_as_calculator_editor/expression_editor_controller.js +380 -0
- data/app/assets/javascripts/acts_as_calculator_editor/list_filter_controller.js +39 -0
- data/app/assets/javascripts/acts_as_calculator_editor/template_editor_controller.js +86 -0
- data/app/assets/stylesheets/acts_as_calculator_editor/application.css +2 -0
- data/app/controllers/acts_as_calculator_editor/application_controller.rb +115 -0
- data/app/controllers/acts_as_calculator_editor/exports_controller.rb +41 -0
- data/app/controllers/acts_as_calculator_editor/formula_versions_controller.rb +128 -0
- data/app/controllers/acts_as_calculator_editor/formulas_controller.rb +94 -0
- data/app/controllers/acts_as_calculator_editor/lookup_tables_controller.rb +112 -0
- data/app/controllers/acts_as_calculator_editor/templates_controller.rb +130 -0
- data/app/controllers/acts_as_calculator_editor/validations_controller.rb +23 -0
- data/app/helpers/acts_as_calculator_editor/application_helper.rb +85 -0
- data/app/jobs/acts_as_calculator_editor/application_job.rb +6 -0
- data/app/mailers/acts_as_calculator_editor/application_mailer.rb +8 -0
- data/app/models/acts_as_calculator_editor/application_record.rb +7 -0
- data/app/views/acts_as_calculator_editor/formula_versions/_expression_field.html.erb +64 -0
- data/app/views/acts_as_calculator_editor/formula_versions/_variable_fields.html.erb +51 -0
- data/app/views/acts_as_calculator_editor/formula_versions/index.html.erb +45 -0
- data/app/views/acts_as_calculator_editor/formula_versions/new.html.erb +80 -0
- data/app/views/acts_as_calculator_editor/formula_versions/show.html.erb +75 -0
- data/app/views/acts_as_calculator_editor/formulas/_form.html.erb +23 -0
- data/app/views/acts_as_calculator_editor/formulas/edit.html.erb +11 -0
- data/app/views/acts_as_calculator_editor/formulas/index.html.erb +71 -0
- data/app/views/acts_as_calculator_editor/formulas/new.html.erb +10 -0
- data/app/views/acts_as_calculator_editor/formulas/show.html.erb +72 -0
- data/app/views/acts_as_calculator_editor/lookup_tables/_entry_fields.html.erb +24 -0
- data/app/views/acts_as_calculator_editor/lookup_tables/_form.html.erb +47 -0
- data/app/views/acts_as_calculator_editor/lookup_tables/edit.html.erb +12 -0
- data/app/views/acts_as_calculator_editor/lookup_tables/index.html.erb +67 -0
- data/app/views/acts_as_calculator_editor/lookup_tables/new.html.erb +11 -0
- data/app/views/acts_as_calculator_editor/lookup_tables/show.html.erb +71 -0
- data/app/views/acts_as_calculator_editor/shared/_error_message.html.erb +12 -0
- data/app/views/acts_as_calculator_editor/shared/_export_panel.html.erb +52 -0
- data/app/views/acts_as_calculator_editor/shared/_flash.html.erb +8 -0
- data/app/views/acts_as_calculator_editor/shared/_page_header.html.erb +16 -0
- data/app/views/acts_as_calculator_editor/shared/error.html.erb +11 -0
- data/app/views/acts_as_calculator_editor/templates/_form.html.erb +124 -0
- data/app/views/acts_as_calculator_editor/templates/_lexxy_toolbar.html.erb +101 -0
- data/app/views/acts_as_calculator_editor/templates/index.html.erb +83 -0
- data/app/views/acts_as_calculator_editor/templates/new.html.erb +13 -0
- data/app/views/acts_as_calculator_editor/templates/preview.html.erb +29 -0
- data/app/views/acts_as_calculator_editor/templates/show.html.erb +111 -0
- data/app/views/layouts/acts_as_calculator_editor/application.html.erb +52 -0
- data/config/importmap.rb +13 -0
- data/config/routes.rb +46 -0
- data/lib/acts_as_calculator_editor/build_export_document.rb +37 -0
- data/lib/acts_as_calculator_editor/build_sample_result.rb +26 -0
- data/lib/acts_as_calculator_editor/cast_entry_rows.rb +52 -0
- data/lib/acts_as_calculator_editor/cast_export_decimal.rb +19 -0
- data/lib/acts_as_calculator_editor/cast_variable_rows.rb +90 -0
- data/lib/acts_as_calculator_editor/delete_lookup_table.rb +54 -0
- data/lib/acts_as_calculator_editor/engine.rb +32 -0
- data/lib/acts_as_calculator_editor/export_formulas.rb +89 -0
- data/lib/acts_as_calculator_editor/export_lookup_tables.rb +49 -0
- data/lib/acts_as_calculator_editor/export_owner_reference.rb +18 -0
- data/lib/acts_as_calculator_editor/export_templates.rb +51 -0
- data/lib/acts_as_calculator_editor/publish_lookup_table.rb +53 -0
- data/lib/acts_as_calculator_editor/render_template_preview.rb +60 -0
- data/lib/acts_as_calculator_editor/validate_expression.rb +127 -0
- data/lib/acts_as_calculator_editor/version.rb +5 -0
- data/lib/acts_as_calculator_editor.rb +76 -0
- data/lib/tasks/acts_as_calculator_editor_tasks.rake +6 -0
- metadata +248 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<%# Our own <lexxy-toolbar>, copied from `Toolbar.defaultTemplate` in the lexxy 0.9.31 source
|
|
2
|
+
(`src/elements/toolbar.js`, recovered from the shipped sourcemap) with the upload buttons
|
|
3
|
+
removed — docs/lexxy-decision.md §4.
|
|
4
|
+
|
|
5
|
+
`#findOrCreateDefaultToolbar` is `this.querySelector("lexxy-toolbar") ?? this.#createDefaultToolbar()`,
|
|
6
|
+
so supplying one replaces the default wholesale rather than layering on top of it.
|
|
7
|
+
|
|
8
|
+
Dropped from the default: `uploadImage`, `uploadFile` (attachments are off — a Liquid
|
|
9
|
+
template rendered by `ActsAsCalculator::RenderLiquid` has no way to resolve an
|
|
10
|
+
`<action-text-attachment sgid="...">`, so those buttons would produce markup that renders
|
|
11
|
+
as garbage), `insertTable` and `insertCodeBlock`.
|
|
12
|
+
|
|
13
|
+
Code blocks are dropped deliberately: they are Lexxy's Prism-highlighted `CodeNode`, whose
|
|
14
|
+
language list (`src/config/prism.js`) contains no Liquid and no Dentaku, so the button
|
|
15
|
+
offers highlighting that cannot apply to anything an author writes here.
|
|
16
|
+
|
|
17
|
+
Kept: bold, italic, strikethrough, underline, the heading/paragraph dropdown, link, quote,
|
|
18
|
+
lists, divider, undo/redo, and the overflow menu. The overflow dropdown must stay — the
|
|
19
|
+
toolbar's ResizeObserver moves buttons into it on a narrow viewport and drops them on the
|
|
20
|
+
floor if the panel is missing. %>
|
|
21
|
+
<lexxy-toolbar>
|
|
22
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="bold" data-command="bold" title="Bold">
|
|
23
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.05273 1.88232C10.6866 1.88237 12.0033 2.20353 12.9529 2.89673L13.1272 3.0293C13.974 3.70864 14.4008 4.63245 14.4009 5.76562C14.4008 6.49354 14.2316 7.15281 13.8845 7.73145C13.6683 8.09188 13.3997 8.40162 13.0818 8.66016C13.5902 8.92606 14.0196 9.28599 14.3635 9.74121C14.8586 10.3834 15.0945 11.1743 15.0945 12.0879C15.0944 13.3698 14.5922 14.3931 13.5879 15.1106L13.5857 15.1128C12.5967 15.805 11.196 16.125 9.43799 16.125H3.10547V1.88232L9.05273 1.88232ZM6.36108 13.4084H9.28418C10.224 13.4084 10.8634 13.2491 11.2581 12.9851C11.6259 12.7389 11.8198 12.3768 11.8198 11.8367C11.8197 11.2968 11.6259 10.9351 11.2581 10.689C10.8634 10.425 10.2241 10.2649 9.28418 10.2649H6.36108V13.4084ZM6.36108 7.56812H8.78247C9.5163 7.56809 10.0547 7.45371 10.429 7.25757L10.5791 7.16895C10.9438 6.92178 11.1255 6.57934 11.1255 6.09302C11.1254 5.59017 10.9414 5.25227 10.5835 5.02002L10.5784 5.01636L10.5732 5.01343C10.1994 4.75387 9.61878 4.59818 8.78247 4.59814H6.36108V7.56812Z"/></svg>
|
|
24
|
+
</button>
|
|
25
|
+
|
|
26
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="italic" data-command="italic" title="Italic">
|
|
27
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M14.1379 3.91187L14.1086 4.06421H11.4668L9.49805 13.9431H12.0981L11.7473 15.7852L11.7188 15.9375H4.16675L4.51758 14.0955L4.54614 13.9431H7.18799L9.17505 4.06421H6.55664L6.90747 2.22217L6.93677 2.06982H14.4888L14.1379 3.91187Z"/></svg>
|
|
28
|
+
</button>
|
|
29
|
+
|
|
30
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough">
|
|
31
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M14.3723 11.8015C14.3771 11.8858 14.3811 11.9756 14.3811 12.0681C14.3811 12.811 14.1777 13.4959 13.7725 14.1174L13.7717 14.1189C13.3624 14.7329 12.7463 15.2162 11.9377 15.5742L11.9348 15.5757C11.1214 15.9223 10.1306 16.092 8.96997 16.092C7.9356 16.092 6.93308 15.9348 5.96338 15.6204L5.96045 15.6189C5.00593 15.292 4.24112 14.8699 3.67676 14.3459L3.57568 14.2522L3.63501 14.1277L4.45605 12.397L4.64282 12.5654C5.13492 13.0083 5.76733 13.3759 6.54492 13.6648C7.33475 13.9406 8.14322 14.0786 8.96997 14.0786C10.0731 14.0786 10.8638 13.8932 11.3708 13.5513C11.8757 13.1982 12.1172 12.7464 12.1172 12.1838C12.1172 12.0662 12.1049 11.9556 12.0828 11.8513L12.0344 11.625H14.3621L14.3723 11.8015Z"/><path d="M9.2981 1.91602C10.111 1.91604 10.9109 2.02122 11.6975 2.23096C12.4855 2.44111 13.1683 2.74431 13.7417 3.14429L13.8655 3.23071L13.8083 3.36987L13.1726 4.91235L13.0869 5.1189L12.8987 4.99878C12.3487 4.64881 11.761 4.38633 11.1365 4.21143L11.1328 4.20996C10.585 4.04564 10.0484 3.95419 9.52295 3.93384L9.2981 3.92944C8.22329 3.92944 7.44693 4.12611 6.94043 4.49121C6.44619 4.85665 6.20874 5.31616 6.20874 5.88135L6.21533 6.03296C6.24495 6.37662 6.37751 6.65526 6.61011 6.87964L6.72144 6.97632C6.98746 7.19529 7.30625 7.37584 7.68018 7.51538L8.05151 7.63184C8.45325 7.75061 8.94669 7.87679 9.53247 8.01123L9.53467 8.01196C10.1213 8.15305 10.6426 8.29569 11.0991 8.4375H15C15.5178 8.4375 15.9375 8.85723 15.9375 9.375C15.9375 9.89277 15.5178 10.3125 15 10.3125H3C2.48223 10.3125 2.0625 9.89277 2.0625 9.375C2.0625 8.85723 2.48223 8.4375 3 8.4375H4.93726C4.83783 8.34526 4.74036 8.24896 4.64795 8.146L4.64502 8.14233C4.1721 7.58596 3.94482 6.85113 3.94482 5.95825C3.94483 5.20441 4.14059 4.51965 4.53369 3.90967L4.53516 3.90747C4.94397 3.29427 5.55262 2.81114 6.34863 2.45288C7.15081 2.0919 8.13683 1.91602 9.2981 1.91602Z"/></svg>
|
|
32
|
+
</button>
|
|
33
|
+
|
|
34
|
+
<button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="underline" data-command="underline" title="Underline">
|
|
35
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M14 14C14.5523 14 15 14.4477 15 15C15 15.5523 14.5523 16 14 16H4C3.44772 16 3 15.5523 3 15C3 14.4477 3.44772 14 4 14H14Z"/><path d="M12.625 1.59375C13.25 1.59375 13.625 1.97656 13.625 2.64062V9.02344C13.625 11.4844 11.8516 13.1875 8.99219 13.1875C6.14062 13.1875 4.35938 11.4844 4.35938 9.02344V2.64062C4.35938 1.97656 4.74219 1.59375 5.36719 1.59375C6 1.59375 6.375 1.97656 6.375 2.64062V8.84375C6.375 10.3828 7.32031 11.4297 8.99219 11.4297C10.6641 11.4297 11.6172 10.3828 11.6172 8.84375V2.64062C11.6172 1.97656 11.9922 1.59375 12.625 1.59375Z"/></svg>
|
|
36
|
+
</button>
|
|
37
|
+
|
|
38
|
+
<lexxy-toolbar-dropdown class="lexxy-editor__toolbar-dropdown">
|
|
39
|
+
<button data-dropdown-trigger class="lexxy-editor__toolbar-button lexxy-editor__toolbar-button--chevron" type="button" name="format" title="Text formatting" aria-haspopup="menu" aria-expanded="false">
|
|
40
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M11.5 2C12.0523 2 12.5 2.44772 12.5 3V3.5C12.5 4.05228 12.0523 4.5 11.5 4.5H8V15C8 15.5523 7.55228 16 7 16H6.5C5.94772 16 5.5 15.5523 5.5 15V4.5H2C1.44772 4.5 1 4.05228 1 3.5V3C1 2.44772 1.44772 2 2 2H11.5ZM16 7C16.5523 7 17 7.44772 17 8V8.5C17 9.05228 16.5523 9.5 16 9.5H15V15C15 15.5523 14.5523 16 14 16H13.5C12.9477 16 12.5 15.5523 12.5 15V9.5H11.5C10.9477 9.5 10.5 9.05228 10.5 8.5V8C10.5 7.44772 10.9477 7 11.5 7H16Z"/></svg>
|
|
41
|
+
</button>
|
|
42
|
+
<div data-dropdown-panel role="menu" class="lexxy-editor__toolbar-dropdown-list" hidden>
|
|
43
|
+
<button type="button" name="paragraph" data-command="setFormatParagraph" title="Paragraph" role="menuitem">
|
|
44
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M9 12C9.55228 12 10 12.4477 10 13C10 13.5523 9.55228 14 9 14H3C2.44772 14 2 13.5523 2 13C2 12.4477 2.44772 12 3 12H9ZM15 8C15.5523 8 16 8.44772 16 9C16 9.55228 15.5523 10 15 10H3C2.44772 10 2 9.55228 2 9C2 8.44772 2.44772 8 3 8H15ZM15 4C15.5523 4 16 4.44772 16 5C16 5.55228 15.5523 6 15 6H3C2.44772 6 2 5.55228 2 5C2 4.44772 2.44772 4 3 4H15Z"/></svg>
|
|
45
|
+
<span>Normal</span>
|
|
46
|
+
</button>
|
|
47
|
+
<lexxy-heading-dropdown>
|
|
48
|
+
<div class="lexxy-heading-options"></div>
|
|
49
|
+
</lexxy-heading-dropdown>
|
|
50
|
+
<div class="lexxy-editor__toolbar-separator" role="separator"></div>
|
|
51
|
+
<button type="button" name="clear-formatting" data-command="clearFormatting" title="Clear formatting" role="menuitem">
|
|
52
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M10.0607 2.07533C10.8417 1.29432 12.1078 1.2943 12.8888 2.07533L16.424 5.61049C17.205 6.39154 17.205 7.65854 16.424 8.43959L9.44937 15.4142C9.07435 15.7891 8.5656 16.0001 8.03531 16.0001H5.0148C4.55074 16.0001 4.10309 15.8385 3.74722 15.547L3.60074 15.4142L1.57534 13.3888C0.79431 12.6078 0.794336 11.3417 1.57534 10.5607L10.0607 2.07533ZM2.98941 11.9747L5.0148 14.0001H8.03531L9.71792 12.3165L6.18179 8.78139L2.98941 11.9747Z"/></svg>
|
|
53
|
+
<span>Clear formatting</span>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</lexxy-toolbar-dropdown>
|
|
57
|
+
|
|
58
|
+
<lexxy-link-dropdown class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-dropdown--link">
|
|
59
|
+
<button data-dropdown-trigger class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="link" title="Link" data-hotkey="cmd+k ctrl+k" aria-haspopup="dialog" aria-expanded="false">
|
|
60
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M12.8885 7.23091L13.9479 6.17155C14.5337 5.58576 14.5337 4.63602 13.9479 4.05023C13.3621 3.46444 12.4124 3.46444 11.8266 4.05023L8.29235 7.58446C7.9263 7.95051 7.90312 8.52994 8.2233 8.92271L8.36141 9.07463C8.68158 9.4674 8.65841 10.0468 8.29235 10.4129C7.90183 10.8034 7.26866 10.8034 6.87814 10.4129C5.70657 9.24131 5.70657 7.34182 6.87814 6.17025L10.4124 2.63602C11.7792 1.26918 13.9953 1.26918 15.3621 2.63602C16.729 4.00285 16.729 6.21893 15.3621 7.58576L14.3028 8.64512C13.9122 9.03564 13.2791 9.03564 12.8885 8.64512C12.498 8.2546 12.498 7.62143 12.8885 7.23091Z"/><path d="M5.11038 10.7664L4.04843 11.8284C3.46264 12.4142 3.46264 13.3639 4.04842 13.9497C4.63421 14.5355 5.58396 14.5355 6.16975 13.9497L9.70657 10.4129C10.0726 10.0468 10.0958 9.46741 9.77563 9.07464L9.63752 8.92272C9.31734 8.52995 9.34052 7.95052 9.70657 7.58446C10.0971 7.19394 10.7303 7.19394 11.1208 7.58446C12.2924 8.75604 12.2924 10.6555 11.1208 11.8271L7.58396 15.3639C6.21712 16.7308 4.00105 16.7308 2.63421 15.3639C1.26738 13.9971 1.26738 11.781 2.63421 10.4142L3.69617 9.35223C4.08669 8.96171 4.71986 8.96171 5.11038 9.35223C5.5009 9.74275 5.5009 10.3759 5.11038 10.7664Z"/></svg>
|
|
61
|
+
</button>
|
|
62
|
+
<div data-dropdown-panel role="dialog" aria-label="Link" hidden>
|
|
63
|
+
<input type="url" placeholder="Enter a URL…" class="input" id="lexxy-link-url-template-body">
|
|
64
|
+
<div class="lexxy-editor__toolbar-dropdown-actions">
|
|
65
|
+
<button type="button" class="lexxy-editor__toolbar-button" value="link">Link</button>
|
|
66
|
+
<button type="button" class="lexxy-editor__toolbar-button" value="unlink">Unlink</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</lexxy-link-dropdown>
|
|
70
|
+
|
|
71
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="quote" data-command="insertQuoteBlock" title="Quote">
|
|
72
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M4.96387 4.23438C6.8769 4.23438 8.42767 5.78522 8.42773 7.69824C8.42773 8.32925 8.25769 8.92015 7.96289 9.42969L7.96387 9.43066L5.11816 14.3584C4.77659 14.95 4.02038 15.153 3.42871 14.8115C2.83701 14.4699 2.63397 13.7128 2.97559 13.1211L4.16113 11.0674C2.63532 10.7052 1.5 9.33485 1.5 7.69824C1.50006 5.78524 3.05086 4.2344 4.96387 4.23438ZM13.0361 4.23438C14.9491 4.23449 16.4999 5.7853 16.5 7.69824C16.5 8.32921 16.3299 8.92017 16.0352 9.42969L16.0361 9.43066L13.1904 14.3584C12.8488 14.9501 12.0917 15.1531 11.5 14.8115C10.9085 14.4698 10.7063 13.7127 11.0479 13.1211L12.2324 11.0674C10.7069 10.7049 9.57227 9.33461 9.57227 7.69824C9.57233 5.78522 11.1231 4.23438 13.0361 4.23438Z"/></svg>
|
|
73
|
+
</button>
|
|
74
|
+
|
|
75
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="unordered-list" data-command="insertUnorderedList" title="Bullet list">
|
|
76
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M3 12.5C3.82843 12.5 4.5 13.1716 4.5 14C4.5 14.8284 3.82843 15.5 3 15.5C2.17157 15.5 1.5 14.8284 1.5 14C1.5 13.1716 2.17157 12.5 3 12.5ZM15.5 13C16.0523 13 16.5 13.4477 16.5 14C16.5 14.5523 16.0523 15 15.5 15H7C6.44772 15 6 14.5523 6 14C6 13.4477 6.44772 13 7 13H15.5ZM3 7.5C3.82843 7.5 4.5 8.17157 4.5 9C4.5 9.82843 3.82843 10.5 3 10.5C2.17157 10.5 1.5 9.82843 1.5 9C1.5 8.17157 2.17157 7.5 3 7.5ZM15.5 8C16.0523 8 16.5 8.44772 16.5 9C16.5 9.55228 16.0523 10 15.5 10H7C6.44772 10 6 9.55228 6 9C6 8.44772 6.44772 8 7 8H15.5ZM3 2.5C3.82843 2.5 4.5 3.17157 4.5 4C4.5 4.82843 3.82843 5.5 3 5.5C2.17157 5.5 1.5 4.82843 1.5 4C1.5 3.17157 2.17157 2.5 3 2.5ZM15.5 3C16.0523 3 16.5 3.44772 16.5 4C16.5 4.55228 16.0523 5 15.5 5H7C6.44772 5 6 4.55228 6 4C6 3.44772 6.44772 3 7 3H15.5Z"/></svg>
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
<button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="ordered-list" data-command="insertOrderedList" title="Numbered list">
|
|
80
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M15.5 13C16.0523 13 16.5 13.4477 16.5 14C16.5 14.5523 16.0523 15 15.5 15H7C6.44772 15 6 14.5523 6 14C6 13.4477 6.44772 13 7 13H15.5ZM15.5 8C16.0523 8 16.5 8.44772 16.5 9C16.5 9.55228 16.0523 10 15.5 10H7C6.44772 10 6 9.55228 6 9C6 8.44772 6.44772 8 7 8H15.5ZM15.5 3C16.0523 3 16.5 3.44772 16.5 4C16.5 4.55228 16.0523 5 15.5 5H7C6.44772 5 6 4.55228 6 4C6 3.44772 6.44772 3 7 3H15.5Z"/><path d="M2.98657 16.0967C2.68042 16.0967 2.41187 16.0465 2.18091 15.9463C1.95174 15.846 1.77002 15.7046 1.63574 15.522C1.50146 15.3376 1.42448 15.1227 1.40479 14.8774L1.4021 14.8452H2.34204L2.34741 14.8748C2.35815 14.9589 2.39038 15.035 2.44409 15.103C2.49959 15.1711 2.5721 15.2248 2.66162 15.2642C2.75293 15.3035 2.86035 15.3232 2.98389 15.3232C3.10563 15.3232 3.21037 15.3027 3.2981 15.2615C3.38761 15.2185 3.45654 15.1603 3.50488 15.0869C3.55322 15.0135 3.57739 14.9294 3.57739 14.8345V14.8291C3.57739 14.6715 3.51921 14.5516 3.40283 14.4692C3.28646 14.3869 3.12085 14.3457 2.90601 14.3457H2.48706V13.677H2.90063C3.02775 13.677 3.13607 13.6582 3.22559 13.6206C3.31689 13.583 3.38672 13.5302 3.43506 13.4622C3.48519 13.3941 3.51025 13.3153 3.51025 13.2258V13.2205C3.51025 13.1256 3.48877 13.0441 3.4458 12.9761C3.40462 12.9062 3.34375 12.8534 3.26318 12.8176C3.18441 12.78 3.08952 12.7612 2.97852 12.7612C2.86572 12.7612 2.76636 12.7809 2.68042 12.8203C2.59627 12.8579 2.52913 12.9125 2.479 12.9841C2.43066 13.054 2.40112 13.1363 2.39038 13.2312L2.3877 13.2581H1.49341L1.49609 13.2205C1.514 12.977 1.58561 12.7666 1.71094 12.5894C1.83805 12.4103 2.00903 12.2725 2.22388 12.1758C2.44051 12.0773 2.69206 12.0281 2.97852 12.0281C3.27393 12.0281 3.52995 12.0728 3.74658 12.1624C3.96322 12.2501 4.13062 12.3727 4.24878 12.5303C4.36694 12.6878 4.42603 12.8722 4.42603 13.0835V13.0889C4.42603 13.2518 4.38932 13.3941 4.31592 13.5159C4.2443 13.6358 4.14762 13.7343 4.02588 13.8113C3.90592 13.8883 3.77254 13.942 3.62573 13.9724V13.9912C3.91756 14.0199 4.14941 14.1121 4.32129 14.2678C4.49316 14.4236 4.5791 14.6295 4.5791 14.8855V14.8909C4.5791 15.1344 4.51375 15.3474 4.38306 15.53C4.25236 15.7109 4.06795 15.8505 3.82983 15.949C3.59172 16.0474 3.31063 16.0967 2.98657 16.0967Z"/><path d="M1.54443 11V10.342L2.76099 9.20874C2.95076 9.03507 3.09757 8.89274 3.20142 8.78174C3.30705 8.66895 3.37956 8.57316 3.41895 8.49438C3.46012 8.41382 3.48071 8.33415 3.48071 8.25537V8.24463C3.48071 8.14795 3.46012 8.0638 3.41895 7.99219C3.37777 7.92057 3.31779 7.86507 3.23901 7.82568C3.16024 7.7863 3.06714 7.7666 2.95972 7.7666C2.84692 7.7666 2.74756 7.78988 2.66162 7.83643C2.57747 7.88298 2.51123 7.94743 2.46289 8.02979C2.41455 8.11035 2.39038 8.20345 2.39038 8.30908V8.33057L1.48804 8.32788V8.31177C1.48804 8.05396 1.5507 7.82837 1.67603 7.63501C1.80314 7.44165 1.97949 7.29126 2.20508 7.18384C2.43245 7.07463 2.69653 7.02002 2.99731 7.02002C3.28556 7.02002 3.53711 7.06836 3.75195 7.16504C3.96859 7.25993 4.13688 7.39331 4.25684 7.56519C4.37858 7.73706 4.43945 7.93758 4.43945 8.16675V8.18018C4.43945 8.3252 4.40902 8.46932 4.34814 8.61255C4.28727 8.75578 4.18701 8.90885 4.04736 9.07178C3.90771 9.23291 3.71883 9.41642 3.48071 9.62231L2.58374 10.4092L2.85498 9.98486V10.4092L2.58374 10.2319H4.49048V11H1.54443Z"/><path d="M2.84155 6V3.01367H2.79053L1.85596 3.64478V2.79614L2.84155 2.12476H3.82715V6H2.84155Z"/></svg>
|
|
81
|
+
</button>
|
|
82
|
+
|
|
83
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="divider" data-command="insertHorizontalDivider" title="Insert a divider">
|
|
84
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M12.75 12C13.1642 12 13.5 12.3358 13.5 12.75V14.25C13.5 14.6642 13.1642 15 12.75 15H5.25C4.83579 15 4.5 14.6642 4.5 14.25V12.75C4.5 12.3358 4.83579 12 5.25 12H12.75ZM15.4863 8C16.0461 8 16.5 8.44771 16.5 9C16.5 9.55229 16.0461 10 15.4863 10H2.51367C1.95392 10 1.5 9.55229 1.5 9C1.5 8.44771 1.95392 8 2.51367 8H15.4863ZM12.75 3C13.1642 3 13.5 3.33579 13.5 3.75V5.25C13.5 5.66421 13.1642 6 12.75 6H5.25C4.83579 6 4.5 5.66421 4.5 5.25V3.75C4.5 3.33579 4.83579 3 5.25 3H12.75Z"/></svg>
|
|
85
|
+
</button>
|
|
86
|
+
|
|
87
|
+
<button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-button--push-right" type="button" name="undo" data-command="undo" title="Undo" disabled aria-disabled="true">
|
|
88
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.36612 5.36612C8.85427 4.87796 9.64554 4.87796 10.1337 5.36612C10.6218 5.85428 10.6218 6.64557 10.1337 7.13369L7.26748 9.9999H15.2499C18.1494 9.99996 20.4999 12.3504 20.4999 15.2499V19.2499C20.4999 19.9402 19.9402 20.4999 19.2499 20.4999C18.5596 20.4999 18 19.9402 17.9999 19.2499V15.2499C17.9999 13.7312 16.7686 12.5 15.2499 12.4999H7.26748L10.1337 15.3661C10.6218 15.8543 10.6218 16.6456 10.1337 17.1337C9.64557 17.6218 8.85428 17.6218 8.36612 17.1337L3.36612 12.1337C2.87796 11.6455 2.87796 10.8543 3.36612 10.3661L8.36612 5.36612Z"/></svg>
|
|
89
|
+
</button>
|
|
90
|
+
|
|
91
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="redo" data-command="redo" title="Redo" disabled aria-disabled="true">
|
|
92
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M15.6338 5.1163C15.1456 4.62814 14.3543 4.62814 13.8662 5.1163C13.3781 5.60446 13.3781 6.39575 13.8662 6.88388L16.7324 9.75009H8.74997C5.85052 9.75014 3.49997 12.1006 3.49997 15.0001V19.0001C3.50002 19.6904 4.05969 20.25 4.74997 20.2501C5.4403 20.2501 5.99992 19.6904 5.99997 19.0001V15.0001C5.99997 13.4813 7.23123 12.2501 8.74997 12.2501H16.7324L13.8662 15.1163C13.3781 15.6045 13.3781 16.3958 13.8662 16.8839C14.3543 17.372 15.1456 17.3719 15.6338 16.8839L20.6338 11.8839C21.1219 11.3957 21.1219 10.6045 20.6338 10.1163L15.6338 5.1163Z"/></svg>
|
|
93
|
+
</button>
|
|
94
|
+
|
|
95
|
+
<lexxy-toolbar-dropdown class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-button--push-right lexxy-editor__toolbar-overflow">
|
|
96
|
+
<button data-dropdown-trigger class="lexxy-editor__toolbar-button" type="button" aria-haspopup="menu" aria-expanded="false" aria-label="Show more toolbar buttons">
|
|
97
|
+
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M3 6.75C4.24264 6.75 5.25 7.75736 5.25 9C5.25 10.2426 4.24264 11.25 3 11.25C1.75736 11.25 0.75 10.2426 0.75 9C0.75 7.75736 1.75736 6.75 3 6.75ZM9 6.75C10.2426 6.75 11.25 7.75736 11.25 9C11.25 10.2426 10.2426 11.25 9 11.25C7.75736 11.25 6.75 10.2426 6.75 9C6.75 7.75736 7.75736 6.75 9 6.75ZM15 6.75C16.2426 6.75 17.25 7.75736 17.25 9C17.25 10.2426 16.2426 11.25 15 11.25C13.7574 11.25 12.75 10.2426 12.75 9C12.75 7.75736 13.7574 6.75 15 6.75Z"/></svg>
|
|
98
|
+
</button>
|
|
99
|
+
<div data-dropdown-panel role="menu" class="lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons" hidden></div>
|
|
100
|
+
</lexxy-toolbar-dropdown>
|
|
101
|
+
</lexxy-toolbar>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<% content_for :title, "Templates" %>
|
|
2
|
+
|
|
3
|
+
<%= render "acts_as_calculator_editor/shared/page_header",
|
|
4
|
+
title: "Templates",
|
|
5
|
+
subtitle: "Templates are versioned, not effective-dated. Publishing makes a new version current; promoting rolls back to an older one.",
|
|
6
|
+
actions: link_to("New template", new_template_path, class: button_class(:primary)) %>
|
|
7
|
+
|
|
8
|
+
<div data-controller="list-filter" class="rounded-lg border border-slate-200 bg-white">
|
|
9
|
+
<div class="flex flex-wrap items-end gap-3 border-b border-slate-200 p-4">
|
|
10
|
+
<%= form_with url: templates_path, method: :get, class: "flex flex-wrap items-end gap-3" do |form| %>
|
|
11
|
+
<div>
|
|
12
|
+
<%= form.label :key, "Key", class: label_class %>
|
|
13
|
+
<%= form.text_field :key, value: params[:key], class: field_class("w-40 font-mono") %>
|
|
14
|
+
</div>
|
|
15
|
+
<div>
|
|
16
|
+
<%= form.label :scope, "Scope", class: label_class %>
|
|
17
|
+
<%= form.text_field :scope, value: params[:scope], class: field_class("w-40 font-mono") %>
|
|
18
|
+
</div>
|
|
19
|
+
<label class="flex items-center gap-2 pb-2 text-sm text-slate-700">
|
|
20
|
+
<%= form.check_box :history, { checked: @history, class: "rounded border-slate-300" }, "true", nil %>
|
|
21
|
+
Show every version
|
|
22
|
+
</label>
|
|
23
|
+
<%= form.submit "Filter", class: button_class(:secondary) %>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<div class="ml-auto">
|
|
27
|
+
<%= label_tag :template_quick_filter, "Narrow this page", class: label_class %>
|
|
28
|
+
<%= text_field_tag :template_quick_filter, nil,
|
|
29
|
+
class: field_class("w-56"),
|
|
30
|
+
placeholder: "type to filter rows",
|
|
31
|
+
autocomplete: "off",
|
|
32
|
+
data: { list_filter_target: "query", action: "input->list-filter#filter" } %>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="overflow-x-auto">
|
|
37
|
+
<table class="w-full text-sm">
|
|
38
|
+
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
|
|
39
|
+
<tr>
|
|
40
|
+
<th class="px-4 py-2 font-medium">Key</th>
|
|
41
|
+
<th class="px-4 py-2 font-medium">Scope</th>
|
|
42
|
+
<th class="px-4 py-2 font-medium">Owner</th>
|
|
43
|
+
<th class="px-4 py-2 font-medium">Format</th>
|
|
44
|
+
<th class="px-4 py-2 font-medium">Version</th>
|
|
45
|
+
<th class="px-4 py-2 font-medium">Current</th>
|
|
46
|
+
<th class="px-4 py-2"></th>
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody class="divide-y divide-slate-100">
|
|
50
|
+
<% @templates.each do |template| %>
|
|
51
|
+
<tr id="<%= dom_id(template) %>" class="hover:bg-slate-50"
|
|
52
|
+
data-list-filter-target="row">
|
|
53
|
+
<td class="px-4 py-2 font-mono font-medium text-slate-900"><%= template.key %></td>
|
|
54
|
+
<td class="px-4 py-2 font-mono text-slate-600"><%= template.scope %></td>
|
|
55
|
+
<td class="px-4 py-2 text-slate-600"><%= template.owner_type ? "#{template.owner_type}##{template.owner_id}" : "global" %></td>
|
|
56
|
+
<td class="px-4 py-2">
|
|
57
|
+
<span class="inline-flex rounded bg-slate-100 px-1.5 py-0.5 font-mono text-xs text-slate-700"><%= template.format %></span>
|
|
58
|
+
</td>
|
|
59
|
+
<td class="px-4 py-2 text-slate-600"><%= template.version_number %></td>
|
|
60
|
+
<td class="px-4 py-2">
|
|
61
|
+
<% if template.current? %>
|
|
62
|
+
<span class="inline-flex rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-800">current</span>
|
|
63
|
+
<% else %>
|
|
64
|
+
<span class="inline-flex rounded-full bg-slate-200 px-2 py-0.5 text-xs font-medium text-slate-600">superseded</span>
|
|
65
|
+
<% end %>
|
|
66
|
+
</td>
|
|
67
|
+
<td class="px-4 py-2 text-right">
|
|
68
|
+
<%= link_to "Show", template_path(template), class: "font-medium text-slate-700 hover:text-slate-900" %>
|
|
69
|
+
</td>
|
|
70
|
+
</tr>
|
|
71
|
+
<% end %>
|
|
72
|
+
</tbody>
|
|
73
|
+
</table>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<p class="px-4 py-3 text-sm text-slate-500" data-list-filter-target="empty" hidden>
|
|
77
|
+
No templates match that filter.
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
<% if @templates.empty? %>
|
|
81
|
+
<p class="px-4 py-3 text-sm text-slate-500">No templates yet.</p>
|
|
82
|
+
<% end %>
|
|
83
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<% content_for :title, "New template version" %>
|
|
2
|
+
|
|
3
|
+
<%# There is no "edit template" form anywhere in this engine: PublishTemplate always writes
|
|
4
|
+
a new row and demotes the outgoing one, which is what makes rollback possible. Reaching
|
|
5
|
+
this page from an existing template prefills it, so an edit lands as a new version. %>
|
|
6
|
+
<%= render "acts_as_calculator_editor/shared/page_header",
|
|
7
|
+
back_label: "← Templates", back_path: templates_path,
|
|
8
|
+
title: "Publish a template version",
|
|
9
|
+
subtitle: "Publishing writes a new version and demotes the outgoing one. Nothing is overwritten." %>
|
|
10
|
+
|
|
11
|
+
<%= render "acts_as_calculator_editor/shared/error_message" %>
|
|
12
|
+
|
|
13
|
+
<%= render "form", form_values: @form %>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<%= turbo_frame_tag "template_preview" do %>
|
|
2
|
+
<section class="rounded-lg border border-slate-200 bg-white">
|
|
3
|
+
<header class="flex items-center justify-between border-b border-slate-200 px-4 py-2">
|
|
4
|
+
<h2 class="text-sm font-semibold text-slate-900">Preview</h2>
|
|
5
|
+
<span class="text-xs text-slate-500">
|
|
6
|
+
Rendered against a sample result — value 41250.5, inputs salary and tax.
|
|
7
|
+
</span>
|
|
8
|
+
</header>
|
|
9
|
+
|
|
10
|
+
<div class="p-4">
|
|
11
|
+
<% if @preview_error.present? %>
|
|
12
|
+
<%# Liquid parses in strict mode with line numbers on, so this message points at the
|
|
13
|
+
line that broke — worth showing as-is. %>
|
|
14
|
+
<p class="editor-error whitespace-pre-line rounded-md border border-red-200 bg-red-50 px-3 py-2 font-mono text-xs text-red-800"
|
|
15
|
+
role="alert"><%= @preview_error %></p>
|
|
16
|
+
<% elsif @preview_format == ActsAsCalculator::Template::TEXT %>
|
|
17
|
+
<pre class="preview-text overflow-x-auto whitespace-pre-wrap rounded-md bg-slate-50 p-3 font-mono text-xs text-slate-800"><%= @preview_body %></pre>
|
|
18
|
+
<% else %>
|
|
19
|
+
<%# Never `raw`/`html_safe` into this document. The body is author-controlled HTML and
|
|
20
|
+
the core gem deliberately declines to decide whether it is safe to inject, so it is
|
|
21
|
+
rendered in a sandboxed iframe where it cannot reach the operator's page. `srcdoc`
|
|
22
|
+
is attribute-escaped by ERB, and `sandbox=""` denies scripts, forms and same-origin.
|
|
23
|
+
docs/core-gem-contract.md §7.8. %>
|
|
24
|
+
<iframe class="preview-html h-64 w-full rounded-md border border-slate-200 bg-white"
|
|
25
|
+
title="Template preview" sandbox="" srcdoc="<%= @preview_body %>"></iframe>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
<% end %>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<% content_for :title, "#{@template.key} v#{@template.version_number}" %>
|
|
2
|
+
|
|
3
|
+
<%= render "acts_as_calculator_editor/shared/page_header",
|
|
4
|
+
back_label: "← Templates", back_path: templates_path,
|
|
5
|
+
title: "#{@template.key} — version #{@template.version_number}",
|
|
6
|
+
subtitle: "Scope: #{@template.scope} · Format: #{@template.format} · Owner: #{@template.owner_type ? "#{@template.owner_type}##{@template.owner_id}" : 'global'} · #{@template.current? ? 'current' : 'superseded'}",
|
|
7
|
+
actions: link_to("Publish a new version from this", new_template_path(from_id: @template.id), class: button_class(:primary)) %>
|
|
8
|
+
|
|
9
|
+
<%= render "acts_as_calculator_editor/shared/error_message" %>
|
|
10
|
+
|
|
11
|
+
<section class="mb-8 rounded-lg border border-slate-200 bg-white">
|
|
12
|
+
<header class="border-b border-slate-200 px-4 py-3">
|
|
13
|
+
<h2 class="text-sm font-semibold text-slate-900">Body</h2>
|
|
14
|
+
</header>
|
|
15
|
+
<div class="p-4">
|
|
16
|
+
<%# The stored Liquid source, shown as source. Never rendered into this document — that
|
|
17
|
+
is what the sandboxed preview iframe below is for. %>
|
|
18
|
+
<pre class="overflow-x-auto whitespace-pre-wrap rounded-md bg-slate-900 p-3 font-mono text-xs text-slate-100"><code><%= @template.body %></code></pre>
|
|
19
|
+
</div>
|
|
20
|
+
</section>
|
|
21
|
+
|
|
22
|
+
<section class="mb-8">
|
|
23
|
+
<h2 class="mb-3 text-sm font-semibold text-slate-900">Preview</h2>
|
|
24
|
+
|
|
25
|
+
<%= form_with url: preview_template_path(@template), method: :post,
|
|
26
|
+
html: { class: "mb-4 rounded-lg border border-slate-200 bg-white p-4" },
|
|
27
|
+
data: { turbo_frame: "template_preview" } do |form| %>
|
|
28
|
+
<%= form.label :context, "Sample context (JSON object, merged under `result`)", class: label_class %>
|
|
29
|
+
<%= form.text_area :context, rows: 3, spellcheck: false,
|
|
30
|
+
class: field_class("font-mono text-xs"),
|
|
31
|
+
placeholder: '{"employee_name": "Ada"}' %>
|
|
32
|
+
<%= form.submit "Render preview", class: "#{button_class(:secondary)} mt-3" %>
|
|
33
|
+
<% end %>
|
|
34
|
+
|
|
35
|
+
<%= turbo_frame_tag "template_preview" do %>
|
|
36
|
+
<div class="rounded-lg border border-dashed border-slate-300 bg-white p-5 text-sm text-slate-500">
|
|
37
|
+
Preview appears here.
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<section class="mb-8 overflow-x-auto rounded-lg border border-slate-200 bg-white">
|
|
43
|
+
<header class="border-b border-slate-200 px-4 py-3">
|
|
44
|
+
<h2 class="text-sm font-semibold text-slate-900">Versions</h2>
|
|
45
|
+
</header>
|
|
46
|
+
<table class="w-full text-sm">
|
|
47
|
+
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
|
|
48
|
+
<tr>
|
|
49
|
+
<th class="px-4 py-2 font-medium">#</th>
|
|
50
|
+
<th class="px-4 py-2 font-medium">Current</th>
|
|
51
|
+
<th class="px-4 py-2"></th>
|
|
52
|
+
</tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody class="divide-y divide-slate-100">
|
|
55
|
+
<% @siblings.each do |sibling| %>
|
|
56
|
+
<tr id="<%= dom_id(sibling) %>" class="hover:bg-slate-50">
|
|
57
|
+
<td class="px-4 py-2 text-slate-600"><%= sibling.version_number %></td>
|
|
58
|
+
<td class="px-4 py-2">
|
|
59
|
+
<% if sibling.current? %>
|
|
60
|
+
<span class="inline-flex rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-800">current</span>
|
|
61
|
+
<% else %>
|
|
62
|
+
<span class="text-slate-400">—</span>
|
|
63
|
+
<% end %>
|
|
64
|
+
</td>
|
|
65
|
+
<td class="px-4 py-2">
|
|
66
|
+
<div class="flex items-center justify-end gap-3">
|
|
67
|
+
<%= link_to "Show", template_path(sibling), class: "font-medium text-slate-700 hover:text-slate-900" %>
|
|
68
|
+
<% unless sibling.current? %>
|
|
69
|
+
<%# Rollback: point `current` at an older row. The newer one stays in the
|
|
70
|
+
history rather than being deleted. %>
|
|
71
|
+
<%= button_to "Make current", promote_template_path(sibling), method: :post,
|
|
72
|
+
class: button_class(:secondary) %>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
</td>
|
|
76
|
+
</tr>
|
|
77
|
+
<% end %>
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<%# `Template` has no before_destroy that hands `current` to a surviving sibling, so deleting
|
|
83
|
+
the current version leaves the whole key with no current row at all — the older versions
|
|
84
|
+
stay in the table but `ResolveTemplate` looks only at `Template.current` and raises
|
|
85
|
+
`TemplateNotFoundError` from then on. That is the core gem's own DELETE behaviour, not
|
|
86
|
+
something this UI adds, but this UI is the first place with a button on it, so the copy
|
|
87
|
+
says what the button does rather than "removes this one version". `PromoteTemplate` on a
|
|
88
|
+
sibling is the way back. %>
|
|
89
|
+
<% orphans_the_key = @template.current? && @siblings.many? %>
|
|
90
|
+
|
|
91
|
+
<section class="rounded-lg border border-red-200 bg-red-50 p-4">
|
|
92
|
+
<h2 class="text-sm font-semibold text-red-900">Delete</h2>
|
|
93
|
+
|
|
94
|
+
<p class="mt-1 mb-3 text-sm text-red-800">
|
|
95
|
+
Removes this one version from the history.
|
|
96
|
+
<% if orphans_the_key %>
|
|
97
|
+
<strong>This is the current version.</strong> Deleting it does not promote another one:
|
|
98
|
+
<%= @template.key %> would have no current version at all and would stop rendering, even
|
|
99
|
+
though its older versions remain. Make another version current first.
|
|
100
|
+
<% end %>
|
|
101
|
+
</p>
|
|
102
|
+
|
|
103
|
+
<%= button_to "Delete this version", template_path(@template), method: :delete,
|
|
104
|
+
class: button_class(:danger),
|
|
105
|
+
form: { data: { turbo_confirm: orphans_the_key ?
|
|
106
|
+
"Delete version #{@template.version_number} of #{@template.key}? " \
|
|
107
|
+
"It is the current version, and no other version is promoted in " \
|
|
108
|
+
"its place — #{@template.key} will stop rendering until you make " \
|
|
109
|
+
"one current." :
|
|
110
|
+
"Delete version #{@template.version_number} of #{@template.key}?" } } %>
|
|
111
|
+
</section>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= content_for(:title).presence || "Calculator editor" %></title>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
8
|
+
|
|
9
|
+
<%= yield :head %>
|
|
10
|
+
|
|
11
|
+
<%# Lexxy's own stylesheet, resolved through its engine's app/assets/stylesheets. It is
|
|
12
|
+
two @imports deep (lexxy.css -> lexxy-content.css + lexxy-editor.css -> lexxy-variables
|
|
13
|
+
.css), so this one tag is the whole set. It goes BEFORE ours so our scoped rules win. %>
|
|
14
|
+
<%= stylesheet_link_tag "lexxy", media: "all" %>
|
|
15
|
+
<%= stylesheet_link_tag "acts_as_calculator_editor/application", media: "all" %>
|
|
16
|
+
|
|
17
|
+
<%# Guarded because a host app is free to use a bundler other than importmap-rails.
|
|
18
|
+
`javascript_importmap_tags` emits the host's map (which is where @hotwired/stimulus and
|
|
19
|
+
lexxy are pinned) plus the host's own entry point; the engine's pins are merged into
|
|
20
|
+
that map by the "acts_as_calculator_editor.importmap" initializer. The second tag is
|
|
21
|
+
what actually loads OUR entry point — being in the map does not execute anything. %>
|
|
22
|
+
<% if respond_to?(:javascript_importmap_tags) %>
|
|
23
|
+
<%= javascript_importmap_tags %>
|
|
24
|
+
<%= javascript_import_module_tag "acts_as_calculator_editor/application" %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</head>
|
|
27
|
+
<body class="acts-as-calculator-editor">
|
|
28
|
+
<div class="min-h-screen bg-slate-50 text-slate-900">
|
|
29
|
+
<header class="border-b border-slate-200 bg-white">
|
|
30
|
+
<div class="mx-auto flex max-w-6xl flex-wrap items-center gap-x-6 gap-y-2 px-6 py-3">
|
|
31
|
+
<span class="text-sm font-semibold tracking-tight text-slate-900">Calculator editor</span>
|
|
32
|
+
|
|
33
|
+
<nav class="flex items-center gap-1 text-sm" aria-label="Main">
|
|
34
|
+
<%= link_to "Formulas", formulas_path, class: nav_class(controller_name.in?(%w[formulas formula_versions])) %>
|
|
35
|
+
<%= link_to "Templates", templates_path, class: nav_class(controller_name == "templates") %>
|
|
36
|
+
<%= link_to "Lookup tables", lookup_tables_path, class: nav_class(controller_name == "lookup_tables") %>
|
|
37
|
+
</nav>
|
|
38
|
+
|
|
39
|
+
<div class="ml-auto">
|
|
40
|
+
<%= render "acts_as_calculator_editor/shared/export_panel" %>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</header>
|
|
44
|
+
|
|
45
|
+
<%= render "acts_as_calculator_editor/shared/flash" %>
|
|
46
|
+
|
|
47
|
+
<main class="mx-auto max-w-6xl px-6 py-8">
|
|
48
|
+
<%= yield %>
|
|
49
|
+
</main>
|
|
50
|
+
</div>
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This engine's own importmap, merged into the host's by the `importmap` initializer in
|
|
4
|
+
# lib/acts_as_calculator_editor/engine.rb. It pins only files this gem ships.
|
|
5
|
+
#
|
|
6
|
+
# Deliberately absent: `@hotwired/stimulus`, `@hotwired/turbo-rails` and `lexxy`. Pinning a
|
|
7
|
+
# name the host has already pinned would silently override the host's copy with ours (an
|
|
8
|
+
# importmap is a flat last-one-wins map), so those three stay the host's to declare. The
|
|
9
|
+
# engine's asset paths make `lexxy.js` resolvable; `pin "lexxy"` is still the host's line to
|
|
10
|
+
# write. See _workspace/04_frontend_report.md for the host install checklist.
|
|
11
|
+
|
|
12
|
+
pin_all_from ActsAsCalculatorEditor::Engine.root.join("app/assets/javascripts/acts_as_calculator_editor"),
|
|
13
|
+
under: "acts_as_calculator_editor"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# An HTML/Turbo surface, not a second JSON API (docs/core-gem-contract.md §1). These
|
|
4
|
+
# controllers read `calculator_*` tables through ActiveRecord and write to them only
|
|
5
|
+
# through the core gem's own Decrees, in-process — so a host that keeps
|
|
6
|
+
# `ActsAsCalculator.configuration.enable_api` off (the default) can still install this
|
|
7
|
+
# editor, and the core engine need not even be mounted.
|
|
8
|
+
ActsAsCalculatorEditor::Engine.routes.draw do
|
|
9
|
+
root to: "formulas#index"
|
|
10
|
+
|
|
11
|
+
# Identity only — key and scope. A formula's maths lives on its versions, which is why
|
|
12
|
+
# there is no `expression` anywhere in this resource (contract §2.1).
|
|
13
|
+
resources :formulas do
|
|
14
|
+
# Immutable: publishing a new version is the only way to change a formula's maths, so
|
|
15
|
+
# there is deliberately no edit/update/destroy here (contract §2.4).
|
|
16
|
+
resources :versions, only: %i[index show new create], controller: "formula_versions"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# No update, for the same reason: `PublishTemplate` writes a new row and `PromoteTemplate`
|
|
20
|
+
# rolls back to an older one (contract §2.5).
|
|
21
|
+
resources :templates, only: %i[index show new create destroy] do
|
|
22
|
+
collection do
|
|
23
|
+
# Live preview of an unsaved body. `RenderTemplate` reads only `#body`, so this needs
|
|
24
|
+
# no persisted record and no throwaway version row (contract §4.3).
|
|
25
|
+
post :preview
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
member do
|
|
29
|
+
post :preview
|
|
30
|
+
post :promote
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The core gem has no controller and no serializer for these (contract §4.4), so this is
|
|
35
|
+
# the only authoring surface they have short of hand-writing an import file. Writes go
|
|
36
|
+
# through `ImportLookupTable`, which carries the guard against rewriting brackets an
|
|
37
|
+
# audited formula version resolved to.
|
|
38
|
+
resources :lookup_tables
|
|
39
|
+
|
|
40
|
+
# Emits the *import* document shape, not the API serializer shape (contract §4.2).
|
|
41
|
+
resource :export, only: :show, controller: "exports"
|
|
42
|
+
|
|
43
|
+
# The one JSON endpoint in the engine: a live-typing helper for the expression field,
|
|
44
|
+
# not a CRUD surface. Backed by `BuildCalculator#ast`/`#identifiers` (contract §4.1).
|
|
45
|
+
post "validations", to: "validations#create", as: :validations
|
|
46
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActsAsCalculatorEditor
|
|
4
|
+
# A document `ActsAsCalculator::ImportDefinitions` can read back, section for section.
|
|
5
|
+
#
|
|
6
|
+
# Section order matches `ImportDefinitions::SECTIONS` and is not arbitrary: a formula
|
|
7
|
+
# variable with `source_type: "lookup"` cannot resolve until its table exists, so
|
|
8
|
+
# `lookup_tables` has to be applied first. Ruby hashes keep insertion order and so does
|
|
9
|
+
# `to_json`, which is the whole mechanism keeping that promise.
|
|
10
|
+
#
|
|
11
|
+
# **The default is what is live, not what has ever been.** Active formula versions and
|
|
12
|
+
# current template versions only. A full-history export of a long-lived formula is mostly
|
|
13
|
+
# a re-creation of history nobody will consult, and for templates it is actively wrong
|
|
14
|
+
# (see `ExportTemplates`). `full_history: true` widens the formulas section to every
|
|
15
|
+
# version, in the order a re-import has to apply them; nothing widens the templates one.
|
|
16
|
+
class BuildExportDocument
|
|
17
|
+
def self.call(...)
|
|
18
|
+
new(...).call
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(key: nil, scope: nil, full_history: false)
|
|
22
|
+
@key = key.presence
|
|
23
|
+
@scope = scope.presence
|
|
24
|
+
@full_history = full_history
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call
|
|
28
|
+
{ "lookup_tables" => ExportLookupTables.(key:, scope:),
|
|
29
|
+
"formulas" => ExportFormulas.(key:, scope:, full_history:),
|
|
30
|
+
"templates" => ExportTemplates.(key:, scope:) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :key, :scope, :full_history
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActsAsCalculatorEditor
|
|
4
|
+
# Something for `{{ result.value }}` to be, while the author is still typing the template.
|
|
5
|
+
#
|
|
6
|
+
# `ActsAsCalculator::Result` is a plain `Data.define`, and `CastLiquidValue` wraps whatever
|
|
7
|
+
# it is handed in a `ResultDrop` on the way into the template — so a preview needs no run,
|
|
8
|
+
# no formula version and no database row (docs/core-gem-contract.md §4.3). `formula_version`
|
|
9
|
+
# is left nil deliberately: `{{ result.formula_version }}` in a preview should read as
|
|
10
|
+
# empty rather than as some arbitrary row the author did not choose.
|
|
11
|
+
class BuildSampleResult
|
|
12
|
+
DEFAULT_VALUE = "41250.5"
|
|
13
|
+
DEFAULT_INPUTS = { "salary" => 50_000, "tax" => 8_749.5 }.freeze
|
|
14
|
+
|
|
15
|
+
def self.call(value: DEFAULT_VALUE, inputs: DEFAULT_INPUTS, expression: "salary - tax", as_of: nil)
|
|
16
|
+
value = ActsAsCalculator::CastDecimal.(value)
|
|
17
|
+
|
|
18
|
+
ActsAsCalculator::Result.new(
|
|
19
|
+
value:,
|
|
20
|
+
breakdown: { expression:, inputs: inputs.to_h, value: },
|
|
21
|
+
formula_version: nil,
|
|
22
|
+
as_of: as_of || Date.current
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActsAsCalculatorEditor
|
|
4
|
+
# Form rows in, the `entries` list an import document's `lookup_tables` section takes out.
|
|
5
|
+
#
|
|
6
|
+
# `from` and `to` are nullable on both ends — an empty field means an unbounded bracket,
|
|
7
|
+
# not zero — so blanks become `nil` rather than being cast. `value` is required and is
|
|
8
|
+
# left as the operator typed it: `ImportLookupTable` runs every bound through
|
|
9
|
+
# `CastDecimal`, which is the one place a decimal is parsed.
|
|
10
|
+
class CastEntryRows
|
|
11
|
+
def self.call(...)
|
|
12
|
+
new(...).call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(rows)
|
|
16
|
+
@rows = rows
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
normalized_rows.filter_map { |row| entry(row) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :rows
|
|
26
|
+
|
|
27
|
+
def normalized_rows
|
|
28
|
+
given = plain(rows)
|
|
29
|
+
|
|
30
|
+
case given
|
|
31
|
+
when Array then given.map { |row| plain(row) }
|
|
32
|
+
when Hash then given.sort_by { |index, _| index.to_s.rjust(9, "0") }.map { |(_, row)| plain(row) }
|
|
33
|
+
else []
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def entry(row)
|
|
38
|
+
return nil unless row.is_a?(Hash)
|
|
39
|
+
return nil if %w[from to value].all? { |field| row[field].to_s.strip.empty? }
|
|
40
|
+
|
|
41
|
+
{ "from" => bound(row["from"]), "to" => bound(row["to"]), "value" => row["value"].to_s.strip }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def bound(value)
|
|
45
|
+
value.to_s.strip.presence
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def plain(value)
|
|
49
|
+
value.respond_to?(:to_unsafe_h) ? value.to_unsafe_h.to_h : value
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActsAsCalculatorEditor
|
|
4
|
+
# Lookup bounds and values are `decimal(20, 6)`, and a float round-trip loses precision
|
|
5
|
+
# above 15 significant digits — on numbers that are tax brackets. So they leave as JSON
|
|
6
|
+
# strings, which `CastDecimal` parses back without ceremony.
|
|
7
|
+
#
|
|
8
|
+
# `to_s("F")` rather than plain `to_s`, which renders `0.1` as `"0.1e0"`. The core gem
|
|
9
|
+
# makes the same choice writing decimals into `source_config` (`CastJsonSafe`), so an
|
|
10
|
+
# exported file and an imported one agree on what a decimal looks like.
|
|
11
|
+
class CastExportDecimal
|
|
12
|
+
def self.call(value)
|
|
13
|
+
return nil if value.nil?
|
|
14
|
+
return value.to_s("F") if value.is_a?(BigDecimal)
|
|
15
|
+
|
|
16
|
+
ActsAsCalculator::CastDecimal.(value).to_s("F")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|