comfy_middle_seat 4.0.4 → 4.2.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/CHANGELOG.md +45 -1
- data/README.md +20 -1
- data/app/assets/javascripts/comfy/admin/cms/application.js +3 -0
- data/app/assets/javascripts/comfy/admin/cms/base.js +2 -0
- data/app/assets/javascripts/comfy/admin/cms/flash_notices.js +73 -0
- data/app/assets/stylesheets/comfy/admin/cms/application.sass +2 -0
- data/app/assets/stylesheets/comfy/admin/cms/harbor.sass +98 -0
- data/app/assets/stylesheets/comfy/admin/cms/harbor_variables.sass +36 -0
- data/app/controllers/comfy/admin/cms/pages_controller.rb +54 -9
- data/app/controllers/comfy/cms/content_controller.rb +2 -0
- data/app/models/comfy/cms/page.rb +13 -0
- data/app/views/comfy/admin/cms/layouts/_index_branch.html.haml +2 -2
- data/app/views/comfy/admin/cms/pages/_form.html.haml +7 -5
- data/app/views/comfy/admin/cms/pages/_index_branch.html.haml +4 -1
- data/app/views/comfy/admin/cms/pages/edit.html.haml +43 -7
- data/app/views/layouts/comfy/admin/cms/_flash.html.haml +1 -1
- data/config/locales/en.yml +3 -3
- data/lib/comfy_middle_seat/content/tags/breadcrumbs.rb +8 -3
- data/lib/comfy_middle_seat/content/tags/children.rb +10 -5
- data/lib/comfy_middle_seat/content/tags/siblings.rb +14 -10
- data/lib/comfy_middle_seat/version.rb +1 -1
- data/package-lock.json +1 -3
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25bec879fab0f69cf22d27320c22441fd04a2dd10d489223b5f4dfe42d4647e4
|
|
4
|
+
data.tar.gz: 0ca964528011c2f24c8d804c965f08fb46e1dea98d7b9286557bf9f6cc0da27a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ddc4be334bbdd4dcde5f7a703b2f22c8dd6645b79c36ea3ed1e13e77d4d6a077d0e8193c5de10b29520893d7f8463d019921a12553cd3dd5827b9d1d91741ba0
|
|
7
|
+
data.tar.gz: 4ea20039aeef0f0c580252eee42f09bf1804c7376f2944f5c5dba4927e86126bff5d7c681eda6f4de3e1e79ee33f23d16fa01345a06f90b4f6f178ab9aeea80b
|
data/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,47 @@ This changelog follows the project's lineage: **Comfortable Mexican Sofa → Com
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Keep Page editing actions together in a sticky top toolbar.
|
|
18
|
+
- Save edits and publish or unpublish a page with one button.
|
|
19
|
+
- Shorten publication notices and show save hints as button tooltips.
|
|
20
|
+
- Auto-dismiss success and informational notices; keep warnings and errors visible.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Keep editing buttons aligned with the page heading above publication status.
|
|
25
|
+
- Restore dismissal of admin flash notices.
|
|
26
|
+
- Keep Add Child Layout on one line when space allows.
|
|
27
|
+
|
|
28
|
+
## [v4.1.0] - 2026-10-12
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Refresh the admin with the Harbor navy and teal theme.
|
|
33
|
+
- Hide pages beneath unpublished ancestors without changing their publication settings.
|
|
34
|
+
- Show when a page is hidden by an unpublished ancestor.
|
|
35
|
+
- Group descendant publishing actions in the Child pages menu.
|
|
36
|
+
- Gray out descendant publishing actions when no changes are needed.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Refresh page and translation navigation when publication changes.
|
|
41
|
+
|
|
42
|
+
## [v4.0.4] - 2026-10-10
|
|
43
|
+
|
|
44
|
+
No functional changes from v4.0.3.
|
|
45
|
+
|
|
46
|
+
## [v4.0.3] - 2026-10-10
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- Publish or unpublish pages directly from the Page index.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Keep Add Child Page buttons on one line.
|
|
55
|
+
|
|
15
56
|
## [v4.0.2] - 2026-09-21
|
|
16
57
|
|
|
17
58
|
### Changed
|
|
@@ -210,7 +251,10 @@ First release of `comfortable_media_surfer`. This new gem is a revival of [Comfo
|
|
|
210
251
|
|
|
211
252
|
- Rebranded **ComfortableMexicanSofa** as **ComfortableMediaSurfer** in order to publish new gem (database table names and schema have not changed).
|
|
212
253
|
|
|
213
|
-
[Unreleased]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.0
|
|
254
|
+
[Unreleased]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.1.0...master
|
|
255
|
+
[v4.1.0]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.0.4...v4.1.0
|
|
256
|
+
[v4.0.4]: https://github.com/avonderluft/comfy-middle-seat/compare/ea2bb99c0398e1f4d6e850a25e0c13afc577c3ad...v4.0.4
|
|
257
|
+
[v4.0.3]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.0.2...ea2bb99c0398e1f4d6e850a25e0c13afc577c3ad
|
|
214
258
|
[v4.0.2]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.0.1...v4.0.2
|
|
215
259
|
[v4.0.1]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.0.0...v4.0.1
|
|
216
260
|
[v4.0.0]: https://github.com/avonderluft/comfy-middle-seat/compare/v3.2.1...v4.0.0
|
data/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Comfy Middle Seat is best suited to Rails applications that render their own web
|
|
|
34
34
|
- Revision history with comparison, preview, and restore workflows.
|
|
35
35
|
- Page translations and multilingual publishing.
|
|
36
36
|
- Multi-site publishing from one installation.
|
|
37
|
-
-
|
|
37
|
+
- Publish individual pages or whole branches, with ancestor-aware visibility.
|
|
38
38
|
|
|
39
39
|
### For Rails developers
|
|
40
40
|
|
|
@@ -114,6 +114,14 @@ A Site defines a hostname, path, and language. Layouts define page structure and
|
|
|
114
114
|
|
|
115
115
|
When an editor creates a Page using this Layout, the admin automatically presents fields for `title` and `content`. Content tags can also provide Markdown, files, images, dates, numbers, checkboxes, snippets, partials, helpers, navigation, and other reusable content.
|
|
116
116
|
|
|
117
|
+
### Publishing pages
|
|
118
|
+
|
|
119
|
+
A page is publicly visible only when it and all its ancestors are published. Unpublishing a parent hides its branch without changing descendants’ individual publication settings. Republishing the parent restores only descendants whose full ancestor chain is published; drafts stay unpublished. Translations must also satisfy their own publication settings.
|
|
120
|
+
|
|
121
|
+
The Page index toggle changes only that page. The **Child pages** menu’s **Publish descendants** and **Unpublish descendants** actions change all descendants, not the current page or its ancestors. The admin marks published pages hidden by an unpublished ancestor.
|
|
122
|
+
|
|
123
|
+
Development mode continues to show unpublished pages for preview. Check public visibility in test, staging, or production mode.
|
|
124
|
+
|
|
117
125
|
## Compatibility and requirements
|
|
118
126
|
|
|
119
127
|
| Component | Support |
|
|
@@ -143,6 +151,17 @@ When adapting upstream examples, use the `comfy_middle_seat` gem, `config/initia
|
|
|
143
151
|
|
|
144
152
|
## Upgrading
|
|
145
153
|
|
|
154
|
+
### Ancestor-aware publishing (unreleased)
|
|
155
|
+
|
|
156
|
+
Review branches with unpublished ancestors: their descendants will no longer be publicly accessible. No database migration is required. After deploying, clear existing rendered content once from the Rails console so cached navigation follows the new rule:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
Comfy::Cms::Page.update_all(content_cache: nil)
|
|
160
|
+
Comfy::Cms::Translation.update_all(content_cache: nil)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Content is regenerated on demand. Subsequent publication and parent changes refresh the affected site’s caches automatically.
|
|
164
|
+
|
|
146
165
|
### From Comfortable Media Surfer
|
|
147
166
|
|
|
148
167
|
1. Replace `comfortable_media_surfer` with `comfy_middle_seat` in the `Gemfile`. Do not include both gems.
|
|
@@ -4,8 +4,10 @@ import ujs from "@rails/ujs";
|
|
|
4
4
|
window.jQuery = window.$ = jQuery;
|
|
5
5
|
ujs.start();
|
|
6
6
|
|
|
7
|
+
import "bootstrap/js/src/alert";
|
|
7
8
|
import "bootstrap/js/src/tab";
|
|
8
9
|
import "bootstrap/js/src/collapse";
|
|
10
|
+
import "bootstrap/js/src/dropdown";
|
|
9
11
|
|
|
10
12
|
import "../../vendor/diff/diff_match_patch.min";
|
|
11
13
|
import "../../vendor/diff/pretty_text_diff";
|
|
@@ -19,6 +21,7 @@ import "./draft_forms";
|
|
|
19
21
|
import "./file_link";
|
|
20
22
|
import "./file_upload";
|
|
21
23
|
import "./files_modal";
|
|
24
|
+
import "./flash_notices";
|
|
22
25
|
import "./page_fragments";
|
|
23
26
|
import "./sortable_list";
|
|
24
27
|
import "./slugify";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import $ from "jquery";
|
|
2
|
+
import Alert from "bootstrap/js/src/alert";
|
|
3
|
+
|
|
4
|
+
(() => {
|
|
5
|
+
const DELAY = 5000;
|
|
6
|
+
let cleanups = [];
|
|
7
|
+
|
|
8
|
+
const watch = (notice) => {
|
|
9
|
+
let remaining = DELAY;
|
|
10
|
+
let startedAt;
|
|
11
|
+
let timer = null;
|
|
12
|
+
let hovered = notice.matches(":hover");
|
|
13
|
+
let focused = notice.contains(document.activeElement);
|
|
14
|
+
|
|
15
|
+
const pause = () => {
|
|
16
|
+
if (timer === null) return;
|
|
17
|
+
window.clearTimeout(timer);
|
|
18
|
+
timer = null;
|
|
19
|
+
remaining = Math.max(0, remaining - (performance.now() - startedAt));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const resume = () => {
|
|
23
|
+
if (timer !== null || hovered || focused || !notice.isConnected) return;
|
|
24
|
+
startedAt = performance.now();
|
|
25
|
+
timer = window.setTimeout(() => {
|
|
26
|
+
timer = null;
|
|
27
|
+
cleanup();
|
|
28
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
29
|
+
notice.classList.remove("fade");
|
|
30
|
+
}
|
|
31
|
+
new Alert(notice).close();
|
|
32
|
+
}, remaining);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const onMouseEnter = () => { hovered = true; pause(); };
|
|
36
|
+
const onMouseLeave = () => { hovered = false; resume(); };
|
|
37
|
+
const onFocusIn = () => { focused = true; pause(); };
|
|
38
|
+
const onFocusOut = (event) => {
|
|
39
|
+
focused = notice.contains(event.relatedTarget);
|
|
40
|
+
if (!focused) resume();
|
|
41
|
+
};
|
|
42
|
+
const cleanup = () => {
|
|
43
|
+
pause();
|
|
44
|
+
notice.removeEventListener("mouseenter", onMouseEnter);
|
|
45
|
+
notice.removeEventListener("mouseleave", onMouseLeave);
|
|
46
|
+
notice.removeEventListener("focusin", onFocusIn);
|
|
47
|
+
notice.removeEventListener("focusout", onFocusOut);
|
|
48
|
+
$(notice).off("close.bs.alert", cleanup);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
notice.addEventListener("mouseenter", onMouseEnter);
|
|
52
|
+
notice.addEventListener("mouseleave", onMouseLeave);
|
|
53
|
+
notice.addEventListener("focusin", onFocusIn);
|
|
54
|
+
notice.addEventListener("focusout", onFocusOut);
|
|
55
|
+
$(notice).on("close.bs.alert", cleanup);
|
|
56
|
+
cleanups.push(cleanup);
|
|
57
|
+
resume();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
CMS.flashNotices = {
|
|
61
|
+
init() {
|
|
62
|
+
this.dispose();
|
|
63
|
+
document.querySelectorAll(
|
|
64
|
+
"[data-cms-flash].alert-success, [data-cms-flash].alert-info"
|
|
65
|
+
).forEach(watch);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
dispose() {
|
|
69
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
70
|
+
cleanups = [];
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
})();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
@import "comfy/admin/cms/harbor_variables"
|
|
1
2
|
@import "comfy/vendor/bootstrap/bootstrap"
|
|
2
3
|
|
|
3
4
|
@import "codemirror/lib/codemirror"
|
|
@@ -8,4 +9,5 @@
|
|
|
8
9
|
@import "comfy/admin/cms/codemirror_overrides"
|
|
9
10
|
@import "comfy/admin/cms/redactor_overrides"
|
|
10
11
|
@import "comfy/admin/cms/base"
|
|
12
|
+
@import "comfy/admin/cms/harbor"
|
|
11
13
|
@import "comfy/admin/cms/custom"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
body#comfy
|
|
2
|
+
.btn-primary, .btn-secondary
|
|
3
|
+
@include button-variant($harbor-teal, $harbor-teal, $harbor-hover, $harbor-hover, $harbor-pressed, $harbor-pressed)
|
|
4
|
+
|
|
5
|
+
.btn-outline-primary, .btn-outline-secondary
|
|
6
|
+
@include button-outline-variant($harbor-teal, $harbor-hover, $harbor-tint, $harbor-hover)
|
|
7
|
+
background-color: $white
|
|
8
|
+
border-color: $gray-400
|
|
9
|
+
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active
|
|
10
|
+
color: $harbor-pressed
|
|
11
|
+
&:disabled, &.disabled
|
|
12
|
+
background-color: $white
|
|
13
|
+
border-color: $gray-300
|
|
14
|
+
|
|
15
|
+
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible
|
|
16
|
+
outline: 2px solid $harbor-teal
|
|
17
|
+
outline-offset: 2px
|
|
18
|
+
|
|
19
|
+
#cms-left
|
|
20
|
+
nav.navbar ul.nav li
|
|
21
|
+
a
|
|
22
|
+
color: $harbor-nav-text
|
|
23
|
+
@include transition(color .15s ease-in-out, background-color .15s ease-in-out)
|
|
24
|
+
a:hover
|
|
25
|
+
color: $white
|
|
26
|
+
background-color: $gray-700
|
|
27
|
+
a.active
|
|
28
|
+
color: $white
|
|
29
|
+
background-color: $harbor-teal
|
|
30
|
+
box-shadow: inset 3px 0 0 $harbor-gold
|
|
31
|
+
a.active:hover
|
|
32
|
+
background-color: $harbor-hover
|
|
33
|
+
a:focus-visible, button:focus-visible
|
|
34
|
+
outline-color: $harbor-gold
|
|
35
|
+
.left-footer
|
|
36
|
+
color: $harbor-nav-text
|
|
37
|
+
a
|
|
38
|
+
color: $harbor-nav-text
|
|
39
|
+
&:hover
|
|
40
|
+
color: $white
|
|
41
|
+
|
|
42
|
+
#cms-main
|
|
43
|
+
.page-header
|
|
44
|
+
color: $harbor-navy
|
|
45
|
+
.page-editor-toolbar
|
|
46
|
+
position: sticky
|
|
47
|
+
top: 0
|
|
48
|
+
z-index: 1000
|
|
49
|
+
display: grid
|
|
50
|
+
grid-template-columns: max-content minmax(0, 1fr)
|
|
51
|
+
align-items: center
|
|
52
|
+
gap: .5rem 1rem
|
|
53
|
+
padding: .75rem 0
|
|
54
|
+
background-color: $body-bg
|
|
55
|
+
.page-editor-heading h2
|
|
56
|
+
margin-bottom: 0
|
|
57
|
+
.page-publication-state
|
|
58
|
+
grid-column: 1 / -1
|
|
59
|
+
.page-editor-actions
|
|
60
|
+
display: flex
|
|
61
|
+
flex-wrap: wrap
|
|
62
|
+
align-items: center
|
|
63
|
+
justify-content: flex-end
|
|
64
|
+
gap: .5rem
|
|
65
|
+
min-width: 0
|
|
66
|
+
|
|
67
|
+
@media (max-width: map-get($grid-breakpoints, lg))
|
|
68
|
+
top: 3rem
|
|
69
|
+
@include media-breakpoint-down(sm)
|
|
70
|
+
grid-template-columns: minmax(0, 1fr)
|
|
71
|
+
.page-editor-actions
|
|
72
|
+
justify-content: flex-start
|
|
73
|
+
.layout-actions .btn-group
|
|
74
|
+
max-width: 100%
|
|
75
|
+
ul.list
|
|
76
|
+
li .row
|
|
77
|
+
@include transition(background-color .15s ease-in-out)
|
|
78
|
+
.item .item-title a
|
|
79
|
+
color: $gray-800
|
|
80
|
+
&:hover
|
|
81
|
+
color: $harbor-hover
|
|
82
|
+
&.draft
|
|
83
|
+
color: $gray-600
|
|
84
|
+
.item .item-controls .dragger, .item .item-controls .toggle
|
|
85
|
+
color: $gray-600
|
|
86
|
+
li.sortable-ghost > .row, li.sortable-chosen > .row, &:not(.sortable-active) > li > .row:hover
|
|
87
|
+
background-color: $harbor-tint
|
|
88
|
+
|
|
89
|
+
#cms-right
|
|
90
|
+
.box
|
|
91
|
+
border-color: $gray-300
|
|
92
|
+
.box.translations a.translation
|
|
93
|
+
color: $gray-800
|
|
94
|
+
&.draft
|
|
95
|
+
color: $gray-600
|
|
96
|
+
|
|
97
|
+
#cms-uploader ul.list .file-path
|
|
98
|
+
color: $gray-600
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Loaded before Bootstrap so forms, alerts, badges, and pagination share the palette.
|
|
2
|
+
$harbor-navy: #172b3a
|
|
3
|
+
$harbor-teal: #0f766e
|
|
4
|
+
$harbor-hover: #0b5e57
|
|
5
|
+
$harbor-pressed: #094c47
|
|
6
|
+
$harbor-tint: #e0f2ef
|
|
7
|
+
$harbor-gold: #d5a34a
|
|
8
|
+
$harbor-nav-text: #bfd0dc
|
|
9
|
+
|
|
10
|
+
$gray-100: #f3f7fa
|
|
11
|
+
$gray-200: #e4ecf1
|
|
12
|
+
$gray-300: #cbd7e0
|
|
13
|
+
$gray-400: #91aab9
|
|
14
|
+
$gray-500: #718999
|
|
15
|
+
$gray-600: #526777
|
|
16
|
+
$gray-700: #385364
|
|
17
|
+
$gray-800: #243746
|
|
18
|
+
$gray-900: $harbor-navy
|
|
19
|
+
|
|
20
|
+
$primary: $harbor-teal
|
|
21
|
+
$secondary: $harbor-teal
|
|
22
|
+
$success: #26734d
|
|
23
|
+
$info: #246b8c
|
|
24
|
+
$warning: $harbor-gold
|
|
25
|
+
$danger: #b23a48
|
|
26
|
+
$dark: $harbor-navy
|
|
27
|
+
$light: $gray-100
|
|
28
|
+
$body-color: $gray-800
|
|
29
|
+
$body-bg: #ffffff
|
|
30
|
+
$link-color: $harbor-teal
|
|
31
|
+
$link-hover-color: $harbor-hover
|
|
32
|
+
$input-border-color: $gray-400
|
|
33
|
+
$input-focus-border-color: $harbor-teal
|
|
34
|
+
$input-focus-box-shadow: 0 0 0 .2rem rgba($harbor-teal, .2)
|
|
35
|
+
$custom-select-focus-border-color: $harbor-teal
|
|
36
|
+
$custom-select-focus-box-shadow: $input-focus-box-shadow
|
|
@@ -8,7 +8,7 @@ class Comfy::Admin::Cms::PagesController < Comfy::Admin::Cms::BaseController
|
|
|
8
8
|
|
|
9
9
|
include ::Comfy::ReorderAction
|
|
10
10
|
|
|
11
|
-
helper_method :page_branch_open
|
|
11
|
+
helper_method :page_branch_open?, :page_publicly_visible?, :descendant_publication_states
|
|
12
12
|
|
|
13
13
|
self.reorder_action_resource = ::Comfy::Cms::Page
|
|
14
14
|
|
|
@@ -64,12 +64,28 @@ class Comfy::Admin::Cms::PagesController < Comfy::Admin::Cms::BaseController
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def update
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
publication_action = params[:publication_action].presence_in(%w[publish unpublish])
|
|
68
|
+
original_publication = @page.is_published_in_database
|
|
69
|
+
@page.is_published = publication_action == 'publish' if publication_action
|
|
70
|
+
|
|
71
|
+
@page.class.transaction do
|
|
72
|
+
@page.save!
|
|
73
|
+
update_family
|
|
74
|
+
end
|
|
69
75
|
acknowledge_draft_save
|
|
70
|
-
flash[:success] =
|
|
76
|
+
flash[:success] = case publication_action
|
|
77
|
+
when 'publish'
|
|
78
|
+
I18n.t('comfy.admin.cms.pages.saved_and_published', default: 'Page saved and published')
|
|
79
|
+
when 'unpublish'
|
|
80
|
+
I18n.t('comfy.admin.cms.pages.saved_and_unpublished', default: 'Page saved and unpublished')
|
|
81
|
+
else
|
|
82
|
+
I18n.t('comfy.admin.cms.pages.updated')
|
|
83
|
+
end
|
|
71
84
|
redirect_to action: :edit, id: @page
|
|
72
|
-
rescue ActiveRecord::RecordInvalid
|
|
85
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
86
|
+
# Keep submitted edits and errors, but show the publication state that was actually saved.
|
|
87
|
+
@page.is_published = original_publication if publication_action
|
|
88
|
+
@page.errors.add(:base, e.message) unless e.record == @page
|
|
73
89
|
flash.now[:danger] = I18n.t('comfy.admin.cms.pages.update_failure')
|
|
74
90
|
render action: :edit
|
|
75
91
|
end
|
|
@@ -135,12 +151,27 @@ class Comfy::Admin::Cms::PagesController < Comfy::Admin::Cms::BaseController
|
|
|
135
151
|
|
|
136
152
|
protected
|
|
137
153
|
|
|
154
|
+
# Query one tree level at a time; mixed flags already enable both actions.
|
|
155
|
+
def descendant_publication_states(page)
|
|
156
|
+
states = []
|
|
157
|
+
parent_ids = [page.id]
|
|
158
|
+
while parent_ids.present? && states.size < 2
|
|
159
|
+
descendants = @site.pages.where(parent_id: parent_ids).pluck(:id, :is_published)
|
|
160
|
+
states |= descendants.map(&:last)
|
|
161
|
+
parent_ids = descendants.map(&:first)
|
|
162
|
+
end
|
|
163
|
+
states
|
|
164
|
+
end
|
|
165
|
+
|
|
138
166
|
def update_children_publication(is_published, message)
|
|
139
167
|
descendant_ids = @page.descendants.map(&:id)
|
|
140
|
-
@
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
|
|
168
|
+
@page.class.transaction do
|
|
169
|
+
@site.pages
|
|
170
|
+
.where(id: descendant_ids)
|
|
171
|
+
.update_all(is_published: is_published, updated_at: Time.current)
|
|
172
|
+
@page.clear_site_content_cache!
|
|
173
|
+
end
|
|
174
|
+
default = is_published ? 'Descendant pages published' : 'Descendant pages unpublished'
|
|
144
175
|
flash[:success] = I18n.t("comfy.admin.cms.pages.#{message}", default: default)
|
|
145
176
|
redirect_to action: :edit, id: @page
|
|
146
177
|
end
|
|
@@ -189,6 +220,20 @@ protected
|
|
|
189
220
|
pages_by_parent
|
|
190
221
|
end
|
|
191
222
|
|
|
223
|
+
# Reuse loaded tree nodes and memoize shared ancestors for filtered results.
|
|
224
|
+
def page_publicly_visible?(page)
|
|
225
|
+
@page_visibility ||= {}
|
|
226
|
+
return @page_visibility[page.id] if @page_visibility.key?(page.id)
|
|
227
|
+
|
|
228
|
+
@visibility_pages ||= (Array(@pages) + [@page] + (@pages_by_parent || {}).values.flatten)
|
|
229
|
+
.compact.index_by(&:id)
|
|
230
|
+
@page_visibility[page.id] = page.is_published? && (
|
|
231
|
+
page.parent_id.nil? || page_publicly_visible?(
|
|
232
|
+
@visibility_pages[page.parent_id] ||= @site.pages.find(page.parent_id)
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
192
237
|
def page_branch_open?(page)
|
|
193
238
|
page.root? || page_tree_session.member?(page.id.to_s)
|
|
194
239
|
end
|
|
@@ -27,6 +27,9 @@ class Comfy::Cms::Page < ActiveRecord::Base
|
|
|
27
27
|
:assign_full_path
|
|
28
28
|
before_create :assign_position
|
|
29
29
|
after_save :sync_child_full_paths!
|
|
30
|
+
after_save :clear_site_content_cache!,
|
|
31
|
+
if: -> { is_published_previously_changed? || parent_id_previously_changed? }
|
|
32
|
+
after_destroy :clear_site_content_cache!
|
|
30
33
|
after_find :unescape_slug_and_path
|
|
31
34
|
|
|
32
35
|
# -- Validations -------------------------------------------------------------
|
|
@@ -89,6 +92,16 @@ class Comfy::Cms::Page < ActiveRecord::Base
|
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
# -- Instance Methods --------------------------------------------------------
|
|
95
|
+
def published_with_ancestors?
|
|
96
|
+
is_published? && ancestors.all?(&:is_published?)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def clear_site_content_cache!
|
|
100
|
+
pages = Comfy::Cms::Page.where(site_id: site_id)
|
|
101
|
+
pages.update_all(content_cache: nil)
|
|
102
|
+
Comfy::Cms::Translation.where(page_id: pages.select(:id)).update_all(content_cache: nil)
|
|
103
|
+
end
|
|
104
|
+
|
|
92
105
|
# For previewing purposes sometimes we need to have full_path set. This
|
|
93
106
|
# full path take care of the pages and its childs but not of the site path
|
|
94
107
|
def full_path
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
%li{data: {id: layout.id}}
|
|
5
5
|
.row
|
|
6
|
-
.col-md
|
|
6
|
+
.col-12.col-md.item
|
|
7
7
|
.item-controls.d-none.d-md-block
|
|
8
8
|
- if has_siblings
|
|
9
9
|
.dragger
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
.item-meta
|
|
16
16
|
= layout.identifier
|
|
17
17
|
|
|
18
|
-
.col-md-
|
|
18
|
+
.col-12.col-md-auto.layout-actions.d-flex.align-items-center.justify-content-md-end
|
|
19
19
|
.btn-group.btn-group-sm
|
|
20
20
|
= link_to t('.add_child_layout'), new_comfy_admin_cms_site_layout_path(@site, parent_id: layout.id), class: "btn btn-outline-secondary"
|
|
21
21
|
= link_to t('.edit'), edit_comfy_admin_cms_site_layout_path(@site, layout), class: "btn btn-outline-secondary"
|
|
@@ -22,11 +22,13 @@
|
|
|
22
22
|
- if (options = ::Comfy::Cms::Page.options_for_select(site: @site, current_page: @page)).present?
|
|
23
23
|
= form.select :target_page_id, [["---- #{t('.select_target_page')} ----", nil]] + options, bootstrap: {custom_control: true}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
- if @page.new_record?
|
|
26
|
+
= form.check_box :is_published
|
|
26
27
|
|
|
27
28
|
= comfy_admin_partial "comfy/admin/cms/partials/page_form_after", form: form
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
=
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
- if @page.new_record?
|
|
31
|
+
= form.form_actions do
|
|
32
|
+
= submit_tag t(".preview"), name: "preview", formtarget: "comfy-cms-preview", id: nil, class: "btn btn-secondary"
|
|
33
|
+
= submit_tag t(".create"), class: "btn btn-primary ml-sm-1"
|
|
34
|
+
= link_to t(".cancel"), comfy_admin_cms_site_pages_path, class: "btn btn-link"
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- has_children = children.present? || page.children_count.to_i.positive?
|
|
6
6
|
- has_siblings = siblings.size > 1
|
|
7
7
|
- branch_open = page_branch_open?(page)
|
|
8
|
+
- publicly_visible = page_publicly_visible?(page)
|
|
8
9
|
|
|
9
10
|
%li{id: dom_id(page), data: {id: page.id}}
|
|
10
11
|
.row
|
|
@@ -25,10 +26,12 @@
|
|
|
25
26
|
|
|
26
27
|
.item-content
|
|
27
28
|
.item-title
|
|
28
|
-
- published_css_class =
|
|
29
|
+
- published_css_class = publicly_visible ? "published" : "draft"
|
|
29
30
|
= link_to page.label, edit_comfy_admin_cms_site_page_path(@site, page), class: published_css_class
|
|
30
31
|
.item-meta
|
|
31
32
|
= link_to page.url, page.url, target: "_blank"
|
|
33
|
+
- if page.is_published? && !publicly_visible
|
|
34
|
+
.publication-status= t('.hidden_by_ancestor', default: 'Hidden by unpublished ancestor')
|
|
32
35
|
|
|
33
36
|
.col-md-2.item-categories.d-flex.align-items-center
|
|
34
37
|
= render "comfy/admin/cms/categories/categories", object: page
|
|
@@ -3,14 +3,50 @@
|
|
|
3
3
|
= render "comfy/admin/cms/revisions/sidebar", record: @page, link: link
|
|
4
4
|
= render "comfy/admin/cms/translations/sidebar"
|
|
5
5
|
|
|
6
|
-
.page-header.d-flex.justify-content-between.align-items-center
|
|
7
|
-
%h2= t('.title')
|
|
8
|
-
- if @page.children.exists?
|
|
9
|
-
.btn-group.btn-group-sm
|
|
10
|
-
= link_to t('.publish_children', default: 'Publish children'), publish_children_comfy_admin_cms_site_page_path(@site, @page), method: :patch, data: {confirm: t('.publish_children_confirmation', default: 'Publish all child pages?')}, class: 'btn btn-success'
|
|
11
|
-
= link_to t('.unpublish_children', default: 'Unpublish children'), unpublish_children_comfy_admin_cms_site_page_path(@site, @page), method: :patch, data: {confirm: t('.unpublish_children_confirmation', default: 'Unpublish all child pages?')}, class: 'btn btn-outline-secondary'
|
|
12
|
-
|
|
13
6
|
- draft_data = cms_draft_form_data('site', @site.id, 'page', @page.id, record: @page)
|
|
14
7
|
= comfy_form_with model: @page, scope: :page, url: {action: :update}, html: {multipart: true, data: draft_data} do |form|
|
|
15
8
|
= hidden_field_tag :cms_draft_key, draft_data[:cms_draft_key]
|
|
9
|
+
.page-header.page-editor-toolbar
|
|
10
|
+
.page-editor-heading
|
|
11
|
+
%h2= t('.title')
|
|
12
|
+
|
|
13
|
+
.page-editor-actions
|
|
14
|
+
= submit_tag t('comfy.admin.cms.pages.form.preview'), name: 'preview', formtarget: 'comfy-cms-preview', id: nil, class: 'btn btn-sm btn-outline-secondary text-nowrap', data: {disable_with: false}
|
|
15
|
+
= submit_tag t('comfy.admin.cms.pages.form.update'), class: 'btn btn-sm btn-primary text-nowrap', data: {disable_with: false}
|
|
16
|
+
- publication_action = @page.is_published? ? 'unpublish' : 'publish'
|
|
17
|
+
- publication_label = @page.is_published? ? t('.unpublish', default: 'Unpublish') : t('.publish', default: 'Publish')
|
|
18
|
+
- publication_hint = @page.is_published? ? t('.save_and_unpublish_hint', default: 'Save and unpublish page') : t('.save_and_publish_hint', default: 'Save and publish page')
|
|
19
|
+
= button_tag publication_label, name: 'publication_action', value: publication_action, class: 'btn btn-sm btn-outline-secondary text-nowrap', title: publication_hint, aria: {describedby: 'page-publication-hint'}
|
|
20
|
+
- if @page.children.exists?
|
|
21
|
+
- publication_states = descendant_publication_states(@page)
|
|
22
|
+
.dropdown
|
|
23
|
+
%button#child-pages-menu.btn.btn-sm.btn-outline-secondary.dropdown-toggle.text-nowrap{type: 'button', data: {toggle: 'dropdown'}, aria: {haspopup: 'true', expanded: 'false', controls: 'child-pages-actions'}}
|
|
24
|
+
= t('.child_pages', default: 'Child pages')
|
|
25
|
+
#child-pages-actions.dropdown-menu.dropdown-menu-right{aria: {labelledby: 'child-pages-menu'}}
|
|
26
|
+
- if publication_states.include?(false)
|
|
27
|
+
= link_to t('.publish_descendants', default: 'Publish descendants'), publish_children_comfy_admin_cms_site_page_path(@site, @page), method: :patch, data: {confirm: t('.publish_descendants_confirmation', default: 'Publish all descendant pages?')}, class: 'dropdown-item'
|
|
28
|
+
- else
|
|
29
|
+
%button.dropdown-item.disabled{type: 'button', disabled: true, aria: {disabled: 'true'}}= t('.publish_descendants', default: 'Publish descendants')
|
|
30
|
+
- if publication_states.include?(true)
|
|
31
|
+
= link_to t('.unpublish_descendants', default: 'Unpublish descendants'), unpublish_children_comfy_admin_cms_site_page_path(@site, @page), method: :patch, data: {confirm: t('.unpublish_descendants_confirmation', default: 'Unpublish all descendant pages?')}, class: 'dropdown-item'
|
|
32
|
+
- else
|
|
33
|
+
%button.dropdown-item.disabled{type: 'button', disabled: true, aria: {disabled: 'true'}}= t('.unpublish_descendants', default: 'Unpublish descendants')
|
|
34
|
+
.dropdown-divider
|
|
35
|
+
.px-4.py-1.small.text-muted= t('.descendants_hint', default: 'Changes descendants only; does not save this page’s edits.')
|
|
36
|
+
= link_to t('comfy.admin.cms.pages.form.cancel'), comfy_admin_cms_site_pages_path(@site), class: 'btn btn-sm btn-link text-nowrap'
|
|
37
|
+
.page-publication-state.small.text-muted{role: 'status'}
|
|
38
|
+
- if !@page.is_published?
|
|
39
|
+
= t('.unpublished_notice', default: 'Unpublished.')
|
|
40
|
+
= t('.branch_visibility_notice', default: 'Descendants hidden, with their publication settings unchanged.')
|
|
41
|
+
- elsif !@page.published_with_ancestors?
|
|
42
|
+
= t('.hidden_status', default: 'Hidden by unpublished ancestor')
|
|
43
|
+
- else
|
|
44
|
+
= t('.published', default: 'Published')
|
|
45
|
+
%span#page-publication-hint{hidden: true}= publication_hint
|
|
46
|
+
|
|
47
|
+
- if @page.is_published? && !@page.published_with_ancestors?
|
|
48
|
+
.alert.alert-info.publication-status{role: 'status'}
|
|
49
|
+
= t('.hidden_by_ancestor', default: 'Hidden by unpublished ancestor.')
|
|
50
|
+
= t('.branch_visibility_notice', default: 'Descendants hidden, with their publication settings unchanged.')
|
|
51
|
+
|
|
16
52
|
= render form
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
- flash.to_hash.except('cms_saved_draft_key').each do |type, message|
|
|
2
|
-
.alert{:
|
|
2
|
+
.alert.fade.show{class: "alert-#{type}", data: {cms_flash: true}, role: %w[success info].include?(type) ? 'status' : 'alert'}
|
|
3
3
|
%button.close{:type => 'button', :class => "close", :data => {:dismiss => 'alert'}} ×
|
|
4
4
|
= message
|
data/config/locales/en.yml
CHANGED
|
@@ -123,10 +123,10 @@ en:
|
|
|
123
123
|
pages:
|
|
124
124
|
created: Page created, siblings, and parent updated
|
|
125
125
|
creation_failure: Failed to create page
|
|
126
|
-
updated: Page
|
|
126
|
+
updated: Page updated
|
|
127
127
|
update_failure: Failed to update page
|
|
128
|
-
children_published:
|
|
129
|
-
children_unpublished:
|
|
128
|
+
children_published: Descendant pages published
|
|
129
|
+
children_unpublished: Descendant pages unpublished
|
|
130
130
|
deleted: Page deleted, siblings, and parent updated
|
|
131
131
|
not_found: Page not found
|
|
132
132
|
layout_not_found: No Layouts found. Please create one.
|
|
@@ -24,16 +24,21 @@ class ComfyMiddleSeat::Content::Tags::Breadcrumbs < ComfyMiddleSeat::Content::Ta
|
|
|
24
24
|
@style = ''
|
|
25
25
|
@style = "<style>#breadcrumbs {#{@locals['style']}}</style>" if @locals['style']
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
page = context.is_a?(Comfy::Cms::Translation) ? context.page : context
|
|
28
|
+
@visible = Rails.env.development? || page.published_with_ancestors?
|
|
29
|
+
@links = ''
|
|
30
|
+
return unless @visible
|
|
30
31
|
|
|
32
|
+
@links = '<div id="breadcrumbs">'
|
|
33
|
+
page.ancestors.reverse.each do |a|
|
|
31
34
|
@links += "<a href=#{a.url(relative: true)}>#{a.label}</a> » "
|
|
32
35
|
end
|
|
33
36
|
@links += "#{context.label}</div>"
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
def content
|
|
40
|
+
return '' unless @visible
|
|
41
|
+
|
|
37
42
|
"#{@style}#{@links}"
|
|
38
43
|
end
|
|
39
44
|
end
|
|
@@ -27,15 +27,20 @@ class ComfyMiddleSeat::Content::Tags::Children < ComfyMiddleSeat::Content::Tag
|
|
|
27
27
|
@exclude = []
|
|
28
28
|
@exclude = @locals['exclude'].split(',').map(&:strip) if @locals['exclude']
|
|
29
29
|
@list = ''
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
page = context.is_a?(Comfy::Cms::Translation) ? context.page : context
|
|
31
|
+
preview = Rails.env.development?
|
|
32
|
+
@visible = preview || page.published_with_ancestors?
|
|
33
|
+
@page_children = []
|
|
34
|
+
return unless @visible
|
|
35
|
+
|
|
36
|
+
@page_children = page.children.order(:position).to_ary
|
|
37
|
+
@page_children.select!(&:is_published?) unless preview
|
|
35
38
|
@page_children.delete_if { |child| @exclude.include? child.slug }
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
def content
|
|
42
|
+
return '' unless @visible
|
|
43
|
+
|
|
39
44
|
if @page_children.any?
|
|
40
45
|
@list = "<ul id=\"children\">\n"
|
|
41
46
|
@page_children.each do |c|
|
|
@@ -29,28 +29,32 @@ class ComfyMiddleSeat::Content::Tags::Siblings < ComfyMiddleSeat::Content::Tag
|
|
|
29
29
|
@exclude = []
|
|
30
30
|
@exclude = @locals['exclude'].split(',') if @locals['exclude']
|
|
31
31
|
@links = ''
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
@page = context.is_a?(Comfy::Cms::Translation) ? context.page : context
|
|
33
|
+
preview = Rails.env.development?
|
|
34
|
+
@visible = preview || @page.published_with_ancestors?
|
|
35
|
+
@sibs = []
|
|
36
|
+
return unless @visible
|
|
37
|
+
|
|
38
|
+
@sibs = @page.self_and_siblings.order(:position).to_ary
|
|
39
|
+
@sibs.select!(&:is_published?) unless preview
|
|
37
40
|
@sibs.delete_if { |sib| @exclude.include? sib.slug }
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
def content
|
|
44
|
+
return '' unless @visible
|
|
45
|
+
|
|
41
46
|
if @sibs.count > 1
|
|
42
47
|
@links = '<div id="siblings">'
|
|
43
48
|
prevp = false
|
|
44
49
|
@sibs.each do |sib|
|
|
45
50
|
sib_idx = @sibs.index(sib)
|
|
46
|
-
next if sib.slug ==
|
|
47
|
-
next
|
|
48
|
-
next unless @sibs.index(context) # current page is excluded
|
|
51
|
+
next if sib.slug == @page.slug
|
|
52
|
+
next unless @sibs.index(@page) # current page is excluded
|
|
49
53
|
|
|
50
|
-
if sib_idx == @sibs.index(
|
|
54
|
+
if sib_idx == @sibs.index(@page) - 1
|
|
51
55
|
@links += "<a href=#{sib.url(relative: true)}>#{sib.label}</a> « <em>Previous</em> • "
|
|
52
56
|
prevp = true
|
|
53
|
-
elsif sib_idx == @sibs.index(
|
|
57
|
+
elsif sib_idx == @sibs.index(@page) + 1
|
|
54
58
|
@links += '•' unless prevp
|
|
55
59
|
@links += "<em>Next</em> » <a href=#{sib.url(relative: true)}>#{sib.label}</a>"
|
|
56
60
|
end
|
data/package-lock.json
CHANGED
|
@@ -868,8 +868,7 @@
|
|
|
868
868
|
"version": "3.7.1",
|
|
869
869
|
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
|
870
870
|
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
|
|
871
|
-
"license": "MIT"
|
|
872
|
-
"peer": true
|
|
871
|
+
"license": "MIT"
|
|
873
872
|
},
|
|
874
873
|
"node_modules/micromatch": {
|
|
875
874
|
"version": "4.0.8",
|
|
@@ -918,7 +917,6 @@
|
|
|
918
917
|
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
|
|
919
918
|
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
|
|
920
919
|
"license": "MIT",
|
|
921
|
-
"peer": true,
|
|
922
920
|
"funding": {
|
|
923
921
|
"type": "opencollective",
|
|
924
922
|
"url": "https://opencollective.com/popperjs"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: comfy_middle_seat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0
|
|
4
|
+
version: 4.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleg Khabarov
|
|
@@ -237,6 +237,7 @@ files:
|
|
|
237
237
|
- app/assets/javascripts/comfy/admin/cms/file_link.js
|
|
238
238
|
- app/assets/javascripts/comfy/admin/cms/file_upload.js
|
|
239
239
|
- app/assets/javascripts/comfy/admin/cms/files_modal.js
|
|
240
|
+
- app/assets/javascripts/comfy/admin/cms/flash_notices.js
|
|
240
241
|
- app/assets/javascripts/comfy/admin/cms/page_fragments.js
|
|
241
242
|
- app/assets/javascripts/comfy/admin/cms/slugify.js
|
|
242
243
|
- app/assets/javascripts/comfy/admin/cms/sortable_list.js
|
|
@@ -306,6 +307,8 @@ files:
|
|
|
306
307
|
- app/assets/stylesheets/comfy/admin/cms/base.sass
|
|
307
308
|
- app/assets/stylesheets/comfy/admin/cms/codemirror_overrides.sass
|
|
308
309
|
- app/assets/stylesheets/comfy/admin/cms/custom.sass
|
|
310
|
+
- app/assets/stylesheets/comfy/admin/cms/harbor.sass
|
|
311
|
+
- app/assets/stylesheets/comfy/admin/cms/harbor_variables.sass
|
|
309
312
|
- app/assets/stylesheets/comfy/admin/cms/redactor_overrides.sass
|
|
310
313
|
- app/assets/stylesheets/comfy/vendor/bootstrap/_alert.scss
|
|
311
314
|
- app/assets/stylesheets/comfy/vendor/bootstrap/_badge.scss
|