active_canvas 0.0.4 → 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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +23 -1
  3. data/app/assets/javascripts/active_canvas/admin/data_panel.js +357 -0
  4. data/app/assets/javascripts/active_canvas/admin/field_builder.js +84 -0
  5. data/app/assets/javascripts/active_canvas/admin/media_select_preview.js +11 -0
  6. data/app/assets/javascripts/active_canvas/editor/ac_bindings.js +81 -0
  7. data/app/assets/javascripts/active_canvas/editor/ac_chips.js +192 -0
  8. data/app/assets/javascripts/active_canvas/editor/code_panel.js +10 -7
  9. data/app/assets/javascripts/active_canvas/editor/directive_traits.js +95 -0
  10. data/app/assets/javascripts/active_canvas/editor/live_data.js +178 -0
  11. data/app/assets/javascripts/active_canvas/editor/panels.js +23 -7
  12. data/app/assets/javascripts/active_canvas/editor/preview_modal.js +112 -0
  13. data/app/assets/javascripts/active_canvas/editor/template_validator.js +95 -0
  14. data/app/assets/javascripts/active_canvas/editor.js +21 -3
  15. data/app/assets/stylesheets/active_canvas/editor.css +626 -0
  16. data/app/controllers/active_canvas/admin/application_controller.rb +8 -0
  17. data/app/controllers/active_canvas/admin/collection_items_controller.rb +93 -0
  18. data/app/controllers/active_canvas/admin/collections_controller.rb +51 -0
  19. data/app/controllers/active_canvas/admin/form_submissions_controller.rb +51 -0
  20. data/app/controllers/active_canvas/admin/pages_controller.rb +127 -5
  21. data/app/controllers/active_canvas/form_submissions_controller.rb +90 -0
  22. data/app/controllers/active_canvas/pages_controller.rb +8 -0
  23. data/app/controllers/active_canvas/redirects_controller.rb +10 -0
  24. data/app/helpers/active_canvas/admin/collection_items_helper.rb +33 -0
  25. data/app/models/active_canvas/auto_drop.rb +69 -0
  26. data/app/models/active_canvas/collection.rb +98 -0
  27. data/app/models/active_canvas/collection_item.rb +66 -0
  28. data/app/models/active_canvas/collection_item_version.rb +18 -0
  29. data/app/models/active_canvas/current.rb +2 -0
  30. data/app/models/active_canvas/form_submission.rb +9 -0
  31. data/app/models/active_canvas/page.rb +76 -15
  32. data/app/models/active_canvas/page_redirect.rb +7 -0
  33. data/app/services/active_canvas/collection_schema.rb +110 -0
  34. data/app/services/active_canvas/collection_source.rb +81 -0
  35. data/app/services/active_canvas/content_sanitizer.rb +33 -21
  36. data/app/services/active_canvas/form_schema.rb +48 -0
  37. data/app/services/active_canvas/form_stamper.rb +86 -0
  38. data/app/services/active_canvas/template_renderer/binding_resolver.rb +138 -0
  39. data/app/services/active_canvas/template_renderer/directive_expander.rb +54 -0
  40. data/app/services/active_canvas/template_renderer.rb +160 -0
  41. data/app/views/active_canvas/admin/collection_items/_field_input.html.erb +40 -0
  42. data/app/views/active_canvas/admin/collection_items/_form.html.erb +28 -0
  43. data/app/views/active_canvas/admin/collection_items/edit.html.erb +24 -0
  44. data/app/views/active_canvas/admin/collection_items/history.html.erb +34 -0
  45. data/app/views/active_canvas/admin/collection_items/index.html.erb +63 -0
  46. data/app/views/active_canvas/admin/collection_items/new.html.erb +9 -0
  47. data/app/views/active_canvas/admin/collections/_field_builder.html.erb +35 -0
  48. data/app/views/active_canvas/admin/collections/_form.html.erb +31 -0
  49. data/app/views/active_canvas/admin/collections/edit.html.erb +10 -0
  50. data/app/views/active_canvas/admin/collections/index.html.erb +45 -0
  51. data/app/views/active_canvas/admin/collections/new.html.erb +9 -0
  52. data/app/views/active_canvas/admin/form_submissions/index.html.erb +80 -0
  53. data/app/views/active_canvas/admin/form_submissions/show.html.erb +99 -0
  54. data/app/views/active_canvas/admin/pages/_data_panel.html.erb +81 -0
  55. data/app/views/active_canvas/admin/pages/_form.html.erb +10 -0
  56. data/app/views/active_canvas/admin/pages/editor.html.erb +62 -1
  57. data/app/views/active_canvas/pages/show.html.erb +4 -1
  58. data/app/views/layouts/active_canvas/admin/application.html.erb +15 -0
  59. data/config/routes.rb +20 -0
  60. data/db/migrate/20260517100001_add_template_enabled_and_bindings_to_active_canvas_pages.rb +6 -0
  61. data/db/migrate/20260517100002_add_bindings_to_active_canvas_page_versions.rb +6 -0
  62. data/db/migrate/20260702000001_create_active_canvas_page_redirects.rb +11 -0
  63. data/db/migrate/20260702000002_create_active_canvas_form_submissions.rb +14 -0
  64. data/db/migrate/20260702000003_create_active_canvas_collections.rb +12 -0
  65. data/db/migrate/20260702000004_create_active_canvas_collection_items.rb +16 -0
  66. data/db/migrate/20260702000005_create_active_canvas_collection_item_versions.rb +17 -0
  67. data/lib/active_canvas/configuration.rb +32 -1
  68. data/lib/active_canvas/data_sources/errors.rb +51 -0
  69. data/lib/active_canvas/data_sources/param_spec.rb +64 -0
  70. data/lib/active_canvas/data_sources/registration.rb +60 -0
  71. data/lib/active_canvas/data_sources/source.rb +47 -0
  72. data/lib/active_canvas/data_sources.rb +54 -0
  73. data/lib/active_canvas/engine.rb +15 -0
  74. data/lib/active_canvas/version.rb +1 -1
  75. data/lib/active_canvas.rb +6 -0
  76. metadata +85 -3
@@ -0,0 +1,45 @@
1
+ <% content_for :page_title, "Collections" %>
2
+
3
+ <div class="page-header">
4
+ <h2>Collections</h2>
5
+ <%= link_to "New collection", new_admin_collection_path, class: "btn btn-primary" %>
6
+ </div>
7
+
8
+ <div class="card">
9
+ <% if @collections.any? %>
10
+ <table>
11
+ <thead>
12
+ <tr>
13
+ <th>Name</th>
14
+ <th>Slug</th>
15
+ <th>Fields</th>
16
+ <th>Items</th>
17
+ <th></th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <% @collections.each do |collection| %>
22
+ <tr>
23
+ <td><%= collection.name %></td>
24
+ <td><code><%= collection.slug %></code></td>
25
+ <td><%= collection.fields.size %></td>
26
+ <td><%= collection.items.count %></td>
27
+ <td class="actions">
28
+ <%= link_to "Items", admin_collection_items_path(collection), class: "btn btn-sm btn-secondary" %>
29
+ <%= link_to "Edit", edit_admin_collection_path(collection), class: "btn btn-sm btn-secondary" %>
30
+ <%= button_to "Delete", admin_collection_path(collection), method: :delete,
31
+ class: "btn btn-sm btn-danger", form: { style: "display:inline" },
32
+ onclick: "return confirm('Delete this collection and all its items?')" %>
33
+ </td>
34
+ </tr>
35
+ <% end %>
36
+ </tbody>
37
+ </table>
38
+ <% else %>
39
+ <div class="empty-state">
40
+ <h3>No collections yet</h3>
41
+ <p>Create a collection to manage typed content you can loop over in pages.</p>
42
+ <%= link_to "New collection", new_admin_collection_path, class: "btn btn-primary" %>
43
+ </div>
44
+ <% end %>
45
+ </div>
@@ -0,0 +1,9 @@
1
+ <% content_for :page_title, "New collection" %>
2
+
3
+ <div class="page-header">
4
+ <h2>New collection</h2>
5
+ </div>
6
+
7
+ <div class="card" style="padding:1.5rem;">
8
+ <%= render "form" %>
9
+ </div>
@@ -0,0 +1,80 @@
1
+ <% content_for :page_title, "Form Submissions" %>
2
+
3
+ <!-- Page Header -->
4
+ <div class="page-header">
5
+ <div class="page-header-left">
6
+ <h2>Form Submissions</h2>
7
+ <p class="page-header-subtitle">Browse and export submissions from your page forms</p>
8
+ </div>
9
+ </div>
10
+
11
+ <!-- Filters -->
12
+ <div class="card" style="margin-bottom: 1.5rem;">
13
+ <div class="card-body">
14
+ <%= form_with url: admin_form_submissions_path, method: :get, local: true do %>
15
+ <div style="display: flex; gap: 0.75rem; align-items: flex-end; flex-wrap: wrap;">
16
+ <label style="display: flex; flex-direction: column; gap: 0.375rem;">
17
+ <span style="font-size: 0.75rem; font-weight: 600; color: var(--text-muted);">Page</span>
18
+ <%= select_tag :page_id,
19
+ options_from_collection_for_select(ActiveCanvas::Page.order(:title), :id, :title, params[:page_id]),
20
+ include_blank: "All pages", class: "form-control" %>
21
+ </label>
22
+ <label style="display: flex; flex-direction: column; gap: 0.375rem;">
23
+ <span style="font-size: 0.75rem; font-weight: 600; color: var(--text-muted);">Form</span>
24
+ <% form_key_scope = params[:page_id].present? ? ActiveCanvas::FormSubmission.where(page_id: params[:page_id]) : ActiveCanvas::FormSubmission %>
25
+ <%= select_tag :form_key,
26
+ options_for_select(form_key_scope.distinct.pluck(:form_key).sort, params[:form_key]),
27
+ include_blank: "All forms", class: "form-control" %>
28
+ </label>
29
+ <%= submit_tag "Filter", class: "btn btn-secondary" %>
30
+ <%= link_to "Export CSV", admin_form_submissions_path(format: :csv, page_id: params[:page_id], form_key: params[:form_key]), class: "btn btn-secondary" %>
31
+ </div>
32
+ <% end %>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Submissions Table -->
37
+ <% if @submissions.any? %>
38
+ <div class="card">
39
+ <table>
40
+ <thead>
41
+ <tr>
42
+ <th>Page</th>
43
+ <th>Form</th>
44
+ <th>Data</th>
45
+ <th>IP</th>
46
+ <th>Received</th>
47
+ <th></th>
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ <% @submissions.each do |submission| %>
52
+ <tr>
53
+ <td><%= submission.page.title %></td>
54
+ <td><span class="badge badge-gray"><%= submission.form_key %></span></td>
55
+ <td><%= truncate(submission.data.map { |k, v| "#{k}: #{v}" }.join(" · "), length: 80) %></td>
56
+ <td><code><%= submission.ip %></code></td>
57
+ <td><span style="color: var(--text-muted);"><%= submission.created_at.strftime("%b %d, %Y at %l:%M %p") %></span></td>
58
+ <td class="actions">
59
+ <%= link_to "View", admin_form_submission_path(submission), class: "btn btn-secondary btn-sm" %>
60
+ </td>
61
+ </tr>
62
+ <% end %>
63
+ </tbody>
64
+ </table>
65
+ </div>
66
+ <% if @submissions.size == ActiveCanvas::Admin::FormSubmissionsController::MAX_ROWS %>
67
+ <p class="help-text" style="margin-top: 0.75rem;">Showing the most recent <%= ActiveCanvas::Admin::FormSubmissionsController::MAX_ROWS %> submissions.</p>
68
+ <% end %>
69
+ <% else %>
70
+ <div class="card">
71
+ <div class="empty-state">
72
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
73
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
74
+ <polyline points="22,6 12,13 2,6"/>
75
+ </svg>
76
+ <h3>No submissions yet</h3>
77
+ <p>Submissions from your published page forms will appear here</p>
78
+ </div>
79
+ </div>
80
+ <% end %>
@@ -0,0 +1,99 @@
1
+ <% content_for :page_title, "#{@submission.form_key} — #{@submission.page.title}" %>
2
+
3
+ <!-- Page Header -->
4
+ <div class="page-header">
5
+ <div class="page-header-left">
6
+ <h2><%= @submission.form_key %> — <%= @submission.page.title %></h2>
7
+ <p class="page-header-subtitle">Received <%= time_ago_in_words(@submission.created_at) %> ago</p>
8
+ </div>
9
+ <div class="page-header-actions">
10
+ <%= link_to admin_form_submissions_path, class: "btn btn-ghost" do %>
11
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
12
+ <line x1="19" y1="12" x2="5" y2="12"/>
13
+ <polyline points="12 19 5 12 12 5"/>
14
+ </svg>
15
+ Back to Submissions
16
+ <% end %>
17
+ </div>
18
+ </div>
19
+
20
+ <!-- Submitted Fields -->
21
+ <div class="card" style="margin-bottom: 1.5rem;">
22
+ <div class="card-header">
23
+ <span class="card-title">Submitted Fields</span>
24
+ </div>
25
+ <div class="card-body" style="padding: 0;">
26
+ <% if @submission.data.any? %>
27
+ <% @submission.data.each do |key, value| %>
28
+ <div class="detail-row">
29
+ <div class="detail-label"><%= key %></div>
30
+ <div class="detail-value" style="white-space: pre-wrap;"><%= value %></div>
31
+ </div>
32
+ <% end %>
33
+ <% else %>
34
+ <div class="detail-row">
35
+ <div class="detail-value" style="color: var(--text-muted);">No fields were submitted.</div>
36
+ </div>
37
+ <% end %>
38
+ </div>
39
+ </div>
40
+
41
+ <!-- Metadata -->
42
+ <div class="card" style="margin-bottom: 1.5rem;">
43
+ <div class="card-header">
44
+ <span class="card-title">Details</span>
45
+ </div>
46
+ <div class="card-body" style="padding: 0;">
47
+ <div class="detail-row">
48
+ <div class="detail-label">Received</div>
49
+ <div class="detail-value"><%= @submission.created_at.strftime("%B %d, %Y at %l:%M %p") %></div>
50
+ </div>
51
+ <div class="detail-row">
52
+ <div class="detail-label">IP Address</div>
53
+ <div class="detail-value"><code><%= @submission.ip %></code></div>
54
+ </div>
55
+ <div class="detail-row">
56
+ <div class="detail-label">User Agent</div>
57
+ <div class="detail-value" style="word-break: break-all;"><%= @submission.user_agent %></div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+
62
+ <!-- Danger Zone -->
63
+ <div class="card" style="border-color: #fecaca;">
64
+ <div class="card-header" style="background: var(--danger-light);">
65
+ <span class="card-title" style="color: var(--danger);">Danger Zone</span>
66
+ </div>
67
+ <div class="card-body">
68
+ <p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem;">
69
+ Deleting this submission removes it permanently.
70
+ </p>
71
+ <%= button_to "Delete Submission", admin_form_submission_path(@submission), method: :delete,
72
+ class: "btn btn-danger", data: { turbo_confirm: "Delete this submission?" } %>
73
+ </div>
74
+ </div>
75
+
76
+ <style>
77
+ .detail-row {
78
+ padding: 0.875rem 1.25rem;
79
+ border-bottom: 1px solid var(--border);
80
+ }
81
+
82
+ .detail-row:last-child {
83
+ border-bottom: none;
84
+ }
85
+
86
+ .detail-row .detail-label {
87
+ font-size: 0.6875rem;
88
+ text-transform: uppercase;
89
+ letter-spacing: 0.05em;
90
+ color: var(--text-muted);
91
+ font-weight: 600;
92
+ margin-bottom: 0.25rem;
93
+ }
94
+
95
+ .detail-row .detail-value {
96
+ font-size: 0.875rem;
97
+ color: var(--text);
98
+ }
99
+ </style>
@@ -0,0 +1,81 @@
1
+ <%# app/views/active_canvas/admin/pages/_data_panel.html.erb %>
2
+ <div id="data-panel-container" class="data-panel" style="display: none;"
3
+ data-page-id="<%= @page.id %>"
4
+ data-template-enabled="<%= @page.template_enabled? %>"
5
+ data-data-sources-url="<%= data_sources_admin_pages_path(format: :json) %>"
6
+ data-validate-url="<%= validate_template_admin_page_path(@page) %>"
7
+ data-sample-data-url="<%= sample_data_admin_page_path(@page) %>"
8
+ data-chip-values-url="<%= chip_values_admin_page_path(@page) %>">
9
+
10
+ <div class="data-panel-header">
11
+ <span class="data-panel-title">Bindings</span>
12
+ <button type="button" id="ac-add-binding-btn" class="data-panel-add-btn" title="Add a new binding">
13
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
14
+ <line x1="12" y1="5" x2="12" y2="19"/>
15
+ <line x1="5" y1="12" x2="19" y2="12"/>
16
+ </svg>
17
+ Add
18
+ </button>
19
+ </div>
20
+
21
+ <p class="data-panel-message" id="ac-panel-message" hidden></p>
22
+
23
+ <label class="data-panel-toggle">
24
+ <input type="checkbox" id="ac-template-toggle" <%= "checked" if @page.template_enabled? %>>
25
+ <span>Render dynamic content (Liquid)</span>
26
+ </label>
27
+ <p class="data-panel-notice" id="ac-template-notice" <%= "hidden" if @page.template_enabled? %>>
28
+ Dynamic rendering is off. Bindings and <code>{{ tags }}</code> are saved but visitors see them as plain text until you turn it on.
29
+ </p>
30
+
31
+ <ul id="ac-bindings-list" class="data-panel-list" data-empty="true"></ul>
32
+
33
+ <div class="data-panel-empty" id="ac-bindings-empty">
34
+ <div class="data-panel-empty-icon">
35
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
36
+ <ellipse cx="12" cy="5" rx="9" ry="3"/>
37
+ <path d="M3 5v14a9 3 0 0 0 18 0V5"/>
38
+ <path d="M3 12a9 3 0 0 0 18 0"/>
39
+ </svg>
40
+ </div>
41
+ <h3>No bindings yet</h3>
42
+ <p>Connect a data source to use <code>&#123;&#123; variables &#125;&#125;</code> in your page.</p>
43
+ <button type="button" id="ac-empty-add-btn" class="data-panel-empty-btn">
44
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
45
+ <line x1="12" y1="5" x2="12" y2="19"/>
46
+ <line x1="5" y1="12" x2="19" y2="12"/>
47
+ </svg>
48
+ Add binding
49
+ </button>
50
+ </div>
51
+
52
+ <p class="data-panel-help">Tip: click a chip to copy the snippet to your clipboard.</p>
53
+
54
+ <template id="ac-binding-form-template">
55
+ <form id="ac-binding-form" class="data-panel-add-card">
56
+ <div class="data-panel-add-card-header">
57
+ <span class="data-panel-add-card-title">New binding</span>
58
+ <button type="button" id="ac-binding-cancel" class="data-panel-add-card-close" title="Cancel">
59
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
60
+ <line x1="18" y1="6" x2="6" y2="18"/>
61
+ <line x1="6" y1="6" x2="18" y2="18"/>
62
+ </svg>
63
+ </button>
64
+ </div>
65
+ <label class="data-panel-field">
66
+ <span class="data-panel-field-label">Name</span>
67
+ <input type="text" name="name" required autocomplete="off" placeholder="articles">
68
+ <span class="data-panel-field-error" id="ac-binding-name-error" hidden></span>
69
+ </label>
70
+ <label class="data-panel-field">
71
+ <span class="data-panel-field-label">Source</span>
72
+ <select name="source"></select>
73
+ </label>
74
+ <div id="ac-binding-params" class="data-panel-add-card-params"></div>
75
+ <div class="data-panel-add-card-actions">
76
+ <button type="button" class="data-panel-btn data-panel-btn-ghost" data-cancel>Cancel</button>
77
+ <button type="submit" class="data-panel-btn data-panel-btn-primary">Save binding</button>
78
+ </div>
79
+ </form>
80
+ </template>
81
+ </div>
@@ -56,6 +56,16 @@
56
56
  </div>
57
57
  </div>
58
58
 
59
+ <div class="form-group">
60
+ <div class="form-check" style="background: var(--bg-main, #f8fafc); padding: 1rem; border-radius: 0.5rem; border: 1px solid var(--border, #e2e8f0);">
61
+ <%= form.check_box :template_enabled, id: "page_template_enabled" %>
62
+ <label for="page_template_enabled" style="font-weight: 500;">
63
+ Render dynamic content (Liquid)
64
+ </label>
65
+ <p class="help-text" style="margin-left: 1.5rem; margin-top: 0.25rem;">Enable Liquid template rendering for dynamic content and data bindings.</p>
66
+ </div>
67
+ </div>
68
+
59
69
  <div class="form-actions">
60
70
  <%= form.submit class: "btn btn-primary" %>
61
71
  <%= link_to "Cancel", admin_pages_path, class: "btn btn-secondary" %>
@@ -149,6 +149,21 @@
149
149
  </div>
150
150
  </div>
151
151
  <div class="header-separator"></div>
152
+ <button class="header-btn header-btn-secondary" id="btn-live-data" title="Show the real values inside the canvas" aria-pressed="false">
153
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
154
+ <ellipse cx="12" cy="5" rx="9" ry="3"/>
155
+ <path d="M3 5v14a9 3 0 0 0 18 0V5"/>
156
+ <path d="M3 12a9 3 0 0 0 18 0"/>
157
+ </svg>
158
+ Live data
159
+ </button>
160
+ <button class="header-btn header-btn-secondary" id="btn-preview" title="Preview (Ctrl+Shift+P)" data-preview-url="<%= preview_iframe_admin_page_path(@page) %>">
161
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
162
+ <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/>
163
+ <circle cx="12" cy="12" r="3"/>
164
+ </svg>
165
+ Preview
166
+ </button>
152
167
  <button class="header-btn header-btn-primary" id="btn-save">
153
168
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
154
169
  <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
@@ -170,6 +185,7 @@
170
185
  <button class="panel-tab active" data-panel="blocks">Blocks</button>
171
186
  <button class="panel-tab" data-panel="assets">Assets</button>
172
187
  <button class="panel-tab" data-panel="layers">Layers</button>
188
+ <button class="panel-tab" data-panel="data">Data</button>
173
189
  </div>
174
190
  <div class="panel-content">
175
191
  <div id="blocks-container"></div>
@@ -199,6 +215,7 @@
199
215
  </div>
200
216
  </div>
201
217
  <div id="layers-container" style="display: none;"></div>
218
+ <%= render "data_panel" %>
202
219
  </div>
203
220
  </div>
204
221
 
@@ -576,6 +593,41 @@
576
593
  </div>
577
594
  </div>
578
595
 
596
+ <!-- Preview Modal (full-screen iframe preview) -->
597
+ <div class="preview-modal" id="preview-modal" hidden>
598
+ <div class="preview-modal-toolbar">
599
+ <div class="preview-modal-title">
600
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
601
+ <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/>
602
+ <circle cx="12" cy="12" r="3"/>
603
+ </svg>
604
+ <span>Preview · <strong><%= @page.title %></strong></span>
605
+ </div>
606
+ <button class="preview-modal-close" id="btn-close-preview" title="Close (Esc)">
607
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
608
+ <line x1="18" y1="6" x2="6" y2="18"/>
609
+ <line x1="6" y1="6" x2="18" y2="18"/>
610
+ </svg>
611
+ </button>
612
+ </div>
613
+ <div class="preview-modal-body">
614
+ <iframe class="preview-modal-iframe" id="preview-modal-iframe" title="Page preview" sandbox="allow-scripts allow-forms"></iframe>
615
+ <div class="preview-modal-loading" id="preview-modal-loading">
616
+ <div class="preview-modal-spinner"></div>
617
+ <span>Rendering preview…</span>
618
+ </div>
619
+ <div class="preview-modal-error" id="preview-modal-error" hidden>
620
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
621
+ <circle cx="12" cy="12" r="10"/>
622
+ <line x1="12" y1="8" x2="12" y2="12"/>
623
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
624
+ </svg>
625
+ <h3>Preview failed</h3>
626
+ <p id="preview-modal-error-message"></p>
627
+ </div>
628
+ </div>
629
+ </div>
630
+
579
631
  <!-- Toast Notification -->
580
632
  <div id="editor-toast" class="editor-toast"></div>
581
633
 
@@ -599,13 +651,14 @@
599
651
  window.ActiveCanvasEditor = {
600
652
  config: {
601
653
  pageId: <%= @page.id %>,
654
+ templateEnabled: <%= @page.template_enabled? %>,
602
655
  saveUrl: '<%= save_editor_admin_page_path(@page) %>',
603
656
  mediaUrl: '<%= admin_media_path(format: :json) %>',
604
657
  uploadUrl: '<%= admin_media_path %>',
605
658
  content: <%= raw (@page.content || '').to_json %>,
606
659
  contentCss: <%= raw (@page.content_css || '').to_json %>,
607
660
  contentJs: <%= raw (@page.content_js || '').to_json %>,
608
- contentComponents: <%= raw (@page.content_components || '').to_json %>,
661
+ contentComponents: <%= raw (@page.template_enabled? ? '' : (@page.content_components || '')).to_json %>,
609
662
  cssFramework: '<%= ActiveCanvas::Setting.css_framework %>',
610
663
  cssFrameworkUrl: <%= raw (ActiveCanvas::Setting.css_framework_url || '').to_json %>,
611
664
  cssFrameworkType: '<%= ActiveCanvas::Setting.css_framework_type %>',
@@ -640,7 +693,15 @@
640
693
  <%= javascript_include_tag "active_canvas/editor/component_toolbar" %>
641
694
  <%= javascript_include_tag "active_canvas/editor/panels" %>
642
695
  <%= javascript_include_tag "active_canvas/editor/ai_panel" %>
696
+ <%= javascript_include_tag "active_canvas/editor/preview_modal" %>
697
+ <script id="ac-server-bindings" type="application/json"><%= json_escape(@page.bindings.to_json).html_safe %></script>
698
+ <%= javascript_include_tag "active_canvas/editor/ac_bindings" %>
699
+ <%= javascript_include_tag "active_canvas/editor/ac_chips" %>
700
+ <%= javascript_include_tag "active_canvas/editor/directive_traits" %>
701
+ <%= javascript_include_tag "active_canvas/editor/template_validator" %>
702
+ <%= javascript_include_tag "active_canvas/editor/live_data" %>
643
703
  <%= javascript_include_tag "active_canvas/editor" %>
704
+ <%= javascript_include_tag "active_canvas/admin/data_panel" %>
644
705
 
645
706
  <!-- Theme Switcher JavaScript -->
646
707
  <script>
@@ -93,7 +93,10 @@
93
93
  <% end %>
94
94
 
95
95
  <%# Main Page Content %>
96
- <%= @page.rendered_content %>
96
+ <%= @page.rendered_content(
97
+ form_feedback: { submitted: params[:ac_submitted], error: params[:ac_form_error], form_key: params[:ac_form] },
98
+ form_action: public_form_submissions_path
99
+ ) %>
97
100
 
98
101
  <%# Footer %>
99
102
  <% if footer_partial&.content.present? %>
@@ -877,6 +877,21 @@
877
877
  </svg>
878
878
  <span>Header & Footer</span>
879
879
  <% end %>
880
+ <%= link_to admin_form_submissions_path, class: "nav-link #{controller_name == 'form_submissions' ? 'active' : ''}" do %>
881
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
882
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
883
+ <polyline points="22,6 12,13 2,6"/>
884
+ </svg>
885
+ <span>Form submissions</span>
886
+ <% end %>
887
+ <%= link_to admin_collections_path, class: "nav-link #{controller_name.in?(%w[collections collection_items]) ? 'active' : ''}" do %>
888
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
889
+ <path d="M12 2 2 7l10 5 10-5-10-5Z"/>
890
+ <path d="m2 17 10 5 10-5"/>
891
+ <path d="m2 12 10 5 10-5"/>
892
+ </svg>
893
+ <span>Collections</span>
894
+ <% end %>
880
895
  </div>
881
896
 
882
897
  <div class="nav-section">
data/config/routes.rb CHANGED
@@ -1,12 +1,19 @@
1
1
  ActiveCanvas::Engine.routes.draw do
2
2
  namespace :admin do
3
3
  resources :pages do
4
+ collection do
5
+ get :data_sources
6
+ end
4
7
  member do
5
8
  get :content
6
9
  patch :update_content
7
10
  get :editor
8
11
  patch :save_editor
9
12
  get :versions
13
+ post :validate_template
14
+ post :sample_data
15
+ post :chip_values
16
+ post :preview_iframe
10
17
  end
11
18
  resources :versions, only: [:show], controller: "page_versions"
12
19
  end
@@ -18,6 +25,16 @@ ActiveCanvas::Engine.routes.draw do
18
25
  end
19
26
  end
20
27
  resources :media, only: [:index, :show, :create, :destroy]
28
+ resources :form_submissions, only: [ :index, :show, :destroy ]
29
+ resources :collections do
30
+ resources :items, controller: "collection_items", only: %i[index new create edit update destroy] do
31
+ member do
32
+ patch :publish
33
+ patch :unpublish
34
+ get :history
35
+ end
36
+ end
37
+ end
21
38
  resource :settings, only: [:show, :update] do
22
39
  patch :update_global_css
23
40
  patch :update_global_js
@@ -44,6 +61,9 @@ ActiveCanvas::Engine.routes.draw do
44
61
  end
45
62
 
46
63
  root to: "pages#home"
64
+ post "forms", to: "form_submissions#create", as: :public_form_submissions
47
65
  get ":slug", to: "pages#show", as: :public_page,
48
66
  constraints: ->(req) { ActiveCanvas::Page.published.exists?(slug: req.params[:slug]) }
67
+ get ":slug", to: "redirects#show", as: :page_redirect,
68
+ constraints: ->(req) { ActiveCanvas::PageRedirect.exists?(from_slug: req.params[:slug]) }
49
69
  end
@@ -0,0 +1,6 @@
1
+ class AddTemplateEnabledAndBindingsToActiveCanvasPages < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :active_canvas_pages, :template_enabled, :boolean, default: false, null: false
4
+ add_column :active_canvas_pages, :bindings, :json, default: {}, null: false
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class AddBindingsToActiveCanvasPageVersions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :active_canvas_page_versions, :bindings_before, :json
4
+ add_column :active_canvas_page_versions, :bindings_after, :json
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ class CreateActiveCanvasPageRedirects < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :active_canvas_page_redirects do |t|
4
+ t.string :from_slug, null: false
5
+ t.references :page, null: false, foreign_key: { to_table: :active_canvas_pages }
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :active_canvas_page_redirects, :from_slug, unique: true
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ class CreateActiveCanvasFormSubmissions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :active_canvas_form_submissions do |t|
4
+ t.references :page, null: false, index: false, foreign_key: { to_table: :active_canvas_pages }
5
+ t.string :form_key, null: false
6
+ t.json :data, null: false, default: {}
7
+ t.string :ip
8
+ t.string :user_agent
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :active_canvas_form_submissions, [ :page_id, :form_key ]
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ class CreateActiveCanvasCollections < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :active_canvas_collections do |t|
4
+ t.string :name, null: false
5
+ t.string :slug, null: false
6
+ t.json :fields, null: false, default: []
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :active_canvas_collections, :slug, unique: true
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ class CreateActiveCanvasCollectionItems < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :active_canvas_collection_items do |t|
4
+ t.references :collection, null: false, index: false,
5
+ foreign_key: { to_table: :active_canvas_collections }
6
+ t.json :data, null: false, default: {}
7
+ t.json :draft_data, null: false, default: {}
8
+ t.string :status, null: false, default: "draft"
9
+ t.string :slug
10
+ t.datetime :published_at
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :active_canvas_collection_items, [ :collection_id, :status ]
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ class CreateActiveCanvasCollectionItemVersions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :active_canvas_collection_item_versions do |t|
4
+ t.references :collection_item, null: false, index: false,
5
+ foreign_key: { to_table: :active_canvas_collection_items }
6
+ t.integer :version_number, null: false
7
+ t.json :data, null: false, default: {}
8
+ t.string :changed_by
9
+ t.string :change_summary
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :active_canvas_collection_item_versions,
14
+ [ :collection_item_id, :version_number ], unique: true,
15
+ name: "idx_ac_collection_item_versions_on_item_and_number"
16
+ end
17
+ end