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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4278365e6cb2036d0909007c27e36878cb58dd915c801f4b9e0c9814a632eaf
4
- data.tar.gz: 3d17520fe067057e477f7b269fe1eed2afe014846873d21cc5e68eda3041d143
3
+ metadata.gz: 25bec879fab0f69cf22d27320c22441fd04a2dd10d489223b5f4dfe42d4647e4
4
+ data.tar.gz: 0ca964528011c2f24c8d804c965f08fb46e1dea98d7b9286557bf9f6cc0da27a
5
5
  SHA512:
6
- metadata.gz: 046346cefdb3ded2bd29e494d55a7c9ec9d4b84fe129b182a56aff0ce78f9c12802b232ce00bf73898461bd51aa9436759b0e04df0de948e72be45fe59b718e3
7
- data.tar.gz: 8d888d9f83bd7c91693ba03335303513dde1f9df574f03ac757dcd67dfdf1ce9fbf6f213da0426a18b2ab9b5d46b8884d82b7881d9a292169052ab81b0ea04a0
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.2...master
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
- - Controls to publish or unpublish child pages together.
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";
@@ -32,9 +32,11 @@
32
32
  CMS.fileUpload.init();
33
33
  CMS.diff();
34
34
  CMS.draftForms.init();
35
+ CMS.flashNotices.init();
35
36
  };
36
37
 
37
38
  CMS.dispose = () => {
39
+ CMS.flashNotices.dispose();
38
40
  CMS.pageFragments.dispose();
39
41
  CMS.draftForms.dispose();
40
42
  CMS.codemirror.dispose();
@@ -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
- @page.save!
68
- update_family
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] = I18n.t('comfy.admin.cms.pages.updated')
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
- @site.pages
141
- .where(id: descendant_ids)
142
- .update_all(is_published: is_published, updated_at: Time.current)
143
- default = is_published ? 'Child pages published' : 'Child pages unpublished'
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
@@ -80,6 +80,8 @@ protected
80
80
  @cms_site.pages.published.find_by!(full_path: full_path)
81
81
  end
82
82
 
83
+ return nil unless Rails.env == 'development' || @cms_page.published_with_ancestors?
84
+
83
85
  @cms_page.translate!
84
86
  @cms_layout = @cms_page.layout
85
87
 
@@ -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-8.item
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-4.d-flex.align-items-center.justify-content-md-end
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
- = form.check_box :is_published
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
- = form.form_actions do
30
- = submit_tag t(".preview"), name: "preview", formtarget: "comfy-cms-preview", id: nil, class: "btn btn-secondary"
31
- = submit_tag t(@page.new_record?? ".create" : ".update"), class: "btn btn-primary ml-sm-1"
32
- = link_to t(".cancel"), comfy_admin_cms_site_pages_path, class: "btn btn-link"
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 = page.is_published? ? "published" : "draft"
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{:class => "alert-#{type}"}
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'}} &times;
4
4
  = message
@@ -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, siblings, and parent updated
126
+ updated: Page updated
127
127
  update_failure: Failed to update page
128
- children_published: Child pages published
129
- children_unpublished: Child pages 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
- @links = '<div id="breadcrumbs">'
28
- context.ancestors.reverse.each do |a|
29
- next if Rails.env == 'production' && !a.is_published
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> &raquo; "
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
- # ActiveRecord_Associations_CollectionProxy
31
- @page_children = context.children.order(:position).to_ary
32
- unless Rails.env == 'development'
33
- @page_children.delete_if { |child| !child.is_published }
34
- end
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
- # ActiveRecord_Associations_CollectionProxy
33
- @sibs = context.self_and_siblings.order(:position).to_ary
34
- unless Rails.env == 'development'
35
- @sibs.delete_if { |sib| !sib.is_published }
36
- end
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 == context.slug
47
- next if Rails.env == 'production' && !sib.is_published
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(context) - 1
54
+ if sib_idx == @sibs.index(@page) - 1
51
55
  @links += "<a href=#{sib.url(relative: true)}>#{sib.label}</a> &laquo;&nbsp;<em>Previous</em> &bull; "
52
56
  prevp = true
53
- elsif sib_idx == @sibs.index(context) + 1
57
+ elsif sib_idx == @sibs.index(@page) + 1
54
58
  @links += '&bull;' unless prevp
55
59
  @links += "<em>Next</em>&nbsp;&raquo; <a href=#{sib.url(relative: true)}>#{sib.label}</a>"
56
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ComfyMiddleSeat
4
- VERSION = '4.0.4'
4
+ VERSION = '4.2.0'
5
5
  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
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