comfy_middle_seat 4.1.0 → 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: 4d22803345977cfaec60811a1301ce6ad6ada6ab242d41c5c390b4c5aa6da645
4
- data.tar.gz: 537cd86f6e7685b7a771dcd80e5c0c1bc01544f92e88fc3939bc956bc2a08db2
3
+ metadata.gz: 25bec879fab0f69cf22d27320c22441fd04a2dd10d489223b5f4dfe42d4647e4
4
+ data.tar.gz: 0ca964528011c2f24c8d804c965f08fb46e1dea98d7b9286557bf9f6cc0da27a
5
5
  SHA512:
6
- metadata.gz: d0785ac0b03f9f9ce49112a9b8ce41332d077e3535afd1fd46f6baab5ced8316cd95a6daef480c2e67c254d4bedcf1707f2b11ee1df980907a546443ce4a2958
7
- data.tar.gz: e36f136f038d6bbdd043c306306c4d2c9fa8fc4cac7a5b0ad7021c717858eaa354c529516d0642a4d09c501fc892b7c871d0749e4388bad4d22b474a5a581302
6
+ metadata.gz: ddc4be334bbdd4dcde5f7a703b2f22c8dd6645b79c36ea3ed1e13e77d4d6a077d0e8193c5de10b29520893d7f8463d019921a12553cd3dd5827b9d1d91741ba0
7
+ data.tar.gz: 4ea20039aeef0f0c580252eee42f09bf1804c7376f2944f5c5dba4927e86126bff5d7c681eda6f4de3e1e79ee33f23d16fa01345a06f90b4f6f178ab9aeea80b
data/CHANGELOG.md CHANGED
@@ -14,13 +14,44 @@ This changelog follows the project's lineage: **Comfortable Mexican Sofa → Com
14
14
 
15
15
  ### Changed
16
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.
17
33
  - Hide pages beneath unpublished ancestors without changing their publication settings.
18
- - Show inherited page visibility and clarify descendant publishing actions.
34
+ - Show when a page is hidden by an unpublished ancestor.
35
+ - Group descendant publishing actions in the Child pages menu.
19
36
  - Gray out descendant publishing actions when no changes are needed.
20
37
 
21
38
  ### Fixed
22
39
 
23
- - Refresh page and translation navigation when branch visibility changes.
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.
24
55
 
25
56
  ## [v4.0.2] - 2026-09-21
26
57
 
@@ -220,7 +251,10 @@ First release of `comfortable_media_surfer`. This new gem is a revival of [Comfo
220
251
 
221
252
  - Rebranded **ComfortableMexicanSofa** as **ComfortableMediaSurfer** in order to publish new gem (database table names and schema have not changed).
222
253
 
223
- [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
224
258
  [v4.0.2]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.0.1...v4.0.2
225
259
  [v4.0.1]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.0.0...v4.0.1
226
260
  [v4.0.0]: https://github.com/avonderluft/comfy-middle-seat/compare/v3.2.1...v4.0.0
@@ -4,6 +4,7 @@ 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";
9
10
  import "bootstrap/js/src/dropdown";
@@ -20,6 +21,7 @@ import "./draft_forms";
20
21
  import "./file_link";
21
22
  import "./file_upload";
22
23
  import "./files_modal";
24
+ import "./flash_notices";
23
25
  import "./page_fragments";
24
26
  import "./sortable_list";
25
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
+ })();
@@ -42,6 +42,36 @@ body#comfy
42
42
  #cms-main
43
43
  .page-header
44
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%
45
75
  ul.list
46
76
  li .row
47
77
  @include transition(background-color .15s ease-in-out)
@@ -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
@@ -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"
@@ -3,32 +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
- - publication_states = descendant_publication_states(@page)
10
- .dropdown
11
- %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'}}
12
- = t('.child_pages', default: 'Child pages')
13
- #child-pages-actions.dropdown-menu.dropdown-menu-right{aria: {labelledby: 'child-pages-menu'}}
14
- - if publication_states.include?(false)
15
- = 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'
16
- - else
17
- %button.dropdown-item.disabled{type: 'button', disabled: true, aria: {disabled: 'true'}}= t('.publish_descendants', default: 'Publish descendants')
18
- - if publication_states.include?(true)
19
- = 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'
20
- - else
21
- %button.dropdown-item.disabled{type: 'button', disabled: true, aria: {disabled: 'true'}}= t('.unpublish_descendants', default: 'Unpublish descendants')
22
-
23
- - unless @page.published_with_ancestors?
24
- .alert.alert-info.publication-status{role: 'status'}
25
- - if @page.is_published?
26
- = t('.hidden_by_ancestor', default: 'Hidden by unpublished ancestor. Publish its ancestors to make this page visible.')
27
- - else
28
- = t('.unpublished_notice', default: 'This page is unpublished.')
29
- = t('.branch_visibility_notice', default: 'Descendants are visible only when they and all their ancestors are published. Their publication settings are preserved when an ancestor is unpublished.')
30
-
31
6
  - draft_data = cms_draft_form_data('site', @site.id, 'page', @page.id, record: @page)
32
7
  = comfy_form_with model: @page, scope: :page, url: {action: :update}, html: {multipart: true, data: draft_data} do |form|
33
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
+
34
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,7 +123,7 @@ 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
128
  children_published: Descendant pages published
129
129
  children_unpublished: Descendant pages unpublished
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ComfyMiddleSeat
4
- VERSION = '4.1.0'
4
+ VERSION = '4.2.0'
5
5
  end
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.1.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