alchemy_cms 8.2.7 → 8.3.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 +4 -1
- data/app/assets/builds/alchemy/admin.css +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
- data/app/assets/builds/alchemy/dark-theme.css +1 -1
- data/app/assets/builds/alchemy/light-theme.css +1 -1
- data/app/assets/builds/alchemy/preview.min.js +1 -1
- data/app/assets/builds/alchemy/theme.css +1 -1
- data/app/assets/builds/alchemy/welcome.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
- data/app/assets/images/alchemy/admin/logo.svg +27 -0
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/dashboard/widget.rb +40 -0
- data/app/components/alchemy/admin/dashboard/widgets/attachment_counts.rb +17 -0
- data/app/components/alchemy/admin/dashboard/widgets/element_usage.rb +37 -0
- data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +13 -0
- data/app/components/alchemy/admin/dashboard/widgets/greeting.rb +21 -0
- data/app/components/alchemy/admin/dashboard/widgets/locked_pages.html.erb +54 -0
- data/app/components/alchemy/admin/dashboard/widgets/locked_pages.rb +20 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +22 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.rb +19 -0
- data/app/components/alchemy/admin/dashboard/widgets/page_counts.rb +23 -0
- data/app/components/alchemy/admin/dashboard/widgets/page_usage.rb +46 -0
- data/app/components/alchemy/admin/dashboard/widgets/picture_counts.rb +17 -0
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +41 -0
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.rb +16 -0
- data/app/components/alchemy/admin/dashboard/widgets/sites.html.erb +29 -0
- data/app/components/alchemy/admin/dashboard/widgets/sites.rb +15 -0
- data/app/components/alchemy/admin/dashboard/widgets/stat_widget.html.erb +23 -0
- data/app/components/alchemy/admin/dashboard/widgets/stat_widget.rb +19 -0
- data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +32 -0
- data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +37 -0
- data/app/components/alchemy/admin/dashboard/widgets/usage_widget.html.erb +42 -0
- data/app/components/alchemy/admin/dashboard/widgets/usage_widget.rb +66 -0
- data/app/components/alchemy/admin/dashboard/widgets/user_counts.rb +25 -0
- data/app/components/alchemy/admin/element_editor.html.erb +27 -20
- data/app/components/alchemy/admin/element_schedule_timestamps.rb +33 -0
- data/app/components/alchemy/admin/element_select.rb +4 -3
- data/app/components/alchemy/admin/page_node.html.erb +1 -20
- data/app/components/alchemy/admin/page_publication_fields.html.erb +30 -0
- data/app/components/alchemy/admin/page_publication_fields.rb +18 -0
- data/app/components/alchemy/admin/page_status_indicators.html.erb +29 -0
- data/app/components/alchemy/admin/page_status_indicators.rb +9 -0
- data/app/components/alchemy/admin/publish_element_button.html.erb +12 -4
- data/app/components/alchemy/ingredients/headline_editor.rb +1 -1
- data/app/controllers/alchemy/admin/dashboard/widgets_controller.rb +21 -0
- data/app/controllers/alchemy/admin/dashboard_controller.rb +3 -12
- data/app/controllers/alchemy/pages_controller.rb +5 -4
- data/app/helpers/alchemy/elements_block_helper.rb +1 -0
- data/app/javascript/alchemy_admin/components/auto_submit.js +15 -9
- data/app/javascript/alchemy_admin/components/char_counter.js +17 -7
- data/app/javascript/alchemy_admin/components/clipboard_button.js +2 -6
- data/app/javascript/alchemy_admin/components/color_select.js +13 -4
- data/app/javascript/alchemy_admin/components/datepicker.js +11 -14
- data/app/javascript/alchemy_admin/components/dialog_link.js +5 -2
- data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +6 -3
- data/app/javascript/alchemy_admin/components/element_editor.js +45 -28
- data/app/javascript/alchemy_admin/components/element_select.js +7 -4
- data/app/javascript/alchemy_admin/components/elements_window.js +38 -31
- data/app/javascript/alchemy_admin/components/elements_window_handle.js +7 -3
- data/app/javascript/alchemy_admin/components/file_editor.js +5 -2
- data/app/javascript/alchemy_admin/components/ingredient_group.js +6 -4
- data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +1 -2
- data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +1 -2
- data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
- data/app/javascript/alchemy_admin/components/list_filter.js +44 -29
- data/app/javascript/alchemy_admin/components/message.js +22 -15
- data/app/javascript/alchemy_admin/components/overlay.js +5 -7
- data/app/javascript/alchemy_admin/components/page_publication_fields.js +38 -25
- data/app/javascript/alchemy_admin/components/picture_description_select.js +5 -2
- data/app/javascript/alchemy_admin/components/picture_editor.js +5 -10
- data/app/javascript/alchemy_admin/components/picture_thumbnail.js +4 -5
- data/app/javascript/alchemy_admin/components/preview_window.js +5 -10
- data/app/javascript/alchemy_admin/components/publish_page_button.js +2 -5
- data/app/javascript/alchemy_admin/components/remote_select.js +53 -23
- data/app/javascript/alchemy_admin/components/select.js +169 -26
- data/app/javascript/alchemy_admin/components/sortable_elements.js +1 -1
- data/app/javascript/alchemy_admin/components/spinner.js +11 -11
- data/app/javascript/alchemy_admin/components/tags_autocomplete.js +9 -1
- data/app/javascript/alchemy_admin/components/tinymce.js +16 -22
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +48 -45
- data/app/javascript/alchemy_admin/components/uploader/progress.js +70 -84
- data/app/javascript/alchemy_admin/components/uploader.js +71 -46
- data/app/javascript/alchemy_admin/dialog.js +3 -0
- data/app/javascript/alchemy_admin/hotkeys.js +0 -18
- data/app/javascript/alchemy_admin/image_cropper.js +7 -9
- data/app/javascript/alchemy_admin/initializer.js +21 -0
- data/app/javascript/alchemy_admin/utils/dispatch_page_dirty_event.js +7 -0
- data/app/javascript/tinymce/plugins/alchemy_link/index.js +9 -0
- data/app/jobs/alchemy/base_job.rb +2 -2
- data/app/jobs/alchemy/invalidate_elements_cache_job.rb +33 -0
- data/app/models/alchemy/page/page_naming.rb +28 -5
- data/app/models/alchemy/page/page_natures.rb +7 -2
- data/app/models/alchemy/page/page_scopes.rb +2 -2
- data/app/models/alchemy/page/url_path.rb +7 -2
- data/app/models/alchemy/page.rb +2 -2
- data/app/models/alchemy/page_definition.rb +1 -0
- data/app/models/alchemy/permissions.rb +1 -1
- data/app/models/concerns/alchemy/relatable_resource.rb +8 -0
- data/app/services/alchemy/page_finder.rb +88 -0
- data/app/stylesheets/alchemy/_custom-properties.scss +6 -4
- data/app/stylesheets/alchemy/_mixins.scss +1 -7
- data/app/stylesheets/alchemy/_themes.scss +13 -1
- data/app/stylesheets/alchemy/admin/_tom-select.scss +240 -0
- data/app/stylesheets/alchemy/admin/archive.scss +0 -1
- data/app/stylesheets/alchemy/admin/base.scss +0 -19
- data/app/stylesheets/alchemy/admin/dashboard.scss +395 -28
- data/app/stylesheets/alchemy/admin/elements.scss +14 -17
- data/app/stylesheets/alchemy/admin/form_fields.scss +3 -3
- data/app/stylesheets/alchemy/admin/forms.scss +107 -93
- data/app/stylesheets/alchemy/admin/icons.scss +28 -0
- data/app/stylesheets/alchemy/admin/image_library.scss +20 -10
- data/app/stylesheets/alchemy/admin/navigation.scss +4 -1
- data/app/stylesheets/alchemy/admin/popover.scss +3 -5
- data/app/stylesheets/alchemy/admin/resource_info.scss +11 -17
- data/app/stylesheets/alchemy/admin/shoelace.scss +8 -0
- data/app/stylesheets/alchemy/admin/sitemap.scss +5 -0
- data/app/stylesheets/alchemy/admin/tables.scss +32 -3
- data/app/stylesheets/alchemy/admin/toolbar.scss +0 -1
- data/app/stylesheets/alchemy/admin.scss +1 -0
- data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +0 -4
- data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +0 -4
- data/app/types/alchemy/wildcard_url_type.rb +48 -0
- data/app/views/alchemy/_menubar.html.erb +1 -5
- data/app/views/alchemy/admin/attachments/edit.html.erb +6 -3
- data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -2
- data/app/views/alchemy/admin/dashboard/_footer.html.erb +22 -0
- data/app/views/alchemy/admin/dashboard/_stats.html.erb +7 -0
- data/app/views/alchemy/admin/dashboard/_top.html.erb +4 -12
- data/app/views/alchemy/admin/dashboard/_widgets.html.erb +7 -0
- data/app/views/alchemy/admin/dashboard/index.html.erb +0 -17
- data/app/views/alchemy/admin/dashboard/info.html.erb +1 -62
- data/app/views/alchemy/admin/dashboard/widgets/show.html.erb +3 -0
- data/app/views/alchemy/admin/elements/_form.html.erb +2 -1
- data/app/views/alchemy/admin/elements/_schedule.html.erb +2 -15
- data/app/views/alchemy/admin/elements/_schedule_fields.html.erb +2 -0
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +6 -3
- data/app/views/alchemy/admin/nodes/_page_nodes.html.erb +10 -8
- data/app/views/alchemy/admin/pages/_form.html.erb +25 -19
- data/app/views/alchemy/admin/pages/_publication_fields.html.erb +2 -32
- data/app/views/alchemy/admin/pages/_table.html.erb +1 -18
- data/app/views/alchemy/admin/pages/configure.html.erb +2 -2
- data/app/views/alchemy/admin/pages/info.html.erb +6 -0
- data/app/views/alchemy/admin/resources/_form.html.erb +7 -4
- data/app/views/alchemy/admin/resources/edit.html.erb +3 -1
- data/app/views/alchemy/admin/resources/new.html.erb +3 -1
- data/app/views/alchemy/admin/styleguide/index.html.erb +52 -30
- data/app/views/alchemy/admin/translations/_en.js +4 -0
- data/app/views/layouts/alchemy/admin.html.erb +3 -3
- data/config/importmap.rb +2 -0
- data/config/locales/alchemy.en.yml +15 -0
- data/config/routes.rb +1 -0
- data/lib/alchemy/configuration/class_option.rb +46 -3
- data/lib/alchemy/configuration/collection_option.rb +4 -0
- data/lib/alchemy/configurations/dashboard.rb +79 -0
- data/lib/alchemy/configurations/main.rb +15 -0
- data/lib/alchemy/engine.rb +9 -3
- data/lib/alchemy/sprockets/skip_builds_compression.rb +33 -0
- data/lib/alchemy/test_support/capybara_helpers.rb +17 -0
- data/lib/alchemy/test_support/relatable_resource_examples.rb +20 -0
- data/lib/alchemy/test_support/rspec_matchers.rb +8 -0
- data/lib/alchemy/test_support/shared_publishable_examples.rb +38 -31
- data/lib/alchemy/tinymce.rb +1 -1
- data/lib/alchemy/version.rb +17 -3
- data/vendor/javascript/cropperjs.min.js +1 -1
- data/vendor/javascript/flatpickr.min.js +1 -1
- data/vendor/javascript/floating-ui.min.js +1 -0
- data/vendor/javascript/keymaster.min.js +1 -1
- data/vendor/javascript/rails-ujs.min.js +1 -1
- data/vendor/javascript/shoelace.min.js +93 -93
- data/vendor/javascript/sortable.min.js +1 -1
- data/vendor/javascript/tinymce.min.js +5 -1
- data/vendor/javascript/tom-select.min.js +1 -0
- metadata +57 -18
- data/app/javascript/alchemy_admin/components/alchemy_html_element.js +0 -129
- data/app/views/alchemy/admin/dashboard/_left_column.html.erb +0 -4
- data/app/views/alchemy/admin/dashboard/_right_column.html.erb +0 -9
- data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +0 -52
- data/app/views/alchemy/admin/dashboard/widgets/_recent_pages.html.erb +0 -34
- data/app/views/alchemy/admin/dashboard/widgets/_sites.html.erb +0 -25
- data/app/views/alchemy/admin/dashboard/widgets/_users.html.erb +0 -21
- data/app/views/alchemy/admin/languages/edit.html.erb +0 -1
- data/app/views/alchemy/admin/languages/new.html.erb +0 -1
- data/app/views/alchemy/admin/sites/edit.html.erb +0 -1
- data/app/views/alchemy/admin/sites/new.html.erb +0 -1
|
@@ -17,6 +17,9 @@ export default class ImageCropper {
|
|
|
17
17
|
settings.crop_size_form_field_id
|
|
18
18
|
)
|
|
19
19
|
this.elementId = settings.element_id
|
|
20
|
+
this.elementEditor = document.querySelector(
|
|
21
|
+
`[data-element-id='${this.elementId}']`
|
|
22
|
+
)
|
|
20
23
|
this.dialog = Alchemy.currentDialog()
|
|
21
24
|
if (this.dialog) {
|
|
22
25
|
this.dialog.options.closed = () => this.destroy()
|
|
@@ -32,11 +35,7 @@ export default class ImageCropper {
|
|
|
32
35
|
zoomable: false,
|
|
33
36
|
checkCrossOrigin: false, // Prevent CORS issues
|
|
34
37
|
checkOrientation: false, // Prevent loading the image via AJAX which can cause CORS issues
|
|
35
|
-
data: this.box
|
|
36
|
-
cropend: () => {
|
|
37
|
-
const data = this.#cropper.getData(true)
|
|
38
|
-
this.update(data)
|
|
39
|
-
}
|
|
38
|
+
data: this.box
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
|
|
@@ -116,10 +115,9 @@ export default class ImageCropper {
|
|
|
116
115
|
|
|
117
116
|
bind() {
|
|
118
117
|
this.dialog.dialog_body.find('button[type="submit"]').on("click", () => {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
elementEditor.setDirty()
|
|
118
|
+
const data = this.#cropper.getData(true)
|
|
119
|
+
this.update(data)
|
|
120
|
+
this.elementEditor.setDirty()
|
|
123
121
|
this.dialog.close()
|
|
124
122
|
return false
|
|
125
123
|
})
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import Hotkeys from "alchemy_admin/hotkeys"
|
|
2
2
|
import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
|
|
3
|
+
import { openDialog } from "alchemy_admin/dialog"
|
|
4
|
+
|
|
5
|
+
// Opens the help dialog when the user presses the "?" key outside of a field.
|
|
6
|
+
function showHelp(evt) {
|
|
7
|
+
if (
|
|
8
|
+
!$(evt.target).is("input, textarea") &&
|
|
9
|
+
String.fromCharCode(evt.which) === "?"
|
|
10
|
+
) {
|
|
11
|
+
openDialog("/admin/help", {
|
|
12
|
+
title: Alchemy.t("help"),
|
|
13
|
+
size: "400x492"
|
|
14
|
+
})
|
|
15
|
+
return false
|
|
16
|
+
} else {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
3
20
|
|
|
4
21
|
export default function Initializer() {
|
|
5
22
|
// We obviously have javascript enabled.
|
|
@@ -8,6 +25,10 @@ export default function Initializer() {
|
|
|
8
25
|
// Initialize hotkeys.
|
|
9
26
|
Hotkeys()
|
|
10
27
|
|
|
28
|
+
// (Re)bind the help dialog hotkey.
|
|
29
|
+
document.removeEventListener("keypress", showHelp)
|
|
30
|
+
document.addEventListener("keypress", showHelp)
|
|
31
|
+
|
|
11
32
|
// Add observer for please wait overlay.
|
|
12
33
|
document.querySelectorAll(".please_wait").forEach((element) => {
|
|
13
34
|
element.addEventListener("click", pleaseWaitOverlay)
|
|
@@ -48,4 +48,13 @@ tinymce.PluginManager.add("alchemy_link", function (editor) {
|
|
|
48
48
|
// Replace the default link command with our own
|
|
49
49
|
editor.addCommand("mceLink", openLinkDialog)
|
|
50
50
|
editor.addShortcut("Meta+K", "", openLinkDialog)
|
|
51
|
+
|
|
52
|
+
// Override the default link menu item so the contextual menu
|
|
53
|
+
// (right-click) opens the Alchemy link dialog instead of TinyMCE's.
|
|
54
|
+
editor.ui.registry.addMenuItem("link", {
|
|
55
|
+
icon: "link",
|
|
56
|
+
text: "Link...",
|
|
57
|
+
shortcut: "Meta+K",
|
|
58
|
+
onAction: openLinkDialog
|
|
59
|
+
})
|
|
51
60
|
})
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Alchemy
|
|
4
4
|
class BaseJob < ActiveJob::Base
|
|
5
5
|
# Automatically retry jobs that encountered a deadlock
|
|
6
|
-
|
|
6
|
+
retry_on ActiveRecord::Deadlocked
|
|
7
7
|
|
|
8
8
|
# Most jobs are safe to ignore if the underlying records are no longer available
|
|
9
|
-
|
|
9
|
+
discard_on ActiveJob::DeserializationError
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
class InvalidateElementsCacheJob < BaseJob
|
|
3
|
+
queue_as :default
|
|
4
|
+
|
|
5
|
+
def perform(related_object_type, related_object_id)
|
|
6
|
+
element_ids = Ingredient
|
|
7
|
+
.where(related_object_type:, related_object_id:)
|
|
8
|
+
.joins(:element)
|
|
9
|
+
.pluck(:element_id)
|
|
10
|
+
elements = Element.where(id: element_ids)
|
|
11
|
+
|
|
12
|
+
all_element_ids = get_all_element_ids(elements, element_ids)
|
|
13
|
+
Element.where(id: all_element_ids).touch_all
|
|
14
|
+
|
|
15
|
+
page_ids = elements.joins(page_version: :page).select("DISTINCT alchemy_pages.id")
|
|
16
|
+
Page.where(id: page_ids).touch_all
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def get_all_element_ids(elements, element_ids)
|
|
22
|
+
parent_element_ids = elements.where.not(parent_element_id: nil).pluck(:parent_element_id)
|
|
23
|
+
parent_elements = Element.distinct.where(id: parent_element_ids)
|
|
24
|
+
|
|
25
|
+
if parent_elements.any?
|
|
26
|
+
element_ids += parent_element_ids
|
|
27
|
+
get_all_element_ids(parent_elements, element_ids)
|
|
28
|
+
else
|
|
29
|
+
element_ids
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -8,6 +8,8 @@ module Alchemy
|
|
|
8
8
|
|
|
9
9
|
RESERVED_URLNAMES = %w[admin messages new]
|
|
10
10
|
|
|
11
|
+
delegate :wildcard_url, to: :definition
|
|
12
|
+
|
|
11
13
|
included do
|
|
12
14
|
before_validation :set_urlname,
|
|
13
15
|
if: :renamed?,
|
|
@@ -18,6 +20,7 @@ module Alchemy
|
|
|
18
20
|
validates :urlname,
|
|
19
21
|
uniqueness: {scope: [:language_id, :layoutpage], if: -> { urlname.present? }, case_sensitive: false},
|
|
20
22
|
exclusion: {in: RESERVED_URLNAMES}
|
|
23
|
+
validate :unique_wildcard_param_keys, if: :has_wildcard_url?
|
|
21
24
|
|
|
22
25
|
after_update :update_descendants_urlnames,
|
|
23
26
|
if: :saved_change_to_urlname?
|
|
@@ -42,13 +45,32 @@ module Alchemy
|
|
|
42
45
|
end
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
# Returns
|
|
48
|
+
# Returns wildcard url param or the last part of an urlname path
|
|
46
49
|
def slug
|
|
47
50
|
urlname.to_s.split("/").last
|
|
48
51
|
end
|
|
49
52
|
|
|
53
|
+
def has_wildcard_url?
|
|
54
|
+
wildcard_url.present?
|
|
55
|
+
end
|
|
56
|
+
|
|
50
57
|
private
|
|
51
58
|
|
|
59
|
+
def unique_wildcard_param_keys
|
|
60
|
+
conflicting = PageDefinition.all.find do |other|
|
|
61
|
+
other.name != page_layout && other.wildcard_url == wildcard_url
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if conflicting
|
|
65
|
+
errors.add(
|
|
66
|
+
:page_layout,
|
|
67
|
+
:conflicting_wildcard_param_key,
|
|
68
|
+
param: wildcard_url,
|
|
69
|
+
conflicting_layout: conflicting.name
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
52
74
|
def update_descendants_urlnames
|
|
53
75
|
reload
|
|
54
76
|
descendants.each(&:update_urlname!)
|
|
@@ -67,13 +89,14 @@ module Alchemy
|
|
|
67
89
|
end
|
|
68
90
|
|
|
69
91
|
# Returns the full nested urlname.
|
|
70
|
-
#
|
|
92
|
+
# Uses the wildcard_url from the page definition if present,
|
|
93
|
+
# otherwise converts the slug or name to a url-friendly string.
|
|
71
94
|
def nested_url_name
|
|
72
|
-
|
|
95
|
+
url_part = wildcard_url.presence || convert_to_urlname(slug.blank? ? name : slug)
|
|
73
96
|
if parent&.language_root?
|
|
74
|
-
|
|
97
|
+
url_part
|
|
75
98
|
else
|
|
76
|
-
[parent&.urlname,
|
|
99
|
+
[parent&.urlname, url_part].compact.join("/")
|
|
77
100
|
end
|
|
78
101
|
end
|
|
79
102
|
end
|
|
@@ -83,7 +83,8 @@ module Alchemy
|
|
|
83
83
|
{
|
|
84
84
|
public: public?,
|
|
85
85
|
locked: locked?,
|
|
86
|
-
restricted: restricted
|
|
86
|
+
restricted: restricted?,
|
|
87
|
+
scheduled: scheduled?
|
|
87
88
|
}
|
|
88
89
|
end
|
|
89
90
|
|
|
@@ -92,7 +93,11 @@ module Alchemy
|
|
|
92
93
|
# @param [Symbol] status_type
|
|
93
94
|
#
|
|
94
95
|
def status_message(status_type)
|
|
95
|
-
|
|
96
|
+
if status_type == :scheduled && scheduled?
|
|
97
|
+
Alchemy.t(public_on&.future? ? :public_on : :public_until, scope: "page_states.scheduled", public_on: public_on && ::I18n.l(public_on, format: :"alchemy.default"), public_until: public_until && ::I18n.l(public_until, format: :"alchemy.default"))
|
|
98
|
+
else
|
|
99
|
+
Alchemy.t(status[status_type].to_s, scope: "page_states.#{status_type}")
|
|
100
|
+
end
|
|
96
101
|
end
|
|
97
102
|
|
|
98
103
|
# Returns the sort translated status title for given status type.
|
|
@@ -63,11 +63,11 @@ module Alchemy
|
|
|
63
63
|
)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
#
|
|
66
|
+
# Pages that where updated by given user sorted by update time
|
|
67
67
|
#
|
|
68
68
|
scope :all_last_edited_from,
|
|
69
69
|
->(user) {
|
|
70
|
-
where(updater_id: user.id).order("updated_at DESC")
|
|
70
|
+
where(updater_id: user.id).order("updated_at DESC")
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
# Returns all pages that have the given +language_id+
|
|
@@ -20,11 +20,12 @@ module Alchemy
|
|
|
20
20
|
# link_to page.url
|
|
21
21
|
#
|
|
22
22
|
class UrlPath
|
|
23
|
-
def initialize(page, optional_params = {})
|
|
23
|
+
def initialize(page, optional_params = {}, wildcard_params: {})
|
|
24
24
|
@page = page
|
|
25
25
|
@language = @page.language
|
|
26
26
|
@site = @language.site
|
|
27
27
|
@optional_params = optional_params
|
|
28
|
+
@wildcard_params = wildcard_params
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
def call
|
|
@@ -64,7 +65,11 @@ module Alchemy
|
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
def page_path
|
|
67
|
-
|
|
68
|
+
urlname = @page.urlname
|
|
69
|
+
@wildcard_params.each do |key, val|
|
|
70
|
+
urlname = urlname.gsub(":#{key}", val.to_s)
|
|
71
|
+
end
|
|
72
|
+
"#{root_path}#{urlname}"
|
|
68
73
|
end
|
|
69
74
|
|
|
70
75
|
def root_path
|
data/app/models/alchemy/page.rb
CHANGED
|
@@ -305,8 +305,8 @@ module Alchemy
|
|
|
305
305
|
# = The url_path for this page
|
|
306
306
|
#
|
|
307
307
|
# @see Alchemy::Page::UrlPath#call
|
|
308
|
-
def url_path(optional_params = {})
|
|
309
|
-
self.class.url_path_class.new(self, optional_params).call
|
|
308
|
+
def url_path(optional_params = {}, wildcard_params: {})
|
|
309
|
+
self.class.url_path_class.new(self, optional_params, wildcard_params: wildcard_params).call
|
|
310
310
|
end
|
|
311
311
|
|
|
312
312
|
# The page's view partial is dependent from its page layout
|
|
@@ -20,6 +20,7 @@ module Alchemy
|
|
|
20
20
|
attribute :hide, :boolean, default: false
|
|
21
21
|
attribute :editable_by
|
|
22
22
|
attribute :hint
|
|
23
|
+
attribute :wildcard_url, Alchemy::WildcardUrlType.new
|
|
23
24
|
|
|
24
25
|
# Needs to be down here in order to have the attribute reader
|
|
25
26
|
# available after the attribute is defined.
|
|
@@ -94,7 +94,7 @@ module Alchemy
|
|
|
94
94
|
|
|
95
95
|
# Controller actions
|
|
96
96
|
can :leave, :alchemy_admin
|
|
97
|
-
can [:info, :help], :alchemy_admin_dashboard
|
|
97
|
+
can [:info, :help, :show], :alchemy_admin_dashboard
|
|
98
98
|
can :manage, :alchemy_admin_clipboard
|
|
99
99
|
can :update, :alchemy_admin_layoutpages
|
|
100
100
|
can :tree, :alchemy_admin_pages
|
|
@@ -41,6 +41,8 @@ module Alchemy
|
|
|
41
41
|
|
|
42
42
|
has_many :related_elements, through: :related_ingredients, source: :element
|
|
43
43
|
has_many :related_pages, through: :related_elements, source: :page
|
|
44
|
+
|
|
45
|
+
after_touch :touch_related_ingredients, if: -> { related_ingredients.exists? }
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
# Returns true if object is not assigned to any ingredient.
|
|
@@ -48,5 +50,11 @@ module Alchemy
|
|
|
48
50
|
def deletable?
|
|
49
51
|
related_ingredients.none?
|
|
50
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def touch_related_ingredients
|
|
57
|
+
InvalidateElementsCacheJob.perform_later(self.class.name, id)
|
|
58
|
+
end
|
|
51
59
|
end
|
|
52
60
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
class PageFinder
|
|
5
|
+
attr_reader :urlname
|
|
6
|
+
|
|
7
|
+
Result = Data.define(:page, :extracted_params)
|
|
8
|
+
|
|
9
|
+
def initialize(urlname)
|
|
10
|
+
@urlname = urlname
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @return [PageFinder::Result, nil]
|
|
14
|
+
def call
|
|
15
|
+
return if urlname.blank?
|
|
16
|
+
|
|
17
|
+
find_by_urlname || find_by_wildcard_url
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Finds a page by exact urlname match within the current language.
|
|
23
|
+
def find_by_urlname
|
|
24
|
+
page = Current.language.pages.contentpages.find_by(urlname: urlname)
|
|
25
|
+
Result.new(page: page, extracted_params: ActionController::Parameters.new.permit!) if page
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Finds a page whose urlname pattern matches the given URL.
|
|
29
|
+
# Loads all pages whose urlname contains a `:param` segment in a
|
|
30
|
+
# single SQL query, then matches each in Ruby.
|
|
31
|
+
#
|
|
32
|
+
# A urlname may contain more than one `:param` segment when a wildcard
|
|
33
|
+
# page is nested under another wildcard page.
|
|
34
|
+
def find_by_wildcard_url
|
|
35
|
+
return unless any_wildcard_definitions?
|
|
36
|
+
|
|
37
|
+
# Tree depth of a contentpage = urlname slash count + 1 (skipping the language root)
|
|
38
|
+
page_depth = urlname.count("/") + 1
|
|
39
|
+
|
|
40
|
+
wildcard_pages = Current.language.pages.contentpages
|
|
41
|
+
.where("urlname LIKE ?", "%:%")
|
|
42
|
+
.where(depth: page_depth)
|
|
43
|
+
.order(:lft)
|
|
44
|
+
|
|
45
|
+
wildcard_pages.each do |wildcard_page|
|
|
46
|
+
matched_params = match_url_pattern(wildcard_page)
|
|
47
|
+
next unless matched_params
|
|
48
|
+
|
|
49
|
+
# return the first match
|
|
50
|
+
return Result.new(
|
|
51
|
+
page: wildcard_page,
|
|
52
|
+
extracted_params: ActionController::Parameters.new(matched_params).permit!
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Matches the urlname against a page's urlname pattern.
|
|
60
|
+
# Static segments must match literally; each `:param` segment is captured
|
|
61
|
+
# as a single URL segment.
|
|
62
|
+
#
|
|
63
|
+
# @param wildcard_page [Alchemy::Page] a page whose urlname contains one or more `:param` segments
|
|
64
|
+
# @return [Hash<Symbol, String>, nil] matched params or nil
|
|
65
|
+
def match_url_pattern(wildcard_page)
|
|
66
|
+
regex_parts = wildcard_page.urlname.split("/").map do |segment|
|
|
67
|
+
if segment.start_with?(":")
|
|
68
|
+
# create a named capture group for the segment e.g. ":slug" => "(?<slug>.+)"
|
|
69
|
+
"(?<#{segment[1..]}>[\\w\\-]+)"
|
|
70
|
+
else
|
|
71
|
+
# only return the current segment and escape any special regex characters
|
|
72
|
+
Regexp.escape(segment)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# connect regex parts and match them against the urlname
|
|
77
|
+
match = Regexp.new("\\A#{regex_parts.join("/")}\\z").match(urlname)
|
|
78
|
+
|
|
79
|
+
# extract the named capture groups as parameters
|
|
80
|
+
match&.named_captures&.symbolize_keys!
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Boolean] whether any page definition declares a wildcard_url
|
|
84
|
+
def any_wildcard_definitions?
|
|
85
|
+
PageDefinition.all.any?(&:wildcard_url)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
--font-size_small: 10px; /* 0.875rem */
|
|
66
66
|
--font-size_medium: 12px; /* 1rem */
|
|
67
67
|
--font-size_large: 16px; /* 1.25 rem */
|
|
68
|
+
--font-size_xlarge: 24px; /* 2 rem */
|
|
68
69
|
|
|
69
70
|
--font-weight_normal: 500;
|
|
70
71
|
--font-weight_bold: 700;
|
|
@@ -73,6 +74,7 @@
|
|
|
73
74
|
|
|
74
75
|
/* Borders */
|
|
75
76
|
--border-radius_medium: 3px;
|
|
77
|
+
--border-radius_large: 6px;
|
|
76
78
|
--border-width_small: 1px;
|
|
77
79
|
--border-style: solid;
|
|
78
80
|
--border-default: var(--border-width_small) var(--border-style)
|
|
@@ -90,7 +92,7 @@
|
|
|
90
92
|
var(--button-border-color);
|
|
91
93
|
--button-font-size: var(--font-size_medium);
|
|
92
94
|
--button-font-weight: var(--font-weight_bold);
|
|
93
|
-
--button-height:
|
|
95
|
+
--button-height: 31px;
|
|
94
96
|
--button-line-height: var(--form-field-line-height);
|
|
95
97
|
--button-margin: var(--form-field-margin);
|
|
96
98
|
--button-padding: var(--spacing-1) var(--spacing-5);
|
|
@@ -111,12 +113,12 @@
|
|
|
111
113
|
--elements-window-transition-easing: ease-in-out;
|
|
112
114
|
|
|
113
115
|
/* Form */
|
|
114
|
-
--form-left-column-width:
|
|
115
|
-
--form-right-column-width:
|
|
116
|
+
--form-left-column-width: 1fr;
|
|
117
|
+
--form-right-column-width: 2.25fr;
|
|
116
118
|
|
|
117
119
|
/* Form Field */
|
|
118
120
|
--form-field-margin: var(--spacing-1) 0;
|
|
119
|
-
--form-field-height:
|
|
121
|
+
--form-field-height: var(--button-height);
|
|
120
122
|
--form-field-addon-width: 30px;
|
|
121
123
|
--form-field-border-width: var(--border-width_small);
|
|
122
124
|
--form-field-border-style: var(--border-style);
|
|
@@ -84,13 +84,7 @@
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
@mixin form-label {
|
|
87
|
-
width: var(--form-left-column-width);
|
|
88
|
-
padding-right: var(--spacing-2);
|
|
89
|
-
padding-top: 0.6em;
|
|
90
|
-
margin-top: var(--spacing-1);
|
|
91
|
-
vertical-align: top;
|
|
92
87
|
word-break: normal;
|
|
93
|
-
float: left;
|
|
94
88
|
text-align: right;
|
|
95
89
|
font-size: var(--font-size_medium);
|
|
96
90
|
color: var(--form-field-label-color);
|
|
@@ -108,7 +102,7 @@
|
|
|
108
102
|
border: 1px solid $border-color;
|
|
109
103
|
color: $color;
|
|
110
104
|
display: block;
|
|
111
|
-
|
|
105
|
+
grid-column: 2;
|
|
112
106
|
border-radius: var(--border-radius_medium);
|
|
113
107
|
}
|
|
114
108
|
|
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
--code-background-color: var(--a-darkest-grey);
|
|
30
30
|
--code-border-color: var(--a-grey);
|
|
31
31
|
|
|
32
|
+
/* Dashboard */
|
|
33
|
+
--dashboard-widget-background-color: var(--a-darkest-grey);
|
|
34
|
+
--dashboard-widget-box-shadow: none;
|
|
35
|
+
--table-scroll-fade-color: rgba(0, 0, 0, 0);
|
|
36
|
+
--table-scroll-shadow-color: rgba(0, 0, 0, 0.6);
|
|
37
|
+
|
|
32
38
|
/* Datepicker */
|
|
33
39
|
--datepicker-bg-color: var(--a-dark-grey);
|
|
34
40
|
--datepicker-border-color: var(--a-grey);
|
|
@@ -82,7 +88,7 @@
|
|
|
82
88
|
--form-field-background-color: var(--a-darkest-grey);
|
|
83
89
|
--form-field-border-color: var(--border-inset-color);
|
|
84
90
|
--form-field-box-shadow: inset 0 0 1px var(--a-dark-grey);
|
|
85
|
-
--form-field-label-color: var(--text-color
|
|
91
|
+
--form-field-label-color: var(--text-color);
|
|
86
92
|
--form-field-text-color: var(--text-color);
|
|
87
93
|
--form-field-disabled-text-color: var(--form-field-text-color);
|
|
88
94
|
--form-field-disabled-bg-color: transparent;
|
|
@@ -315,6 +321,12 @@
|
|
|
315
321
|
--code-background-color: var(--color-grey_light);
|
|
316
322
|
--code-border-color: var(--border-color);
|
|
317
323
|
|
|
324
|
+
/* Dashboard */
|
|
325
|
+
--dashboard-widget-background-color: var(--color-white);
|
|
326
|
+
--dashboard-widget-box-shadow: 0px 1px 2px 0px rgba(10, 10, 10, 0.1);
|
|
327
|
+
--table-scroll-fade-color: rgba(255, 255, 255, 0);
|
|
328
|
+
--table-scroll-shadow-color: rgba(0, 0, 0, 0.15);
|
|
329
|
+
|
|
318
330
|
/* Datepicker */
|
|
319
331
|
--datepicker-bg-color: var(--color-white);
|
|
320
332
|
--datepicker-border-color: var(--border-color);
|