panda-cms 0.8.0 → 0.10.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 +4 -4
- data/README.md +83 -4
- data/app/components/panda/cms/code_component.rb +117 -39
- data/app/components/panda/cms/grid_component.rb +26 -6
- data/app/components/panda/cms/menu_component.rb +66 -34
- data/app/components/panda/cms/page_menu_component.rb +94 -13
- data/app/components/panda/cms/rich_text_component.rb +198 -140
- data/app/components/panda/cms/text_component.rb +77 -44
- data/app/controllers/panda/cms/admin/base_controller.rb +19 -3
- data/app/controllers/panda/cms/admin/dashboard_controller.rb +3 -3
- data/app/controllers/panda/cms/admin/files_controller.rb +7 -0
- data/app/controllers/panda/cms/admin/menus_controller.rb +47 -3
- data/app/controllers/panda/cms/admin/pages_controller.rb +6 -1
- data/app/controllers/panda/cms/pages_controller.rb +2 -2
- data/app/helpers/panda/cms/application_helper.rb +15 -1
- data/app/helpers/panda/cms/asset_helper.rb +14 -3
- data/app/javascript/panda/cms/application_panda_cms.js +1 -1
- data/app/javascript/panda/cms/controllers/code_editor_controller.js +95 -0
- data/app/javascript/panda/cms/controllers/file_gallery_controller.js +128 -0
- data/app/javascript/panda/cms/controllers/index.js +48 -13
- data/app/javascript/panda/cms/controllers/inline_code_editor_controller.js +96 -0
- data/app/javascript/panda/cms/controllers/menu_form_controller.js +40 -0
- data/app/javascript/panda/cms/controllers/nested_form_controller.js +35 -0
- data/app/javascript/panda/cms/controllers/tree_controller.js +214 -0
- data/app/javascript/panda/cms/stimulus-loading.js +5 -7
- data/app/models/panda/cms/block_content.rb +9 -0
- data/app/models/panda/cms/page.rb +41 -0
- data/app/models/panda/cms/post.rb +1 -0
- data/app/views/panda/cms/admin/dashboard/show.html.erb +5 -5
- data/app/views/panda/cms/admin/files/_file_details.html.erb +45 -0
- data/app/views/panda/cms/admin/files/index.html.erb +11 -118
- data/app/views/panda/cms/admin/forms/index.html.erb +2 -2
- data/app/views/panda/cms/admin/forms/new.html.erb +1 -2
- data/app/views/panda/cms/admin/forms/show.html.erb +15 -30
- data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +11 -0
- data/app/views/panda/cms/admin/menus/edit.html.erb +64 -0
- data/app/views/panda/cms/admin/menus/index.html.erb +3 -2
- data/app/views/panda/cms/admin/menus/new.html.erb +40 -0
- data/app/views/panda/cms/admin/pages/edit.html.erb +15 -9
- data/app/views/panda/cms/admin/pages/index.html.erb +49 -11
- data/app/views/panda/cms/admin/pages/new.html.erb +3 -11
- data/app/views/panda/cms/admin/posts/_form.html.erb +4 -14
- data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
- data/app/views/panda/cms/admin/posts/index.html.erb +3 -3
- data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/index.html.erb +3 -3
- data/config/importmap.rb +4 -6
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb.disabled +31 -0
- data/config/initializers/panda/cms.rb +52 -10
- data/config/routes.rb +4 -2
- data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +9 -2
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +6 -1
- data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +23 -21
- data/db/migrate/20251104150640_add_cached_last_updated_at_to_panda_cms_pages.rb +22 -0
- data/db/migrate/20251104172242_add_page_type_to_panda_cms_pages.rb +6 -0
- data/db/migrate/20251104172638_set_page_types_for_existing_pages.rb +27 -0
- data/db/migrate/20251105000001_add_pending_review_status_to_pages_and_posts.panda_cms.rb +21 -0
- data/lib/generators/panda/cms/install_generator.rb +2 -5
- data/lib/panda/cms/asset_loader.rb +36 -16
- data/lib/panda/cms/debug.rb +29 -0
- data/lib/panda/cms/engine.rb +107 -48
- data/lib/panda/cms/features.rb +52 -0
- data/lib/panda-cms/version.rb +1 -1
- data/lib/panda-cms.rb +5 -6
- data/lib/tasks/assets.rake +5 -52
- data/lib/tasks/panda_cms_tasks.rake +16 -0
- metadata +22 -29
- data/app/components/panda/cms/admin/container_component.html.erb +0 -13
- data/app/components/panda/cms/admin/flash_message_component.html.erb +0 -31
- data/app/components/panda/cms/admin/panel_component.html.erb +0 -7
- data/app/components/panda/cms/admin/slideover_component.html.erb +0 -9
- data/app/components/panda/cms/admin/slideover_component.rb +0 -15
- data/app/components/panda/cms/admin/statistics_component.html.erb +0 -4
- data/app/components/panda/cms/admin/statistics_component.rb +0 -16
- data/app/components/panda/cms/admin/tab_bar_component.html.erb +0 -35
- data/app/components/panda/cms/admin/tab_bar_component.rb +0 -15
- data/app/components/panda/cms/admin/table_component.html.erb +0 -29
- data/app/components/panda/cms/admin/user_activity_component.html.erb +0 -7
- data/app/components/panda/cms/admin/user_activity_component.rb +0 -20
- data/app/components/panda/cms/admin/user_display_component.html.erb +0 -17
- data/app/components/panda/cms/admin/user_display_component.rb +0 -21
- data/app/components/panda/cms/grid_component.html.erb +0 -6
- data/app/components/panda/cms/menu_component.html.erb +0 -6
- data/app/components/panda/cms/page_menu_component.html.erb +0 -21
- data/app/components/panda/cms/rich_text_component.html.erb +0 -90
- data/app/views/layouts/panda/cms/application.html.erb +0 -42
- data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +0 -28
- data/app/views/panda/cms/admin/shared/_flash.html.erb +0 -5
- data/app/views/panda/cms/admin/shared/_sidebar.html.erb +0 -41
- data/app/views/panda/cms/shared/_footer.html.erb +0 -2
- data/app/views/panda/cms/shared/_header.html.erb +0 -25
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb +0 -13
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["row", "toggle", "container"]
|
|
5
|
+
static values = {
|
|
6
|
+
collapsed: { type: Array, default: [] }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
const hasStoredState = localStorage.getItem('panda-cms-pages-collapsed')
|
|
11
|
+
|
|
12
|
+
if (hasStoredState) {
|
|
13
|
+
this.loadCollapsedState()
|
|
14
|
+
} else {
|
|
15
|
+
this.initializeTree()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Helper to get the actual table row element from our target div
|
|
20
|
+
getTableRow(rowTarget) {
|
|
21
|
+
return rowTarget.closest('.table-row')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
initializeTree() {
|
|
25
|
+
// Collapse all level 1 pages (direct children of Home) by default
|
|
26
|
+
this.rowTargets.forEach(row => {
|
|
27
|
+
const level = parseInt(row.dataset.level)
|
|
28
|
+
const pageId = row.dataset.pageId
|
|
29
|
+
|
|
30
|
+
// If it's a level 1 page with children, mark it as collapsed
|
|
31
|
+
if (level === 1) {
|
|
32
|
+
const hasChildren = row.querySelector('[data-tree-target="toggle"]')
|
|
33
|
+
if (hasChildren) {
|
|
34
|
+
this.collapsedValue = [...this.collapsedValue, pageId]
|
|
35
|
+
this.updateToggleIcon(pageId, true)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Hide everything below level 1 (level > 1)
|
|
40
|
+
if (level > 1) {
|
|
41
|
+
const tableRow = this.getTableRow(row)
|
|
42
|
+
if (tableRow) tableRow.style.display = 'none'
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Save the initial collapsed state
|
|
47
|
+
this.saveCollapsedState()
|
|
48
|
+
|
|
49
|
+
// Fade in the tree after initialization
|
|
50
|
+
this.showTree()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toggle(event) {
|
|
54
|
+
event.preventDefault()
|
|
55
|
+
const row = event.currentTarget.closest('[data-tree-target="row"]')
|
|
56
|
+
const pageId = row.dataset.pageId
|
|
57
|
+
const level = parseInt(row.dataset.level)
|
|
58
|
+
|
|
59
|
+
if (this.isCollapsed(pageId)) {
|
|
60
|
+
this.expand(pageId, level)
|
|
61
|
+
} else {
|
|
62
|
+
this.collapse(pageId, level)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.saveCollapsedState()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
collapse(pageId, level) {
|
|
69
|
+
// Add to collapsed set
|
|
70
|
+
if (!this.collapsedValue.includes(pageId)) {
|
|
71
|
+
this.collapsedValue = [...this.collapsedValue, pageId]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Hide all descendant rows
|
|
75
|
+
const descendants = this.getDescendantRows(pageId, level)
|
|
76
|
+
descendants.forEach(row => {
|
|
77
|
+
const tableRow = this.getTableRow(row)
|
|
78
|
+
if (tableRow) {
|
|
79
|
+
tableRow.style.display = 'none'
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Update toggle icon
|
|
84
|
+
this.updateToggleIcon(pageId, true)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
expand(pageId, level) {
|
|
88
|
+
// Remove from collapsed set
|
|
89
|
+
this.collapsedValue = this.collapsedValue.filter(id => id !== pageId)
|
|
90
|
+
|
|
91
|
+
// Show direct children only (they will handle their own children)
|
|
92
|
+
const directChildren = this.getDirectChildRows(pageId, level)
|
|
93
|
+
directChildren.forEach(row => {
|
|
94
|
+
const tableRow = this.getTableRow(row)
|
|
95
|
+
if (tableRow) tableRow.style.display = ''
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Update toggle icon
|
|
99
|
+
this.updateToggleIcon(pageId, false)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getDescendantRows(pageId, parentLevel) {
|
|
103
|
+
const allRows = this.rowTargets
|
|
104
|
+
const parentIndex = allRows.findIndex(row => row.dataset.pageId === pageId)
|
|
105
|
+
const descendants = []
|
|
106
|
+
|
|
107
|
+
for (let i = parentIndex + 1; i < allRows.length; i++) {
|
|
108
|
+
const rowLevel = parseInt(allRows[i].dataset.level)
|
|
109
|
+
if (rowLevel <= parentLevel) break
|
|
110
|
+
descendants.push(allRows[i])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return descendants
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getDirectChildRows(pageId, parentLevel) {
|
|
117
|
+
const allRows = this.rowTargets
|
|
118
|
+
const parentIndex = allRows.findIndex(row => row.dataset.pageId === pageId)
|
|
119
|
+
const children = []
|
|
120
|
+
|
|
121
|
+
for (let i = parentIndex + 1; i < allRows.length; i++) {
|
|
122
|
+
const rowLevel = parseInt(allRows[i].dataset.level)
|
|
123
|
+
if (rowLevel <= parentLevel) break
|
|
124
|
+
if (rowLevel === parentLevel + 1) children.push(allRows[i])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return children
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
isCollapsed(pageId) {
|
|
131
|
+
return this.collapsedValue.includes(pageId)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
updateToggleIcon(pageId, collapsed) {
|
|
135
|
+
const row = this.rowTargets.find(r => r.dataset.pageId === pageId)
|
|
136
|
+
if (!row) return
|
|
137
|
+
|
|
138
|
+
const toggle = row.querySelector('[data-tree-target="toggle"]')
|
|
139
|
+
if (!toggle) return
|
|
140
|
+
|
|
141
|
+
const icon = toggle.querySelector('i')
|
|
142
|
+
if (icon) {
|
|
143
|
+
if (collapsed) {
|
|
144
|
+
icon.classList.remove('fa-chevron-down')
|
|
145
|
+
icon.classList.add('fa-chevron-right')
|
|
146
|
+
} else {
|
|
147
|
+
icon.classList.remove('fa-chevron-right')
|
|
148
|
+
icon.classList.add('fa-chevron-down')
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
loadCollapsedState() {
|
|
154
|
+
try {
|
|
155
|
+
const stored = localStorage.getItem('panda-cms-pages-collapsed')
|
|
156
|
+
if (stored) {
|
|
157
|
+
this.collapsedValue = JSON.parse(stored)
|
|
158
|
+
|
|
159
|
+
// First, show all pages at level 1 (direct children of Home)
|
|
160
|
+
this.rowTargets.forEach(row => {
|
|
161
|
+
const level = parseInt(row.dataset.level)
|
|
162
|
+
if (level === 1) {
|
|
163
|
+
const tableRow = this.getTableRow(row)
|
|
164
|
+
if (tableRow) tableRow.style.display = ''
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Then apply collapsed state - hiding descendants of collapsed items
|
|
169
|
+
this.collapsedValue.forEach(pageId => {
|
|
170
|
+
const row = this.rowTargets.find(r => r.dataset.pageId === pageId)
|
|
171
|
+
if (row) {
|
|
172
|
+
const level = parseInt(row.dataset.level)
|
|
173
|
+
this.collapse(pageId, level)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// For level 1 items NOT in collapsed list, show their children
|
|
178
|
+
this.rowTargets.forEach(row => {
|
|
179
|
+
const level = parseInt(row.dataset.level)
|
|
180
|
+
const pageId = row.dataset.pageId
|
|
181
|
+
const hasToggle = row.querySelector('[data-tree-target="toggle"]')
|
|
182
|
+
|
|
183
|
+
if (level === 1 && hasToggle && !this.isCollapsed(pageId)) {
|
|
184
|
+
// This level 1 item is expanded, show its direct children
|
|
185
|
+
this.getDirectChildRows(pageId, level).forEach(childRow => {
|
|
186
|
+
const tableRow = this.getTableRow(childRow)
|
|
187
|
+
if (tableRow) tableRow.style.display = ''
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// Fade in the tree after loading state
|
|
193
|
+
this.showTree()
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.error('Error loading collapsed state:', e)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
showTree() {
|
|
201
|
+
// Fade in the container
|
|
202
|
+
if (this.hasContainerTarget) {
|
|
203
|
+
this.containerTarget.style.opacity = '1'
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
saveCollapsedState() {
|
|
208
|
+
try {
|
|
209
|
+
localStorage.setItem('panda-cms-pages-collapsed', JSON.stringify(this.collapsedValue))
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.error('Error saving collapsed state:', e)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
// Stimulus loading utilities for Panda CMS
|
|
2
2
|
// This provides the loading functionality that would normally come from stimulus-rails
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Import the shared Stimulus application from Panda Core
|
|
5
|
+
// This ensures all controllers (Core and CMS) are registered in the same application
|
|
6
|
+
import { application } from "/panda/core/application.js"
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// Configure debug mode based on environment
|
|
9
|
-
const railsEnv = document.body?.dataset?.environment || "production";
|
|
10
|
-
application.debug = railsEnv === "development"
|
|
11
|
-
window.Stimulus = application
|
|
8
|
+
// The application is already started and configured in Core
|
|
9
|
+
// No need to start it again or configure debug mode
|
|
12
10
|
|
|
13
11
|
// Auto-registration functionality
|
|
14
12
|
function eagerLoadControllersFrom(context) {
|
|
@@ -12,8 +12,17 @@ module Panda
|
|
|
12
12
|
|
|
13
13
|
validates :block, presence: true, uniqueness: {scope: :page}
|
|
14
14
|
|
|
15
|
+
after_save :refresh_page_cached_timestamp
|
|
16
|
+
after_destroy :refresh_page_cached_timestamp
|
|
17
|
+
|
|
15
18
|
store_accessor :content, [], prefix: true
|
|
16
19
|
store_accessor :cached_content, [], prefix: true
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def refresh_page_cached_timestamp
|
|
24
|
+
page&.refresh_last_updated_at!
|
|
25
|
+
end
|
|
17
26
|
end
|
|
18
27
|
end
|
|
19
28
|
end
|
|
@@ -38,12 +38,22 @@ module Panda
|
|
|
38
38
|
enum :status, {
|
|
39
39
|
active: "active",
|
|
40
40
|
draft: "draft",
|
|
41
|
+
pending_review: "pending_review",
|
|
41
42
|
hidden: "hidden",
|
|
42
43
|
archived: "archived"
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
enum :page_type, {
|
|
47
|
+
standard: "standard",
|
|
48
|
+
hidden_type: "hidden",
|
|
49
|
+
system: "system",
|
|
50
|
+
posts: "posts",
|
|
51
|
+
code: "code"
|
|
52
|
+
}, prefix: :type
|
|
53
|
+
|
|
45
54
|
# Callbacks
|
|
46
55
|
after_save :handle_after_save
|
|
56
|
+
before_save :update_cached_last_updated_at
|
|
47
57
|
|
|
48
58
|
#
|
|
49
59
|
# Update any menus which include this page or its parent as a menu item
|
|
@@ -56,6 +66,31 @@ module Panda
|
|
|
56
66
|
menus_of_parent.find_each(&:generate_auto_menu_items)
|
|
57
67
|
end
|
|
58
68
|
|
|
69
|
+
#
|
|
70
|
+
# Returns the most recent update time between the page and its block contents
|
|
71
|
+
# Uses cached value for performance
|
|
72
|
+
#
|
|
73
|
+
# @return [Time] The most recent updated_at timestamp
|
|
74
|
+
# @visibility public
|
|
75
|
+
#
|
|
76
|
+
def last_updated_at
|
|
77
|
+
cached_last_updated_at || updated_at
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
# Refresh the cached last updated timestamp
|
|
82
|
+
# Used when block contents are updated
|
|
83
|
+
#
|
|
84
|
+
# @return [Time] The updated timestamp
|
|
85
|
+
# @visibility public
|
|
86
|
+
#
|
|
87
|
+
def refresh_last_updated_at!
|
|
88
|
+
block_content_updated_at = block_contents.maximum(:updated_at)
|
|
89
|
+
new_timestamp = [updated_at, block_content_updated_at].compact.max
|
|
90
|
+
update_column(:cached_last_updated_at, new_timestamp)
|
|
91
|
+
new_timestamp
|
|
92
|
+
end
|
|
93
|
+
|
|
59
94
|
private
|
|
60
95
|
|
|
61
96
|
def validate_unique_path_in_scope
|
|
@@ -124,6 +159,12 @@ module Panda
|
|
|
124
159
|
destination_path: new_path
|
|
125
160
|
)
|
|
126
161
|
end
|
|
162
|
+
|
|
163
|
+
def update_cached_last_updated_at
|
|
164
|
+
# Will be set to updated_at automatically during save
|
|
165
|
+
# Block content updates will call refresh_last_updated_at! separately
|
|
166
|
+
self.cached_last_updated_at = Time.current
|
|
167
|
+
end
|
|
127
168
|
end
|
|
128
169
|
end
|
|
129
170
|
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<div class="" data-controller="dashboard">
|
|
2
2
|
<%= render Panda::Core::Admin::ContainerComponent.new do |container| %>
|
|
3
|
-
<% container.
|
|
4
|
-
<% heading.
|
|
3
|
+
<% container.heading(text: "Dashboard", level: 1) do |heading| %>
|
|
4
|
+
<% heading.button(action: :add, text: "Add Page", link: new_admin_cms_page_path) %>
|
|
5
5
|
<% end %>
|
|
6
6
|
<dl class="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-3">
|
|
7
|
-
<%= render Panda::
|
|
8
|
-
<%= render Panda::
|
|
9
|
-
<%= render Panda::
|
|
7
|
+
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Today", value: Panda::CMS::Visit.group_by_day(:visited_at, last: 1).count.values.first) %>
|
|
8
|
+
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Last Week", value: Panda::CMS::Visit.group_by_week(:visited_at, last: 1).count.values.first) %>
|
|
9
|
+
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Last Month", value: Panda::CMS::Visit.group_by_month(:visited_at, last: 1).count.values.first) %>
|
|
10
10
|
</dl>
|
|
11
11
|
<% end %>
|
|
12
12
|
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<div class="block overflow-hidden w-full rounded-lg aspect-h-7 aspect-w-10">
|
|
3
|
+
<% if file.image? %>
|
|
4
|
+
<%= image_tag main_app.rails_blob_path(file, only_path: true), alt: file.filename.to_s, class: "object-cover" %>
|
|
5
|
+
<% else %>
|
|
6
|
+
<div class="flex items-center justify-center h-full bg-gray-100">
|
|
7
|
+
<div class="text-center">
|
|
8
|
+
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
10
|
+
</svg>
|
|
11
|
+
<p class="mt-1 text-xs text-gray-500 uppercase"><%= file.content_type&.split("/")&.last || "file" %></p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="flex justify-between items-start mt-4">
|
|
17
|
+
<div>
|
|
18
|
+
<h2 class="text-lg font-medium text-gray-900"><span class="sr-only">Details for </span><%= file.filename %></h2>
|
|
19
|
+
<p class="text-sm font-medium text-gray-500"><%= number_to_human_size(file.byte_size) %></p>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div>
|
|
25
|
+
<h3 class="font-medium text-gray-900">Information</h3>
|
|
26
|
+
<dl class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
|
|
27
|
+
<div class="flex justify-between py-3 text-sm font-medium">
|
|
28
|
+
<dt class="text-gray-500">Created</dt>
|
|
29
|
+
<dd class="text-gray-900 whitespace-nowrap"><%= file.created_at.strftime("%B %-d, %Y") %></dd>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="flex justify-between py-3 text-sm font-medium">
|
|
32
|
+
<dt class="text-gray-500">Content Type</dt>
|
|
33
|
+
<dd class="text-gray-900 whitespace-nowrap"><%= file.content_type %></dd>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="flex justify-between py-3 text-sm font-medium">
|
|
36
|
+
<dt class="text-gray-500">Checksum</dt>
|
|
37
|
+
<dd class="text-gray-900 whitespace-nowrap font-mono text-xs"><%= file.checksum %></dd>
|
|
38
|
+
</div>
|
|
39
|
+
</dl>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="flex gap-x-3">
|
|
43
|
+
<%= link_to "Download", main_app.rails_blob_path(file, disposition: "attachment"), class: "flex-1 py-2 px-3 text-sm font-semibold text-white bg-black rounded-md shadow-sm hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-panda-dark text-center" %>
|
|
44
|
+
<%= button_tag "Delete", type: "button", class: "flex-1 py-2 px-3 text-sm font-semibold text-gray-900 bg-white rounded-md ring-1 ring-inset shadow-sm hover:bg-gray-50 ring-mid", data: { confirm: "Are you sure you want to delete this file?" } %>
|
|
45
|
+
</div>
|
|
@@ -1,124 +1,17 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
|
-
<% component.
|
|
2
|
+
<% component.heading(text: "Files", level: 1) %>
|
|
3
3
|
|
|
4
|
-
<% component.
|
|
5
|
-
<div class="pt-4 pb-16 space-y-6">
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
</
|
|
10
|
-
|
|
11
|
-
<div>
|
|
12
|
-
<h2 class="text-lg font-medium text-gray-900"><span class="sr-only">Details for </span>IMG_4985.HEIC</h2>
|
|
13
|
-
<p class="text-sm font-medium text-gray-500">3.9 MB</p>
|
|
14
|
-
</div>
|
|
15
|
-
<button type="button" class="flex relative justify-center items-center ml-4 w-8 h-8 text-gray-400 bg-white rounded-full hover:text-gray-500 hover:bg-gray-100 focus:ring-2 focus:outline-none focus:ring-panda-dark">
|
|
16
|
-
<span class="absolute -inset-1.5"></span>
|
|
17
|
-
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
|
18
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
|
|
19
|
-
</svg>
|
|
20
|
-
<span class="sr-only">Favorite</span>
|
|
21
|
-
</button>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
<div>
|
|
25
|
-
<h3 class="font-medium text-gray-900">Information</h3>
|
|
26
|
-
<dl class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
|
|
27
|
-
<div class="flex justify-between py-3 text-sm font-medium">
|
|
28
|
-
<dt class="text-gray-500">Uploaded by</dt>
|
|
29
|
-
<dd class="text-gray-900 whitespace-nowrap">Marie Culver</dd>
|
|
30
|
-
</div>
|
|
31
|
-
<div class="flex justify-between py-3 text-sm font-medium">
|
|
32
|
-
<dt class="text-gray-500">Created</dt>
|
|
33
|
-
<dd class="text-gray-900 whitespace-nowrap">June 8, 2020</dd>
|
|
34
|
-
</div>
|
|
35
|
-
<div class="flex justify-between py-3 text-sm font-medium">
|
|
36
|
-
<dt class="text-gray-500">Last modified</dt>
|
|
37
|
-
<dd class="text-gray-900 whitespace-nowrap">June 8, 2020</dd>
|
|
38
|
-
</div>
|
|
39
|
-
<div class="flex justify-between py-3 text-sm font-medium">
|
|
40
|
-
<dt class="text-gray-500">Dimensions</dt>
|
|
41
|
-
<dd class="text-gray-900 whitespace-nowrap">4032 x 3024</dd>
|
|
42
|
-
</div>
|
|
43
|
-
<div class="flex justify-between py-3 text-sm font-medium">
|
|
44
|
-
<dt class="text-gray-500">Resolution</dt>
|
|
45
|
-
<dd class="text-gray-900 whitespace-nowrap">72 x 72</dd>
|
|
46
|
-
</div>
|
|
47
|
-
</dl>
|
|
48
|
-
</div>
|
|
49
|
-
<div>
|
|
50
|
-
<h3 class="font-medium text-gray-900">Description</h3>
|
|
51
|
-
<div class="flex justify-between items-center mt-2">
|
|
52
|
-
<p class="text-sm italic text-gray-500">Add a description to this image.</p>
|
|
53
|
-
<button type="button" class="flex relative justify-center items-center w-8 h-8 text-gray-400 bg-white rounded-full hover:text-gray-500 hover:bg-gray-100 focus:ring-2 focus:outline-none focus:ring-panda-dark">
|
|
54
|
-
<span class="absolute -inset-1.5"></span>
|
|
55
|
-
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
56
|
-
<path d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
|
|
57
|
-
</svg>
|
|
58
|
-
<span class="sr-only">Add description</span>
|
|
59
|
-
</button>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
<div>
|
|
63
|
-
<h3 class="font-medium text-gray-900">Shared with</h3>
|
|
64
|
-
<ul role="list" class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
|
|
65
|
-
<li class="flex justify-between items-center py-3">
|
|
66
|
-
<div class="flex items-center">
|
|
67
|
-
<img src="https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=3&w=1024&h=1024&q=80" alt="" class="w-8 h-8 rounded-full">
|
|
68
|
-
<p class="ml-4 text-sm font-medium text-gray-900">Aimee Douglas</p>
|
|
69
|
-
</div>
|
|
70
|
-
<button type="button" class="ml-6 text-sm font-medium bg-white rounded-md focus:ring-2 focus:ring-offset-2 focus:outline-none text-panda-dark hover:text-panda-dark focus:ring-panda-dark">Remove<span class="sr-only"> Aimee Douglas</span></button>
|
|
71
|
-
</li>
|
|
72
|
-
<li class="flex justify-between items-center py-3">
|
|
73
|
-
<div class="flex items-center">
|
|
74
|
-
<img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixqx=oilqXxSqey&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" class="w-8 h-8 rounded-full">
|
|
75
|
-
<p class="ml-4 text-sm font-medium text-gray-900">Andrea McMillan</p>
|
|
76
|
-
</div>
|
|
77
|
-
<button type="button" class="ml-6 text-sm font-medium bg-white rounded-md focus:ring-2 focus:ring-offset-2 focus:outline-none text-panda-dark hover:text-panda-dark focus:ring-panda-dark">Remove<span class="sr-only"> Andrea McMillan</span></button>
|
|
78
|
-
</li>
|
|
79
|
-
|
|
80
|
-
<li class="flex justify-between items-center py-2">
|
|
81
|
-
<button type="button" class="flex items-center p-1 -ml-1 bg-white rounded-md focus:ring-2 focus:outline-none group focus:ring-panda-dark">
|
|
82
|
-
<span class="flex justify-center items-center w-8 h-8 text-gray-400 rounded-full border-2 border-gray-300 border-dashed">
|
|
83
|
-
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
84
|
-
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
|
85
|
-
</svg>
|
|
86
|
-
</span>
|
|
87
|
-
<span class="ml-4 text-sm font-medium text-panda-dark group-hover:text-panda-dark">Share</span>
|
|
88
|
-
</button>
|
|
89
|
-
</li>
|
|
90
|
-
</ul>
|
|
91
|
-
</div>
|
|
92
|
-
<div class="flex gap-x-3">
|
|
93
|
-
<button type="button" class="flex-1 py-2 px-3 text-sm font-semibold text-white bg-black rounded-md shadow-sm hover:bg-black focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-panda-dark">Download</button>
|
|
94
|
-
<button type="button" class="flex-1 py-2 px-3 text-sm font-semibold text-gray-900 bg-white rounded-md ring-1 ring-inset shadow-sm hover:bg-gray-50 ring-mid">Delete</button>
|
|
95
|
-
</div>
|
|
4
|
+
<% component.slideover(title: "File Details") do %>
|
|
5
|
+
<div class="pt-4 pb-16 space-y-6" data-file-gallery-target="slideoverContent">
|
|
6
|
+
<% if @selected_file %>
|
|
7
|
+
<%= render partial: "file_details", locals: { file: @selected_file } %>
|
|
8
|
+
<% else %>
|
|
9
|
+
<p class="text-sm text-gray-500">Select a file to view details</p>
|
|
10
|
+
<% end %>
|
|
96
11
|
</div>
|
|
97
12
|
<% end %>
|
|
98
13
|
|
|
99
|
-
|
|
100
|
-
<%=
|
|
101
|
-
|
|
102
|
-
<%= component.with_tab(name: "Favourited") %>
|
|
103
|
-
<% end %>
|
|
104
|
-
|
|
105
|
-
<section>
|
|
106
|
-
<h2 id="gallery-heading" class="sr-only">Recently viewed</h2>
|
|
107
|
-
<ul role="list" class="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 md:grid-cols-4 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
|
|
108
|
-
<li class="relative">
|
|
109
|
-
<!-- Current: "ring-2 ring-panda-dark ring-offset-2", Default: "focus-within:ring-2 focus-within:ring-panda-dark focus-within:ring-offset-2 focus-within:ring-offset-gray-100" -->
|
|
110
|
-
<div class="block overflow-hidden w-full bg-gray-100 rounded-lg ring-2 ring-offset-2 ring-panda-dark aspect-w-10 aspect-h-7 group">
|
|
111
|
-
<!-- Current: "", Default: "group-hover:opacity-75" -->
|
|
112
|
-
<img src="https://images.unsplash.com/photo-1582053433976-25c00369fc93?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=512&q=80" alt="" class="object-cover pointer-events-none">
|
|
113
|
-
<button type="button" class="absolute inset-0 focus:outline-none">
|
|
114
|
-
<span class="sr-only">View details for IMG_4985.HEIC</span>
|
|
115
|
-
</button>
|
|
116
|
-
</div>
|
|
117
|
-
<p class="block mt-2 text-sm font-medium text-gray-900 pointer-events-none truncate">IMG_4985.HEIC</p>
|
|
118
|
-
<p class="block text-sm font-medium text-gray-500 pointer-events-none">3.9 MB</p>
|
|
119
|
-
</li>
|
|
120
|
-
|
|
121
|
-
<!-- More files... -->
|
|
122
|
-
</ul>
|
|
123
|
-
</section>
|
|
14
|
+
<div data-controller="file-gallery" class="pb-24">
|
|
15
|
+
<%= render Panda::Core::Admin::FileGalleryComponent.new(files: @files, selected_file: @selected_file) %>
|
|
16
|
+
</div>
|
|
124
17
|
<% end %>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
|
-
<% component.
|
|
2
|
+
<% component.heading(text: "Forms", level: 1, icon: "fa-solid fa-inbox") do |heading| %>
|
|
3
3
|
<% end %>
|
|
4
4
|
|
|
5
|
-
<%= render Panda::Core::Admin::TableComponent.new(term: "form", rows: forms) do |table| %>
|
|
5
|
+
<%= render Panda::Core::Admin::TableComponent.new(term: "form", rows: forms, icon: "fa-solid fa-inbox") do |table| %>
|
|
6
6
|
<% table.column("Name") { |form| block_link_to form.name, admin_cms_form_path(form) } %>
|
|
7
7
|
<% table.column("Status") { |form| render Panda::Core::Admin::TagComponent.new(status: :active) } %>
|
|
8
8
|
<% table.column("Last Submission") do |form| %>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
|
-
<% component.
|
|
3
|
-
<% end %>
|
|
2
|
+
<% component.heading(text: "Add Form", level: 1) %>
|
|
4
3
|
<%= panda_cms_form_with model: page, url: admin_cms_pages_path, method: :post do |f| %>
|
|
5
4
|
<% options = nested_set_options(Panda::CMS::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
|
|
6
5
|
<div data-controller="slug">
|
|
@@ -1,35 +1,20 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
|
-
<% component.
|
|
2
|
+
<% component.heading(text: form.name, level: 1) do |heading| %>
|
|
3
3
|
<% end %>
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<th scope="col" class="py-3.5 px-3 text-sm font-semibold text-left text-gray-900">Submitted</th>
|
|
13
|
-
</tr>
|
|
14
|
-
</thead>
|
|
15
|
-
<tbody class="bg-white divide-y divide-gray-200">
|
|
16
|
-
<% submissions.each do |submission| %>
|
|
17
|
-
<tr class="relative bg-white cursor-pointer hover:bg-gray-50">
|
|
18
|
-
<% fields.each do |field| %>
|
|
19
|
-
<td class="py-5 px-3 text-sm text-gray-500">
|
|
20
|
-
<% if field[0] == "email" || field[0] == "email_address" %>
|
|
21
|
-
<a href="mailto:<%= submission.data[field[0]] %>" class="border-b border-gray-500 hover:text-gray-900"><%= submission.data[field[0]] %></a>
|
|
22
|
-
<% else %>
|
|
23
|
-
<%= simple_format(submission.data[field[0]]) %>
|
|
24
|
-
<% end %>
|
|
25
|
-
</td>
|
|
26
|
-
<% end %>
|
|
27
|
-
<td class="py-5 px-3 text-sm text-gray-500 whitespace-nowrap">
|
|
28
|
-
<div class="text-gray-500"><%= time_ago_in_words(submission.created_at) %> ago</div>
|
|
29
|
-
</td>
|
|
30
|
-
</tr>
|
|
5
|
+
<%= render Panda::Core::Admin::TableComponent.new(term: "submission", rows: submissions) do |table| %>
|
|
6
|
+
<% fields.each do |field, title| %>
|
|
7
|
+
<% table.column(title) do |submission| %>
|
|
8
|
+
<% if field[0] == "email" || field[0] == "email_address" %>
|
|
9
|
+
<a href="mailto:<%= submission.data[field[0]] %>" class="border-b border-gray-500 hover:text-gray-900"><%= submission.data[field[0]] %></a>
|
|
10
|
+
<% else %>
|
|
11
|
+
<%= simple_format(submission.data[field[0]]) %>
|
|
31
12
|
<% end %>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<% table.column("Submitted") do |submission| %>
|
|
17
|
+
<%= time_ago_in_words(submission.created_at) %> ago
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
35
20
|
<% end %>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div class="mb-6 pb-6 border-b border-gray-200 dark:border-gray-700 nested-form-wrapper" data-new-record="<%= form.object.new_record? %>">
|
|
2
|
+
<%= form.text_field :text, placeholder: "Menu item text" %>
|
|
3
|
+
<%= form.collection_select :panda_cms_page_id, Panda::CMS::Page.order(:title), :id, :title, { include_blank: "Select a page (optional)" }, { class: "mt-1" } %>
|
|
4
|
+
<%= form.text_field :external_url, placeholder: "External URL (optional)" %>
|
|
5
|
+
|
|
6
|
+
<div class="mt-3">
|
|
7
|
+
<%= render Panda::Core::Admin::ButtonComponent.new(text: "Remove", action: :delete, link: "#", size: :small, data: { action: "click->nested-form#remove" }) %>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<%= form.hidden_field :_destroy %>
|
|
11
|
+
</div>
|