diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-08-06 05:17:27 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-08-06 05:17:27 +0200 |
| commit | e19787c6ca542736db40e042554f5c0103531821 (patch) | |
| tree | 8f1642d670d77c587194d6927f6fb5dfe1da96b9 | |
| parent | cb16e35c5f9fc93a3382391de496153515cf05ca (diff) | |
Rebuild the sitemap rows around the shared title and action patterns
| -rw-r--r-- | app/views/nodes/show.html.erb | 11 | ||||
| -rw-r--r-- | app/views/nodes/sitemap.html.erb | 50 | ||||
| -rw-r--r-- | config/locales/de.yml | 7 | ||||
| -rw-r--r-- | config/locales/en.yml | 7 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 64 | ||||
| -rw-r--r-- | test/controllers/nodes_controller_test.rb | 20 |
6 files changed, 130 insertions, 29 deletions
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index a94e001c..105e37ea 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb | |||
| @@ -231,7 +231,7 @@ | |||
| 231 | <% end %> | 231 | <% end %> |
| 232 | </div> | 232 | </div> |
| 233 | 233 | ||
| 234 | <% if @node.head %> | 234 | <% if @node.head && !@node.root? %> |
| 235 | <div class="link_matrix_row"> | 235 | <div class="link_matrix_row"> |
| 236 | <span class="link_matrix_label"> | 236 | <span class="link_matrix_label"> |
| 237 | <span class="info_label"><%= t(".public") %></span> | 237 | <span class="info_label"><%= t(".public") %></span> |
| @@ -251,6 +251,15 @@ | |||
| 251 | </div> | 251 | </div> |
| 252 | <% end %> | 252 | <% end %> |
| 253 | 253 | ||
| 254 | <% if @node.root? %> | ||
| 255 | <div class="link_matrix_row"> | ||
| 256 | <span class="link_matrix_label"> | ||
| 257 | <span class="info_label"><%= t(".public") %></span> | ||
| 258 | <span class="field_hint"><%= t(".root_has_no_public_url") %></span> | ||
| 259 | </span> | ||
| 260 | </div> | ||
| 261 | <% end %> | ||
| 262 | |||
| 254 | <% if @node.draft %> | 263 | <% if @node.draft %> |
| 255 | <div class="link_matrix_row"> | 264 | <div class="link_matrix_row"> |
| 256 | <span class="link_matrix_label"> | 265 | <span class="link_matrix_label"> |
diff --git a/app/views/nodes/sitemap.html.erb b/app/views/nodes/sitemap.html.erb index ea64b356..019d4c92 100644 --- a/app/views/nodes/sitemap.html.erb +++ b/app/views/nodes/sitemap.html.erb | |||
| @@ -1,23 +1,55 @@ | |||
| 1 | <h1><%= t(".title") %></h1> | 1 | <%= render "shared/page_actions", |
| 2 | :title => t(".title"), | ||
| 3 | :icon_name => "file-plus", | ||
| 4 | :label => t(".create_at_root"), | ||
| 5 | :options => [[t(".create_at_root"), new_node_path(:parent_id => Node.root.id)]] %> | ||
| 2 | 6 | ||
| 3 | <div id="sitemap"> | 7 | <div id="sitemap"> |
| 8 | |||
| 4 | <% | 9 | <% |
| 5 | open_details = [] # levels with a currently-open <details> | 10 | open_details = [] # levels with a currently-open <details> |
| 11 | world_icon = icon("world", library: "tabler", "aria-hidden": true) | ||
| 12 | create_icon = icon("file-plus", library: "tabler", "aria-hidden": true) | ||
| 13 | show_icon = icon("eye", library: "tabler", "aria-hidden": true) | ||
| 6 | %> | 14 | %> |
| 15 | |||
| 7 | <% @sitemap.each_with_index do |(node, level), index| %> | 16 | <% @sitemap.each_with_index do |(node, level), index| %> |
| 8 | <% while open_details.any? && open_details.last >= level %> | 17 | <% while open_details.any? && open_details.last >= level %> |
| 9 | </details> | 18 | </details> |
| 10 | <% open_details.pop %> | 19 | <% open_details.pop %> |
| 11 | <% end %> | 20 | <% end %> |
| 12 | 21 | ||
| 13 | <div class="sitemap_node"> | 22 | <% unless level.zero? %> |
| 14 | <h4><%= link_to title_for_node(node), node_path(node) %></h4> | 23 | <div class="sitemap_node"> |
| 15 | <span class="field_hint"><%= link_to_path("#{node.unique_name} ↗", node.unique_name) %></span> | 24 | <div class="title_with_flags"> |
| 16 | <p class="sitemap_node_actions"> | 25 | <span class="flag_stack"> |
| 17 | <%= link_to 'Show', node_path(node) %> | 26 | <%= flag("file-text-shield", t(".flag_restricted"), :tier => :attention) if node.restricted? %> |
| 18 | <%= link_to 'Create Child', new_node_path(:parent_id => node.id) %> | 27 | <%= flag("external-link", t(".flag_external")) if node.external_url.present? %> |
| 19 | </p> | 28 | </span> |
| 20 | </div> | 29 | <div class="title_body"> |
| 30 | <div class="node_title"><%= link_to title_for_node(node), node_path(node) %></div> | ||
| 31 | <div class="node_path"> | ||
| 32 | <%= world_icon %> | ||
| 33 | <%= link_to_path(node.unique_name, node.unique_name) %> | ||
| 34 | </div> | ||
| 35 | </div> | ||
| 36 | <div class="action_grid"> | ||
| 37 | <span class="action_item"> | ||
| 38 | <%= link_to new_node_path(:parent_id => node.id), | ||
| 39 | "aria-label" => t(".create_child"), title: t(".create_child") do %> | ||
| 40 | <%= create_icon %> | ||
| 41 | <% end %> | ||
| 42 | </span> | ||
| 43 | <span class="action_item"> | ||
| 44 | <%= link_to node_path(node), "aria-label" => t("admin.common.show"), | ||
| 45 | title: t("admin.common.show") do %> | ||
| 46 | <%= show_icon %> | ||
| 47 | <% end %> | ||
| 48 | </span> | ||
| 49 | </div> | ||
| 50 | </div> | ||
| 51 | </div> | ||
| 52 | <% end %> | ||
| 21 | 53 | ||
| 22 | <% next_level = @sitemap[index + 1]&.last %> | 54 | <% next_level = @sitemap[index + 1]&.last %> |
| 23 | <% if next_level && next_level > level %> | 55 | <% if next_level && next_level > level %> |
diff --git a/config/locales/de.yml b/config/locales/de.yml index 591b45d1..10f4b606 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -538,7 +538,8 @@ de: | |||
| 538 | add_child: "Kind vom Typ %{kind} anlegen" | 538 | add_child: "Kind vom Typ %{kind} anlegen" |
| 539 | abstract_locale: "Abstract (%{lang})" | 539 | abstract_locale: "Abstract (%{lang})" |
| 540 | body_locale: "Text (%{lang})" | 540 | body_locale: "Text (%{lang})" |
| 541 | restricted_hint: "Geschützter Bereich — Veröffentlichen und Papierkorb sind der Redaktion vorbehalten. Entwürfe kannst du frei bearbeiten." | 541 | restricted_hint: "Geschützter Bereich. Veröffentlichen und Papierkorb sind der Redaktion vorbehalten. Entwürfe kannst du frei bearbeiten." |
| 542 | root_has_no_public_url: "Der Root-Node hat keine öffentliche Adresse. Er wird von der Startseite geliefert." | ||
| 542 | drafts: | 543 | drafts: |
| 543 | title: "Nodes mit Entwürfen, Autosaves oder Sperren" | 544 | title: "Nodes mit Entwürfen, Autosaves oder Sperren" |
| 544 | mine: | 545 | mine: |
| @@ -547,6 +548,10 @@ de: | |||
| 547 | title: "Nodes mit Tag: %{tag}" | 548 | title: "Nodes mit Tag: %{tag}" |
| 548 | sitemap: | 549 | sitemap: |
| 549 | title: "Sitemap" | 550 | title: "Sitemap" |
| 551 | create_at_root: "Seite auf oberster Ebene anlegen" | ||
| 552 | create_child: "Hier eine Seite anlegen" | ||
| 553 | flag_restricted: "Veröffentlichen unterhalb erfordert Redaktion" | ||
| 554 | flag_external: "Leitet weiter — hier keine Seiten anlegen" | ||
| 550 | descendants: | 555 | descendants: |
| 551 | one: "1 Unterseite" | 556 | one: "1 Unterseite" |
| 552 | other: "%{count} Unterseiten" | 557 | other: "%{count} Unterseiten" |
diff --git a/config/locales/en.yml b/config/locales/en.yml index f8a7144c..adcc591e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -485,7 +485,8 @@ en: | |||
| 485 | add_child: "Add child type %{kind}" | 485 | add_child: "Add child type %{kind}" |
| 486 | abstract_locale: "Abstract (%{lang})" | 486 | abstract_locale: "Abstract (%{lang})" |
| 487 | body_locale: "Body (%{lang})" | 487 | body_locale: "Body (%{lang})" |
| 488 | restricted_hint: "Protected section — publishing and trashing are reserved for Redaktion. You can edit drafts freely." | 488 | restricted_hint: "Protected section. Publishing and trashing are reserved for Redaktion. You can edit drafts freely." |
| 489 | root_has_no_public_url: "The root node has no public address. It is served from the home page." | ||
| 489 | drafts: | 490 | drafts: |
| 490 | title: "Nodes with drafts, autosaves or locks" | 491 | title: "Nodes with drafts, autosaves or locks" |
| 491 | mine: | 492 | mine: |
| @@ -494,6 +495,10 @@ en: | |||
| 494 | title: "Nodes tagged: %{tag}" | 495 | title: "Nodes tagged: %{tag}" |
| 495 | sitemap: | 496 | sitemap: |
| 496 | title: "Sitemap" | 497 | title: "Sitemap" |
| 498 | create_at_root: "Create a top-level page" | ||
| 499 | create_child: "Add a page here" | ||
| 500 | flag_restricted: "Publishing below this point needs Redaktion" | ||
| 501 | flag_external: "Redirects elsewhere — do not add pages under this" | ||
| 497 | descendants: | 502 | descendants: |
| 498 | one: "1 descendant" | 503 | one: "1 descendant" |
| 499 | other: "%{count} descendants" | 504 | other: "%{count} descendants" |
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index acb87aba..334e0bd5 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css | |||
| @@ -835,7 +835,6 @@ table.revisions_table tr:hover { | |||
| 835 | gap: 0.3rem; | 835 | gap: 0.3rem; |
| 836 | margin-top: 0.35rem; | 836 | margin-top: 0.35rem; |
| 837 | font-size: 0.875rem; | 837 | font-size: 0.875rem; |
| 838 | color: var(--text-muted); | ||
| 839 | } | 838 | } |
| 840 | 839 | ||
| 841 | .assets_table .asset_meta svg, | 840 | .assets_table .asset_meta svg, |
| @@ -1439,23 +1438,67 @@ form.button_to button[type="submit"] { | |||
| 1439 | margin-bottom: 0; | 1438 | margin-bottom: 0; |
| 1440 | } | 1439 | } |
| 1441 | 1440 | ||
| 1441 | #sitemap { | ||
| 1442 | max-width: 60rem; | ||
| 1443 | } | ||
| 1444 | |||
| 1445 | #sitemap .title_with_flags { | ||
| 1446 | align-items: start; | ||
| 1447 | } | ||
| 1448 | |||
| 1449 | #sitemap .title_body { | ||
| 1450 | flex: 1; | ||
| 1451 | } | ||
| 1452 | |||
| 1453 | #sitemap .node_path { | ||
| 1454 | display: flex; | ||
| 1455 | align-items: center; | ||
| 1456 | gap: 0.3rem; | ||
| 1457 | margin-top: 0.15rem; | ||
| 1458 | font-size: 0.875rem; | ||
| 1459 | color: var(--text-muted); | ||
| 1460 | } | ||
| 1461 | |||
| 1462 | #sitemap .node_path svg { | ||
| 1463 | width: 0.875rem; | ||
| 1464 | height: 0.875rem; | ||
| 1465 | flex-shrink: 0; | ||
| 1466 | } | ||
| 1467 | |||
| 1468 | #sitemap .node_path a { | ||
| 1469 | color: var(--text-muted); | ||
| 1470 | overflow-wrap: anywhere; | ||
| 1471 | } | ||
| 1472 | |||
| 1473 | #sitemap .action_grid { | ||
| 1474 | grid-template-columns: repeat(2, auto); | ||
| 1475 | } | ||
| 1476 | |||
| 1442 | .sitemap_node { | 1477 | .sitemap_node { |
| 1443 | padding-bottom: 0.5rem; | 1478 | padding-bottom: 0.5rem; |
| 1444 | margin-bottom: 0.5rem; | 1479 | margin-bottom: 0.5rem; |
| 1445 | border-bottom: 1px solid var(--hairline); | 1480 | border-bottom: 1px solid var(--hairline); |
| 1446 | } | 1481 | } |
| 1447 | 1482 | ||
| 1448 | .sitemap_node h4 { | 1483 | #sitemap .sitemap_node:has(+ details) { |
| 1449 | margin: 0; | 1484 | border-bottom: none; |
| 1485 | margin-bottom: 0; | ||
| 1486 | padding-bottom: 0.25rem; | ||
| 1450 | } | 1487 | } |
| 1451 | 1488 | ||
| 1452 | .sitemap_node .field_hint { | 1489 | #sitemap .sitemap_node:has(+ details) + details > summary { |
| 1453 | display: block; | 1490 | padding-bottom: 0.5rem; |
| 1454 | margin: 2px 0 0; | 1491 | margin-bottom: 0.5rem; |
| 1492 | border-bottom: 1px solid var(--hairline); | ||
| 1455 | } | 1493 | } |
| 1456 | 1494 | ||
| 1457 | .sitemap_node p { | 1495 | #sitemap > .sitemap_node { |
| 1458 | margin: 2px 0 0; | 1496 | border-bottom-width: 8px; |
| 1497 | margin-bottom: 0.75rem; | ||
| 1498 | } | ||
| 1499 | |||
| 1500 | #sitemap > details > summary { | ||
| 1501 | display: none; | ||
| 1459 | } | 1502 | } |
| 1460 | 1503 | ||
| 1461 | #sitemap details details { | 1504 | #sitemap details details { |
| @@ -1472,7 +1515,10 @@ form.button_to button[type="submit"] { | |||
| 1472 | } | 1515 | } |
| 1473 | 1516 | ||
| 1474 | #sitemap summary { | 1517 | #sitemap summary { |
| 1475 | padding: 4px 0; | 1518 | padding: 0.10rem 0 0.5rem 0; |
| 1519 | margin-left: -0.25rem; | ||
| 1520 | margin-top: -0.1rem; | ||
| 1521 | color: var(--text-muted); | ||
| 1476 | } | 1522 | } |
| 1477 | 1523 | ||
| 1478 | #sitemap details:not([open]) > summary { | 1524 | #sitemap details:not([open]) > summary { |
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb index 27c28cfe..c2898cbb 100644 --- a/test/controllers/nodes_controller_test.rb +++ b/test/controllers/nodes_controller_test.rb | |||
| @@ -644,8 +644,8 @@ class NodesControllerTest < ActionController::TestCase | |||
| 644 | 644 | ||
| 645 | doc = Nokogiri::HTML::DocumentFragment.parse(response.body) | 645 | doc = Nokogiri::HTML::DocumentFragment.parse(response.body) |
| 646 | 646 | ||
| 647 | erfas_node_div = doc.css('.sitemap_node').find { |div| div.at_css('.field_hint')&.text&.include?(erfas.unique_name) } | 647 | erfas_node_div = doc.css('.sitemap_node').find { |div| div.at_css('.node_path')&.text&.include?(erfas.unique_name) } |
| 648 | other_node_div = doc.css('.sitemap_node').find { |div| div.at_css('.field_hint')&.text&.include?(other.unique_name) } | 648 | other_node_div = doc.css('.sitemap_node').find { |div| div.at_css('.node_path')&.text&.include?(other.unique_name) } |
| 649 | 649 | ||
| 650 | erfas_details = erfas_node_div.next_element | 650 | erfas_details = erfas_node_div.next_element |
| 651 | other_details = other_node_div.next_element | 651 | other_details = other_node_div.next_element |
| @@ -667,20 +667,24 @@ class NodesControllerTest < ActionController::TestCase | |||
| 667 | assert_response :success | 667 | assert_response :success |
| 668 | 668 | ||
| 669 | doc = Nokogiri::HTML::DocumentFragment.parse(response.body) | 669 | doc = Nokogiri::HTML::DocumentFragment.parse(response.body) |
| 670 | erfas_node_div = doc.css('.sitemap_node').find { |div| div.at_css('.field_hint')&.text&.include?(erfas.unique_name) } | 670 | erfas_node_div = doc.css('.sitemap_node').find { |div| div.at_css('.node_path')&.text&.include?(erfas.unique_name) } |
| 671 | erfas_details = erfas_node_div.next_element | 671 | erfas_details = erfas_node_div.next_element |
| 672 | 672 | ||
| 673 | assert_equal 'details', erfas_details.name | 673 | assert_equal 'details', erfas_details.name |
| 674 | assert_match I18n.t("nodes.sitemap.descendants", :count => 2), @response.body | 674 | assert_match I18n.t("nodes.sitemap.descendants", :count => 2), @response.body |
| 675 | end | 675 | end |
| 676 | 676 | ||
| 677 | test "sitemap shows Show and Create Child, not Revisions" do | 677 | test "sitemap offers Create Child and Show, not Revisions" do |
| 678 | node = Node.root.children.create!(:slug => "sitemap_actions_test") | 678 | Node.root.children.create!(:slug => "sitemap_actions_test") |
| 679 | login_as :quentin | 679 | login_as :quentin |
| 680 | get :sitemap | 680 | get :sitemap |
| 681 | assert_select ".sitemap_node_actions", :text => /Show/ | 681 | |
| 682 | assert_select ".sitemap_node_actions", :text => /Create Child/ | 682 | assert_select ".sitemap_node .action_grid a[title=?]", |
| 683 | assert_select ".sitemap_node_actions", :text => /Revisions/, :count => 0 | 683 | I18n.t("nodes.sitemap.create_child") |
| 684 | assert_select ".sitemap_node .action_grid a[title=?]", | ||
| 685 | I18n.t("admin.common.show") | ||
| 686 | assert_select ".sitemap_node .action_grid a[title=?]", | ||
| 687 | I18n.t("admin.common.revisions"), :count => 0 | ||
| 684 | end | 688 | end |
| 685 | 689 | ||
| 686 | test "create logs a create NodeAction with path and title" do | 690 | test "create logs a create NodeAction with path and title" do |
