active_canvas 0.0.1
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 +318 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/active_canvas/editor/ai_panel.js +1607 -0
- data/app/assets/javascripts/active_canvas/editor/asset_manager.js +498 -0
- data/app/assets/javascripts/active_canvas/editor/blocks.js +1083 -0
- data/app/assets/javascripts/active_canvas/editor/code_panel.js +572 -0
- data/app/assets/javascripts/active_canvas/editor/component_toolbar.js +394 -0
- data/app/assets/javascripts/active_canvas/editor/panels.js +460 -0
- data/app/assets/javascripts/active_canvas/editor/utils.js +56 -0
- data/app/assets/javascripts/active_canvas/editor.js +295 -0
- data/app/assets/stylesheets/active_canvas/application.css +15 -0
- data/app/assets/stylesheets/active_canvas/editor.css +2929 -0
- data/app/controllers/active_canvas/admin/ai_controller.rb +181 -0
- data/app/controllers/active_canvas/admin/application_controller.rb +56 -0
- data/app/controllers/active_canvas/admin/media_controller.rb +61 -0
- data/app/controllers/active_canvas/admin/page_types_controller.rb +57 -0
- data/app/controllers/active_canvas/admin/page_versions_controller.rb +23 -0
- data/app/controllers/active_canvas/admin/pages_controller.rb +133 -0
- data/app/controllers/active_canvas/admin/partials_controller.rb +88 -0
- data/app/controllers/active_canvas/admin/settings_controller.rb +256 -0
- data/app/controllers/active_canvas/application_controller.rb +20 -0
- data/app/controllers/active_canvas/pages_controller.rb +18 -0
- data/app/controllers/concerns/active_canvas/current_user.rb +12 -0
- data/app/controllers/concerns/active_canvas/rate_limitable.rb +75 -0
- data/app/controllers/concerns/active_canvas/tailwind_compilation.rb +39 -0
- data/app/helpers/active_canvas/application_helper.rb +4 -0
- data/app/jobs/active_canvas/application_job.rb +4 -0
- data/app/jobs/active_canvas/compile_tailwind_job.rb +64 -0
- data/app/mailers/active_canvas/application_mailer.rb +6 -0
- data/app/models/active_canvas/ai_model.rb +136 -0
- data/app/models/active_canvas/application_record.rb +5 -0
- data/app/models/active_canvas/media.rb +141 -0
- data/app/models/active_canvas/page.rb +85 -0
- data/app/models/active_canvas/page_type.rb +22 -0
- data/app/models/active_canvas/page_version.rb +80 -0
- data/app/models/active_canvas/partial.rb +73 -0
- data/app/models/active_canvas/setting.rb +292 -0
- data/app/services/active_canvas/ai_configuration.rb +40 -0
- data/app/services/active_canvas/ai_models.rb +128 -0
- data/app/services/active_canvas/ai_service.rb +289 -0
- data/app/services/active_canvas/content_sanitizer.rb +112 -0
- data/app/services/active_canvas/tailwind_compiler.rb +156 -0
- data/app/views/active_canvas/admin/media/index.html.erb +401 -0
- data/app/views/active_canvas/admin/media/show.html.erb +297 -0
- data/app/views/active_canvas/admin/page_types/_form.html.erb +25 -0
- data/app/views/active_canvas/admin/page_types/edit.html.erb +13 -0
- data/app/views/active_canvas/admin/page_types/index.html.erb +29 -0
- data/app/views/active_canvas/admin/page_types/new.html.erb +9 -0
- data/app/views/active_canvas/admin/page_types/show.html.erb +18 -0
- data/app/views/active_canvas/admin/page_versions/show.html.erb +469 -0
- data/app/views/active_canvas/admin/pages/_form.html.erb +62 -0
- data/app/views/active_canvas/admin/pages/content.html.erb +139 -0
- data/app/views/active_canvas/admin/pages/edit.html.erb +335 -0
- data/app/views/active_canvas/admin/pages/editor.html.erb +710 -0
- data/app/views/active_canvas/admin/pages/index.html.erb +149 -0
- data/app/views/active_canvas/admin/pages/new.html.erb +19 -0
- data/app/views/active_canvas/admin/pages/show.html.erb +258 -0
- data/app/views/active_canvas/admin/pages/versions.html.erb +333 -0
- data/app/views/active_canvas/admin/partials/edit.html.erb +182 -0
- data/app/views/active_canvas/admin/partials/editor.html.erb +703 -0
- data/app/views/active_canvas/admin/partials/index.html.erb +131 -0
- data/app/views/active_canvas/admin/settings/show.html.erb +1864 -0
- data/app/views/active_canvas/pages/no_homepage.html.erb +45 -0
- data/app/views/active_canvas/pages/show.html.erb +113 -0
- data/app/views/layouts/active_canvas/admin/application.html.erb +960 -0
- data/app/views/layouts/active_canvas/admin/editor.html.erb +826 -0
- data/app/views/layouts/active_canvas/application.html.erb +55 -0
- data/config/routes.rb +48 -0
- data/db/migrate/20260202000001_create_active_canvas_tables.rb +113 -0
- data/db/migrate/20260202000002_create_active_canvas_ai_models.rb +26 -0
- data/lib/active_canvas/configuration.rb +232 -0
- data/lib/active_canvas/engine.rb +44 -0
- data/lib/active_canvas/version.rb +3 -0
- data/lib/active_canvas.rb +26 -0
- data/lib/generators/active_canvas/install/install_generator.rb +263 -0
- data/lib/generators/active_canvas/install/templates/initializer.rb.tt +163 -0
- data/lib/tasks/active_canvas_tasks.rake +69 -0
- metadata +150 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
module ActiveCanvas
|
|
2
|
+
module Admin
|
|
3
|
+
class AiController < ApplicationController
|
|
4
|
+
include ActionController::Live
|
|
5
|
+
include ActiveCanvas::RateLimitable
|
|
6
|
+
|
|
7
|
+
# Use null_session for JSON requests (allows CSRF token via header)
|
|
8
|
+
protect_from_forgery with: :null_session, if: -> { request.format.json? }
|
|
9
|
+
|
|
10
|
+
before_action :ensure_ai_configured, except: %i[status models]
|
|
11
|
+
before_action :verify_request_origin, only: %i[chat]
|
|
12
|
+
before_action :check_ai_rate_limit, only: %i[chat image screenshot_to_code]
|
|
13
|
+
|
|
14
|
+
def chat
|
|
15
|
+
unless AiConfiguration.text_enabled?
|
|
16
|
+
return render json: { error: "Text generation is disabled" }, status: :forbidden
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
response.headers["Content-Type"] = "text/event-stream"
|
|
20
|
+
response.headers["Cache-Control"] = "no-cache"
|
|
21
|
+
response.headers["X-Accel-Buffering"] = "no"
|
|
22
|
+
response.headers["Connection"] = "keep-alive"
|
|
23
|
+
|
|
24
|
+
config = ActiveCanvas.config
|
|
25
|
+
start_time = Time.current
|
|
26
|
+
last_write = Time.current
|
|
27
|
+
total_bytes = 0
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
AiService.generate_text(
|
|
31
|
+
prompt: params[:prompt],
|
|
32
|
+
model: params[:model],
|
|
33
|
+
context: build_context
|
|
34
|
+
) do |chunk|
|
|
35
|
+
# Check stream timeout
|
|
36
|
+
if Time.current - start_time > config.ai_stream_timeout
|
|
37
|
+
write_sse_event("error", { error: "Stream timeout exceeded" })
|
|
38
|
+
break
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check idle timeout
|
|
42
|
+
if Time.current - last_write > config.ai_stream_idle_timeout
|
|
43
|
+
write_sse_event("error", { error: "Idle timeout - no data received" })
|
|
44
|
+
break
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check response size limit
|
|
48
|
+
chunk_content = chunk.content.to_s
|
|
49
|
+
chunk_size = chunk_content.bytesize
|
|
50
|
+
total_bytes += chunk_size
|
|
51
|
+
|
|
52
|
+
if total_bytes > config.ai_max_response_size
|
|
53
|
+
write_sse_event("error", { error: "Response too large" })
|
|
54
|
+
break
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
write_sse_event("chunk", { content: chunk_content })
|
|
58
|
+
last_write = Time.current
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
write_sse_event("done", { total_bytes: total_bytes })
|
|
62
|
+
rescue IOError => e
|
|
63
|
+
# Client disconnected, this is normal
|
|
64
|
+
Rails.logger.debug "AI Chat client disconnected: #{e.message}"
|
|
65
|
+
rescue => e
|
|
66
|
+
Rails.logger.error "AI Chat Error: #{e.class.name}: #{e.message}"
|
|
67
|
+
begin
|
|
68
|
+
write_sse_event("error", { error: e.message })
|
|
69
|
+
rescue IOError
|
|
70
|
+
# Client already disconnected
|
|
71
|
+
end
|
|
72
|
+
ensure
|
|
73
|
+
response.stream.close rescue nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def image
|
|
78
|
+
unless AiConfiguration.image_enabled?
|
|
79
|
+
return render json: { error: "Image generation is disabled" }, status: :forbidden
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
media = AiService.generate_image(
|
|
83
|
+
prompt: params[:prompt],
|
|
84
|
+
model: params[:model]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
render json: {
|
|
88
|
+
success: true,
|
|
89
|
+
image: media.as_json_for_editor,
|
|
90
|
+
url: media.url
|
|
91
|
+
}
|
|
92
|
+
rescue => e
|
|
93
|
+
Rails.logger.error "AI Image Error: #{e.message}"
|
|
94
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def screenshot_to_code
|
|
98
|
+
unless AiConfiguration.screenshot_enabled?
|
|
99
|
+
return render json: { error: "Screenshot to code is disabled" }, status: :forbidden
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
unless params[:screenshot].present?
|
|
103
|
+
return render json: { error: "No screenshot provided" }, status: :bad_request
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
html = AiService.screenshot_to_code(
|
|
107
|
+
image_data: params[:screenshot],
|
|
108
|
+
model: params[:model],
|
|
109
|
+
additional_prompt: params[:additional_prompt]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
render json: { success: true, html: html }
|
|
113
|
+
rescue => e
|
|
114
|
+
Rails.logger.error "AI Screenshot Error: #{e.message}"
|
|
115
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def models
|
|
119
|
+
text = AiModels.text_models
|
|
120
|
+
image = AiModels.image_models
|
|
121
|
+
vision = AiModels.vision_models
|
|
122
|
+
|
|
123
|
+
unless params[:debug]
|
|
124
|
+
strip = ->(list) { list.map { |m| m.except(:input_modalities, :output_modalities, "input_modalities", "output_modalities") } }
|
|
125
|
+
text = strip.call(text)
|
|
126
|
+
image = strip.call(image)
|
|
127
|
+
vision = strip.call(vision)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
render json: {
|
|
131
|
+
text: text,
|
|
132
|
+
image: image,
|
|
133
|
+
vision: vision,
|
|
134
|
+
default_text: Setting.ai_default_text_model,
|
|
135
|
+
default_image: Setting.ai_default_image_model,
|
|
136
|
+
default_vision: Setting.ai_default_vision_model
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def status
|
|
141
|
+
render json: {
|
|
142
|
+
configured: AiConfiguration.configured?,
|
|
143
|
+
providers: AiConfiguration.configured_providers,
|
|
144
|
+
text_enabled: AiConfiguration.text_enabled?,
|
|
145
|
+
image_enabled: AiConfiguration.image_enabled?,
|
|
146
|
+
screenshot_enabled: AiConfiguration.screenshot_enabled?
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def ensure_ai_configured
|
|
153
|
+
unless AiConfiguration.configured?
|
|
154
|
+
render json: {
|
|
155
|
+
error: "AI not configured. Add API keys in Settings > AI."
|
|
156
|
+
}, status: :service_unavailable
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def build_context
|
|
161
|
+
context = ""
|
|
162
|
+
|
|
163
|
+
if params[:mode] == "element" && params[:current_html].present?
|
|
164
|
+
context = "You are modifying an existing element. Here is the current HTML:\n```html\n#{params[:current_html]}\n```\n\nModify or enhance this element based on the user's request."
|
|
165
|
+
elsif params[:mode] == "page"
|
|
166
|
+
context = "Generate a complete page section or component that can be inserted into a page."
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
context
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def write_sse_event(event, data)
|
|
173
|
+
response.stream.write("event: #{event}\ndata: #{data.to_json}\n\n")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def check_ai_rate_limit
|
|
177
|
+
check_rate_limit(namespace: "ai")
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module ActiveCanvas
|
|
2
|
+
module Admin
|
|
3
|
+
class ApplicationController < ActiveCanvas.config.admin_parent_controller.constantize
|
|
4
|
+
include ActiveCanvas::CurrentUser
|
|
5
|
+
|
|
6
|
+
protect_from_forgery with: :exception
|
|
7
|
+
layout "active_canvas/admin/application"
|
|
8
|
+
|
|
9
|
+
before_action :enforce_authentication_configured
|
|
10
|
+
before_action :active_canvas_authenticate_admin
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def enforce_authentication_configured
|
|
15
|
+
# Skip if inheriting from a custom parent controller (assumes parent handles auth)
|
|
16
|
+
return if ActiveCanvas.config.admin_parent_controller != "ActionController::Base"
|
|
17
|
+
|
|
18
|
+
ActiveCanvas.config.enforce_authentication!
|
|
19
|
+
rescue SecurityError => e
|
|
20
|
+
Rails.logger.error(e.message)
|
|
21
|
+
render plain: "Admin authentication not configured. Check server logs.", status: :forbidden
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def active_canvas_authenticate_admin
|
|
25
|
+
# Skip if inheriting from a custom parent controller (assumes parent handles auth)
|
|
26
|
+
return if ActiveCanvas.config.admin_parent_controller != "ActionController::Base"
|
|
27
|
+
|
|
28
|
+
auth = ActiveCanvas.config.authenticate_admin
|
|
29
|
+
return unless auth
|
|
30
|
+
|
|
31
|
+
if auth == :http_basic_auth
|
|
32
|
+
authenticate_with_http_basic_auth
|
|
33
|
+
elsif auth.is_a?(Symbol)
|
|
34
|
+
send(auth)
|
|
35
|
+
elsif auth.respond_to?(:call)
|
|
36
|
+
instance_exec(&auth)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def authenticate_with_http_basic_auth
|
|
41
|
+
config = ActiveCanvas.config
|
|
42
|
+
|
|
43
|
+
unless config.http_basic_auth_configured?
|
|
44
|
+
Rails.logger.error "[ActiveCanvas] HTTP Basic Auth selected but credentials not configured"
|
|
45
|
+
render plain: "Authentication misconfigured", status: :forbidden
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
authenticate_or_request_with_http_basic("ActiveCanvas Admin") do |username, password|
|
|
50
|
+
ActiveSupport::SecurityUtils.secure_compare(username, config.http_basic_user) &
|
|
51
|
+
ActiveSupport::SecurityUtils.secure_compare(password, config.http_basic_password)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module ActiveCanvas
|
|
2
|
+
module Admin
|
|
3
|
+
class MediaController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@media = Media.images.recent
|
|
6
|
+
|
|
7
|
+
respond_to do |format|
|
|
8
|
+
format.html
|
|
9
|
+
format.json do
|
|
10
|
+
# Support pagination for the asset manager
|
|
11
|
+
page = (params[:page] || 1).to_i
|
|
12
|
+
per_page = (params[:per_page] || 20).to_i
|
|
13
|
+
|
|
14
|
+
total_count = @media.count
|
|
15
|
+
paginated_media = @media.offset((page - 1) * per_page).limit(per_page)
|
|
16
|
+
|
|
17
|
+
render json: {
|
|
18
|
+
data: paginated_media.map(&:as_json_for_editor),
|
|
19
|
+
meta: {
|
|
20
|
+
current_page: page,
|
|
21
|
+
per_page: per_page,
|
|
22
|
+
total_count: total_count,
|
|
23
|
+
total_pages: (total_count.to_f / per_page).ceil
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def show
|
|
31
|
+
@medium = Media.find(params[:id])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create
|
|
35
|
+
@media = Media.new(media_params)
|
|
36
|
+
|
|
37
|
+
if @media.save
|
|
38
|
+
render json: @media.as_json_for_editor, status: :created
|
|
39
|
+
else
|
|
40
|
+
render json: { errors: @media.errors.full_messages }, status: :unprocessable_entity
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def destroy
|
|
45
|
+
@media = Media.find(params[:id])
|
|
46
|
+
@media.destroy
|
|
47
|
+
|
|
48
|
+
respond_to do |format|
|
|
49
|
+
format.html { redirect_to admin_media_url, notice: "Media deleted successfully." }
|
|
50
|
+
format.json { head :no_content }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def media_params
|
|
57
|
+
params.require(:media).permit(:file, :filename)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module ActiveCanvas
|
|
2
|
+
module Admin
|
|
3
|
+
class PageTypesController < ApplicationController
|
|
4
|
+
before_action :set_page_type, only: %i[show edit update destroy]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@page_types = ActiveCanvas::PageType.order(:name)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def new
|
|
14
|
+
@page_type = ActiveCanvas::PageType.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def edit
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
@page_type = ActiveCanvas::PageType.new(page_type_params)
|
|
22
|
+
|
|
23
|
+
if @page_type.save
|
|
24
|
+
redirect_to admin_page_type_path(@page_type), notice: "Page type was successfully created."
|
|
25
|
+
else
|
|
26
|
+
render :new, status: :unprocessable_entity
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update
|
|
31
|
+
if @page_type.update(page_type_params)
|
|
32
|
+
redirect_to admin_page_type_path(@page_type), notice: "Page type was successfully updated."
|
|
33
|
+
else
|
|
34
|
+
render :edit, status: :unprocessable_entity
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def destroy
|
|
39
|
+
if @page_type.destroy
|
|
40
|
+
redirect_to admin_page_types_path, notice: "Page type was successfully deleted."
|
|
41
|
+
else
|
|
42
|
+
redirect_to admin_page_types_path, alert: @page_type.errors.full_messages.join(", ")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def set_page_type
|
|
49
|
+
@page_type = ActiveCanvas::PageType.find(params[:id])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def page_type_params
|
|
53
|
+
params.require(:page_type).permit(:name, :key)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module ActiveCanvas
|
|
2
|
+
module Admin
|
|
3
|
+
class PageVersionsController < ApplicationController
|
|
4
|
+
before_action :set_page
|
|
5
|
+
before_action :set_version
|
|
6
|
+
|
|
7
|
+
def show
|
|
8
|
+
@previous_version = @page.versions.where("version_number < ?", @version.version_number).order(version_number: :desc).first
|
|
9
|
+
@next_version = @page.versions.where("version_number > ?", @version.version_number).order(version_number: :asc).first
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def set_page
|
|
15
|
+
@page = Page.find(params[:page_id])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def set_version
|
|
19
|
+
@version = @page.versions.find(params[:id])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module ActiveCanvas
|
|
2
|
+
module Admin
|
|
3
|
+
class PagesController < ApplicationController
|
|
4
|
+
include ActiveCanvas::TailwindCompilation
|
|
5
|
+
|
|
6
|
+
before_action :set_page, only: %i[show edit update destroy content update_content editor save_editor versions]
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@pages = ActiveCanvas::Page.includes(:page_type).order(created_at: :desc)
|
|
10
|
+
@media_count = ActiveCanvas::Media.count
|
|
11
|
+
@media_total_size = ActiveCanvas::Media.sum(:byte_size)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def new
|
|
18
|
+
@page = ActiveCanvas::Page.new(page_type: ActiveCanvas::PageType.default)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def edit
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create
|
|
25
|
+
@page = ActiveCanvas::Page.new(page_params)
|
|
26
|
+
|
|
27
|
+
if @page.save
|
|
28
|
+
redirect_to admin_page_path(@page), notice: "Page was successfully created."
|
|
29
|
+
else
|
|
30
|
+
render :new, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update
|
|
35
|
+
if @page.update(page_params)
|
|
36
|
+
redirect_to admin_page_path(@page), notice: "Page was successfully updated."
|
|
37
|
+
else
|
|
38
|
+
render :edit, status: :unprocessable_entity
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def destroy
|
|
43
|
+
@page.destroy
|
|
44
|
+
redirect_to admin_pages_path, notice: "Page was successfully deleted."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def content
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def update_content
|
|
51
|
+
if @page.update(params.require(:page).permit(:content))
|
|
52
|
+
redirect_to content_admin_page_path(@page), notice: "Content saved."
|
|
53
|
+
else
|
|
54
|
+
render :content, status: :unprocessable_entity
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def editor
|
|
59
|
+
respond_to do |format|
|
|
60
|
+
format.html { render layout: "active_canvas/admin/editor" }
|
|
61
|
+
format.json do
|
|
62
|
+
render json: {
|
|
63
|
+
content: @page.content,
|
|
64
|
+
content_css: @page.content_css,
|
|
65
|
+
content_js: @page.content_js,
|
|
66
|
+
content_components: @page.content_components
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def save_editor
|
|
73
|
+
content_changed = @page.content != editor_params[:content]
|
|
74
|
+
Rails.logger.info "[ActiveCanvas::PagesController] save_editor for page ##{@page.id}"
|
|
75
|
+
Rails.logger.info "[ActiveCanvas::PagesController] content_changed: #{content_changed}"
|
|
76
|
+
|
|
77
|
+
if @page.update(editor_params)
|
|
78
|
+
tailwind_info = compile_tailwind_if_needed(content_changed) do
|
|
79
|
+
compiled_css = ActiveCanvas::TailwindCompiler.compile_for_page(@page)
|
|
80
|
+
@page.update_columns(compiled_tailwind_css: compiled_css, tailwind_compiled_at: Time.current)
|
|
81
|
+
compiled_css
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
respond_to do |format|
|
|
85
|
+
format.html { redirect_to editor_admin_page_path(@page), notice: "Page saved successfully." }
|
|
86
|
+
format.json do
|
|
87
|
+
render json: {
|
|
88
|
+
success: true,
|
|
89
|
+
message: "Page saved successfully.",
|
|
90
|
+
tailwind: tailwind_info
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
respond_to do |format|
|
|
96
|
+
format.html { render :editor, layout: "active_canvas/admin/editor", status: :unprocessable_entity }
|
|
97
|
+
format.json { render json: { success: false, errors: @page.errors.full_messages }, status: :unprocessable_entity }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def versions
|
|
103
|
+
@versions = @page.versions.recent.limit(50)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def set_page
|
|
109
|
+
@page = ActiveCanvas::Page.find(params[:id])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def page_params
|
|
113
|
+
params.require(:page).permit(
|
|
114
|
+
:title, :slug, :content, :page_type_id, :published,
|
|
115
|
+
# Header/Footer
|
|
116
|
+
:show_header, :show_footer,
|
|
117
|
+
# SEO fields
|
|
118
|
+
:meta_title, :meta_description, :canonical_url, :meta_robots,
|
|
119
|
+
# Open Graph fields
|
|
120
|
+
:og_title, :og_description, :og_image,
|
|
121
|
+
# Twitter fields
|
|
122
|
+
:twitter_card, :twitter_title, :twitter_description, :twitter_image,
|
|
123
|
+
# Structured data
|
|
124
|
+
:structured_data
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def editor_params
|
|
129
|
+
params.require(:page).permit(:content, :content_css, :content_js, :content_components)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module ActiveCanvas
|
|
2
|
+
module Admin
|
|
3
|
+
class PartialsController < ApplicationController
|
|
4
|
+
include ActiveCanvas::TailwindCompilation
|
|
5
|
+
|
|
6
|
+
before_action :ensure_partials_exist, only: [:index]
|
|
7
|
+
before_action :set_partial, only: %i[edit update editor save_editor]
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@partials = ActiveCanvas::Partial.order(:partial_type)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def edit
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update
|
|
17
|
+
if @partial.update(partial_params)
|
|
18
|
+
redirect_to admin_partials_path, notice: "#{@partial.name} was successfully updated."
|
|
19
|
+
else
|
|
20
|
+
render :edit, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def editor
|
|
25
|
+
respond_to do |format|
|
|
26
|
+
format.html { render layout: "active_canvas/admin/editor" }
|
|
27
|
+
format.json do
|
|
28
|
+
render json: {
|
|
29
|
+
content: @partial.content,
|
|
30
|
+
content_css: @partial.content_css,
|
|
31
|
+
content_js: @partial.content_js,
|
|
32
|
+
content_components: @partial.content_components
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def save_editor
|
|
39
|
+
content_changed = @partial.content != editor_params[:content]
|
|
40
|
+
|
|
41
|
+
if @partial.update(editor_params)
|
|
42
|
+
tailwind_info = compile_tailwind_if_needed(content_changed) do
|
|
43
|
+
compiled_css = ActiveCanvas::TailwindCompiler.compile(
|
|
44
|
+
@partial.content.to_s,
|
|
45
|
+
identifier: "partial ##{@partial.id} (#{@partial.name})"
|
|
46
|
+
)
|
|
47
|
+
@partial.update_columns(compiled_css: compiled_css)
|
|
48
|
+
compiled_css
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
respond_to do |format|
|
|
52
|
+
format.html { redirect_to editor_admin_partial_path(@partial), notice: "#{@partial.name} saved successfully." }
|
|
53
|
+
format.json do
|
|
54
|
+
render json: {
|
|
55
|
+
success: true,
|
|
56
|
+
message: "#{@partial.name} saved successfully.",
|
|
57
|
+
tailwind: tailwind_info
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
respond_to do |format|
|
|
63
|
+
format.html { render :editor, layout: "active_canvas/admin/editor", status: :unprocessable_entity }
|
|
64
|
+
format.json { render json: { success: false, errors: @partial.errors.full_messages }, status: :unprocessable_entity }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def ensure_partials_exist
|
|
72
|
+
ActiveCanvas::Partial.ensure_defaults!
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def set_partial
|
|
76
|
+
@partial = ActiveCanvas::Partial.find(params[:id])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def partial_params
|
|
80
|
+
params.require(:partial).permit(:name, :active)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def editor_params
|
|
84
|
+
params.require(:partial).permit(:content, :content_css, :content_js, :content_components)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|