summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-08-04 07:07:05 +0200
committererdgeist <erdgeist@erdgeist.org>2026-08-04 07:07:05 +0200
commit409d71ed471703be8c56f2639a3ecb0fa387e6a7 (patch)
tree0a7108786cb549e343d8926b232f52a191570422
parent974062f16169f6d07f2289564be6d72610b8770e (diff)
Give node lists a flag column and a shared action bar
-rw-r--r--app/controllers/nodes_controller.rb4
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/models/node.rb4
-rw-r--r--app/models/user.rb18
-rw-r--r--app/views/events/index.html.erb10
-rw-r--r--app/views/nodes/_node_list.html.erb40
-rw-r--r--app/views/nodes/index.html.erb33
-rw-r--r--app/views/users/_user.html.erb18
-rw-r--r--app/views/users/edit.html.erb4
-rw-r--r--config/locales/de.yml1
-rw-r--r--config/locales/en.yml1
-rw-r--r--public/stylesheets/admin.css91
-rw-r--r--test/controllers/users_controller_test.rb2
13 files changed, 104 insertions, 124 deletions
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index ca30976f..447d596d 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -20,9 +20,7 @@ class NodesController < ApplicationController
20 ] 20 ]
21 21
22 def index 22 def index
23 @nodes = Node.root.descendants.includes(:head, :draft) 23 @nodes = index_matching(Node.root.descendants)
24 .order('id DESC')
25 .paginate(:page => params[:page], :per_page => 25)
26 end 24 end
27 25
28 def new 26 def new
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index b06d11fd..20355aa8 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -140,7 +140,7 @@ class UsersController < ApplicationController
140 140
141 def verify_status 141 def verify_status
142 @user ||= User.new 142 @user ||= User.new
143 unless @user.id == current_user.id || current_user.admin 143 unless @user.id == current_user.id || current_user.is_admin?
144 deny_user_access 144 deny_user_access
145 end 145 end
146 end 146 end
diff --git a/app/models/node.rb b/app/models/node.rb
index d6fe32ff..f8c29b48 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -648,6 +648,10 @@ class Node < ApplicationRecord
648 ) 648 )
649 end 649 end
650 650
651 def embargoed?
652 head&.published_at.present? && head.published_at > Time.zone.now
653 end
654
651 # Nodes are never destroyed recursively 655 # Nodes are never destroyed recursively
652 # Descendants must be removed or reparented individually first. 656 # Descendants must be removed or reparented individually first.
653 # The Trash feature will be the ordinary path to deletion. 657 # The Trash feature will be the ordinary path to deletion.
diff --git a/app/models/user.rb b/app/models/user.rb
index 786f8d14..95706f86 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -70,24 +70,6 @@ class User < ApplicationRecord
70 roles.include?("admin") 70 roles.include?("admin")
71 end 71 end
72 72
73 # Compatibility shims for the users form, which posts user[admin] as a
74 # checkbox. Goes away when that form learns about roles.
75 def admin
76 is_admin?
77 end
78
79 def admin?
80 is_admin?
81 end
82
83 def admin=(value)
84 if ActiveModel::Type::Boolean.new.cast(value)
85 self.roles = (roles | ["admin"])
86 else
87 self.roles = (roles - ["admin"])
88 end
89 end
90
91 def redaktion? 73 def redaktion?
92 roles.include?("redaktion") 74 roles.include?("redaktion")
93 end 75 end
diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb
index a2710c2a..cc3f163d 100644
--- a/app/views/events/index.html.erb
+++ b/app/views/events/index.html.erb
@@ -22,10 +22,12 @@
22<% @events.each do |event| %> 22<% @events.each do |event| %>
23 <tr> 23 <tr>
24 <td> 24 <td>
25 <span class="flag_stack"> 25 <div class="title_with_flags">
26 <%= flag("clock-off", Event.human_attribute_name(:allday)) if event.allday %> 26 <span class="flag_stack">
27 </span> 27 <%= flag("clock-off", Event.human_attribute_name(:allday)) if event.allday %>
28 <%= link_to event.display_title, event %> 28 </span>
29 <div class="title_body"><%= link_to event.display_title, event %></div>
30 </div>
29 </td> 31 </td>
30 <td><%= admin_datetime(event.start_time) %></td> 32 <td><%= admin_datetime(event.start_time) %></td>
31 <td><%= admin_datetime(event.end_time) %></td> 33 <td><%= admin_datetime(event.end_time) %></td>
diff --git a/app/views/nodes/_node_list.html.erb b/app/views/nodes/_node_list.html.erb
index 0dc3c5a0..458ab1ae 100644
--- a/app/views/nodes/_node_list.html.erb
+++ b/app/views/nodes/_node_list.html.erb
@@ -15,26 +15,46 @@
15<%= will_paginate @nodes %> 15<%= will_paginate @nodes %>
16<table class="node_table"> 16<table class="node_table">
17 <tr class="header"> 17 <tr class="header">
18 <th class="node_id"><%= t("admin.columns.id") %></th>
19 <th class="title"><%= t("admin.columns.title") %></th> 18 <th class="title"><%= t("admin.columns.title") %></th>
20 <th class="actions"><%= t("admin.columns.actions") %></th> 19 <th class="actions"><%= t("admin.columns.actions") %></th>
21 <th class="editor"><%= t("admin.columns.locked_by") %></th>
22 <th class="revision"><%= t("admin.columns.rev") %></th> 20 <th class="revision"><%= t("admin.columns.rev") %></th>
23 </tr> 21 </tr>
24 <% @nodes.each do |node| %> 22 <% @nodes.each do |node| %>
25 <tr class="<%= cycle("even", "odd") %>"> 23 <tr class="<%= cycle("even", "odd") %>">
26 <td class="node_id"><%= node.id %></td>
27 <td class="title"> 24 <td class="title">
28 <h4><%= link_to title_for_node(node), node_path(node) %></h4> 25 <div class="title_with_flags">
29 <p><%= link_to_path(node.unique_name, node.unique_name) %></p> 26 <span class="flag_stack">
27 <%= flag("lock", t(".flag_locked", :login => node.lock_owner.login),
28 :tier => :attention) if node.lock_owner %>
29 <%= flag("calendar-clock",
30 t(".flag_embargo", :date => admin_datetime(node.head.published_at)),
31 :tier => :attention) if node.embargoed? %>
32 <%= flag("file-pencil", t(".flag_draft")) if node.draft %>
33 <%= flag("file-off", t(".flag_no_head")) unless node.head %>
34 </span>
35 <div class="title_body">
36 <div class="node_title"><%= link_to title_for_node(node), node_path(node) %></div>
37 <div class="node_path"><%= link_to_path(node.unique_name, node.unique_name) %></div>
38 </div>
39 </div>
30 </td> 40 </td>
31 <td class="actions"> 41 <td class="actions">
32 <%= link_to 'show', node_path(node) %> 42 <div class="action_grid">
33 <%= link_to 'edit', edit_node_path(node) %> 43 <span class="action_item">
34 <%= link_to 'revisions', node_revisions_path(node) %> 44 <%= link_to node_path(node), "aria-label" => t("admin.common.show"),
45 title: t("admin.common.show") do %>
46 <%= icon("eye", library: "tabler", "aria-hidden": true) %>
47 <% end %>
48 </span>
49 <span class="action_item">
50 <%= link_to node_revisions_path(node), "aria-label" => t("admin.common.revisions"),
51 title: t("admin.common.revisions") do %>
52 <%= icon("history", library: "tabler", "aria-hidden": true) %>
53 <% end %>
54 </span>
55 </div>
35 </td> 56 </td>
36 <td><%= node.lock_owner.login if node.lock_owner %></td> 57 <td><%= node.head ? node.head.revision : t(".no_revision") %></td>
37 <td><%= node.draft ? node.draft.revision : (node.head ? node.head.revision : t(".no_revision")) %></td>
38 </tr> 58 </tr>
39 <% end %> 59 <% end %>
40</table> 60</table>
diff --git a/app/views/nodes/index.html.erb b/app/views/nodes/index.html.erb
index 7ab5679e..357c938b 100644
--- a/app/views/nodes/index.html.erb
+++ b/app/views/nodes/index.html.erb
@@ -7,35 +7,4 @@
7 <span class="field_hint"><%= t(".create_hint") %></span> 7 <span class="field_hint"><%= t(".create_hint") %></span>
8</div> 8</div>
9 9
10<%= will_paginate @nodes %> 10<%= render 'node_list' %>
11 <table class="node_table">
12 <tr class="header">
13 <th class="node_id"><%= t("admin.columns.id") %></th>
14 <th class="title"><%= t("admin.columns.title") %></th>
15 <th class="actions"><%= t("admin.columns.actions") %></th>
16 <th class="editor"><%= t("admin.columns.locked_by") %></th>
17 <th class="revision"><%= t("admin.columns.rev") %></th>
18 </tr>
19 <% @nodes.each do |node| %>
20 <tr class="<%= cycle("even", "odd") %>">
21 <td class="node_id"><%= node.id %></td>
22 <td class="title">
23 <h4><%= link_to title_for_node(node), node_path(node) %></h4>
24 <p><%= link_to_path(node.unique_name, node.unique_name) %></p>
25 </td>
26 <td class="actions">
27 <%= link_to 'show', node_path(node) %>
28 <%= link_to 'edit', edit_node_path(node) %>
29 <%= link_to 'revisions', node_revisions_path(node) %>
30 </td>
31 <td>
32 <%= node.lock_owner.login if node.lock_owner %>
33 </td>
34 <td>
35 <%= node.draft ? node.draft.revision : (node.head ? node.head.revision : "EMPTY") %>
36 </td>
37 </tr>
38 <% end %>
39 </table>
40<%= will_paginate @nodes %>
41
diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb
index be92ad1d..028ee02f 100644
--- a/app/views/users/_user.html.erb
+++ b/app/views/users/_user.html.erb
@@ -27,17 +27,17 @@
27 <span class="field_hint"><%= t(".no_roles") %></span> 27 <span class="field_hint"><%= t(".no_roles") %></span>
28 <% end %> 28 <% end %>
29 </td> 29 </td>
30 <td class="user_actions"> 30 <td class="actions">
31 <div class="user_action_grid"> 31 <div class="action_grid">
32 <span class="user_action"> 32 <span class="action_item">
33 <%= link_to user_path(user), "aria-label" => t("admin.common.show"), 33 <%= link_to user_path(user), "aria-label" => t("admin.common.show"),
34 title: t("admin.common.show") do %> 34 title: t("admin.common.show") do %>
35 <%= icon("eye", library: "tabler", "aria-hidden": true) %> 35 <%= icon("eye", library: "tabler", "aria-hidden": true) %>
36 <% end %> 36 <% end %>
37 </span> 37 </span>
38 38
39 <span class="user_action"> 39 <span class="action_item">
40 <% if current_user.admin? || current_user == user %> 40 <% if current_user.is_admin? || current_user == user %>
41 <%= link_to edit_user_path(user), "aria-label" => t("admin.common.edit"), 41 <%= link_to edit_user_path(user), "aria-label" => t("admin.common.edit"),
42 title: t("admin.common.edit") do %> 42 title: t("admin.common.edit") do %>
43 <%= icon("edit", library: "tabler", "aria-hidden": true) %> 43 <%= icon("edit", library: "tabler", "aria-hidden": true) %>
@@ -45,15 +45,15 @@
45 <% end %> 45 <% end %>
46 </span> 46 </span>
47 47
48 <span class="user_action"> 48 <span class="action_item">
49 <%= link_to admin_log_path(:user_id => user.id), 49 <%= link_to admin_log_path(:user_id => user.id),
50 "aria-label" => t(".action_log"), title: t(".action_log") do %> 50 "aria-label" => t(".action_log"), title: t(".action_log") do %>
51 <%= icon("history", library: "tabler", "aria-hidden": true) %> 51 <%= icon("history", library: "tabler", "aria-hidden": true) %>
52 <% end %> 52 <% end %>
53 </span> 53 </span>
54 54
55 <span class="user_action"> 55 <span class="action_item">
56 <% if current_user.admin? && current_user != user %> 56 <% if current_user.is_admin? && current_user != user %>
57 <% if user.alumni? %> 57 <% if user.alumni? %>
58 <%= button_to reactivate_user_path(user), method: :put, 58 <%= button_to reactivate_user_path(user), method: :put,
59 form: { class: 'button_to state_changing' }, 59 form: { class: 'button_to state_changing' },
@@ -71,7 +71,7 @@
71 <% end %> 71 <% end %>
72 </span> 72 </span>
73 73
74 <span class="user_action"> 74 <span class="action_item">
75 <% if current_user.redaktion? && !user.alumni? %> 75 <% if current_user.redaktion? && !user.alumni? %>
76 <% if user.redaktion? %> 76 <% if user.redaktion? %>
77 <% unless user == current_user %> 77 <% unless user == current_user %>
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
index ad2e0891..1763394d 100644
--- a/app/views/users/edit.html.erb
+++ b/app/views/users/edit.html.erb
@@ -20,7 +20,7 @@
20 <div class="layout_row_label"><%= t("users.labels.confirm") %></div> 20 <div class="layout_row_label"><%= t("users.labels.confirm") %></div>
21 <div class="layout_row_content"><%= f.password_field :password_confirmation, :autocomplete => "new-password" %></div> 21 <div class="layout_row_content"><%= f.password_field :password_confirmation, :autocomplete => "new-password" %></div>
22 22
23 <% if current_user.admin? %> 23 <% if current_user.is_admin? %>
24 <div class="layout_row_label"><%= t("users.labels.roles") %></div> 24 <div class="layout_row_label"><%= t("users.labels.roles") %></div>
25 <div class="layout_row_content"> 25 <div class="layout_row_content">
26 <% User::ROLES.each do |role| %> 26 <% User::ROLES.each do |role| %>
@@ -62,7 +62,7 @@
62 <% end %> 62 <% end %>
63 <% end %> 63 <% end %>
64 </div> 64 </div>
65 <% elsif current_user.admin? && @user.otp_enrolled? %> 65 <% elsif current_user.is_admin? && @user.otp_enrolled? %>
66 <div class="layout_row_label"><%= t(".second_factor") %></div> 66 <div class="layout_row_label"><%= t(".second_factor") %></div>
67 <div class="layout_row_content"> 67 <div class="layout_row_content">
68 <%= t(".enabled") %> 68 <%= t(".enabled") %>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index a1b29199..6f8dd24a 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -373,6 +373,7 @@ de:
373 unlock_confirm: "Wirklich entsperren?\nGesperrt von %{login}\nZuletzt geändert %{time}" 373 unlock_confirm: "Wirklich entsperren?\nGesperrt von %{login}\nZuletzt geändert %{time}"
374 toggle_preview: "Live-Vorschau umschalten" 374 toggle_preview: "Live-Vorschau umschalten"
375 force_render: "Vorschau neu rendern" 375 force_render: "Vorschau neu rendern"
376 revisions: "Revisionen"
376 "yes": "ja" 377 "yes": "ja"
377 "no": "nein" 378 "no": "nein"
378 columns: 379 columns:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index fc011437..c7a728ab 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -321,6 +321,7 @@ en:
321 unlock_confirm: "Are you sure you want to unlock?\nLocked by %{login}\nLast modified %{time}" 321 unlock_confirm: "Are you sure you want to unlock?\nLocked by %{login}\nLast modified %{time}"
322 toggle_preview: "Toggle live preview" 322 toggle_preview: "Toggle live preview"
323 force_render: "Force preview render" 323 force_render: "Force preview render"
324 revisions: "revisions"
324 "yes": "yes" 325 "yes": "yes"
325 "no": "no" 326 "no": "no"
326 columns: 327 columns:
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index 1c74bbf5..8b976599 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -644,24 +644,20 @@ table tr.header {
644 text-align: left; 644 text-align: left;
645} 645}
646 646
647table.node_table , 647table.node_table,
648table.assets_table, 648table.assets_table,
649table.user_table, 649table.user_table,
650table.events_table { 650table.events_table {
651 border-collapse: collapse; 651 border-collapse: collapse;
652} 652}
653 653
654table.node_table tr { 654table.node_table tr,
655 border-bottom: 1px solid var(--text);
656}
657
658table.assets_table tr, 655table.assets_table tr,
659table.user_table tr, 656table.user_table tr,
660table.events_table tr { 657table.events_table tr {
661 border-bottom: 1px solid var(--hairline); 658 border-bottom: 1px solid var(--hairline);
662} 659}
663 660
664table.node_table th.node_id,
665table.node_table th.revision { 661table.node_table th.revision {
666 min-width: 4rem; 662 min-width: 4rem;
667} 663}
@@ -682,14 +678,7 @@ table.events_table tr.header {
682 text-align: left; 678 text-align: left;
683} 679}
684 680
685table.node_table td { 681table.node_table td,
686 padding-top: 0px;
687 padding-bottom: 0px;
688 padding-right: 25px;
689 padding-left: 0px;
690 min-width: 2rem;
691}
692
693table.assets_table td, 682table.assets_table td,
694table.user_table td, 683table.user_table td,
695table.events_table td { 684table.events_table td {
@@ -700,15 +689,6 @@ table.events_table td {
700 min-width: 2rem; 689 min-width: 2rem;
701} 690}
702 691
703table.node_table .node_id {
704 padding-left: 10px;
705 padding-right: 15px;
706}
707
708table.node_table .actions {
709 text-transform: lowercase;
710}
711
712table.node_table tr.header:hover, 692table.node_table tr.header:hover,
713table.assets_table tr.header:hover, 693table.assets_table tr.header:hover,
714table.user_table tr.header:hover, 694table.user_table tr.header:hover,
@@ -723,10 +703,6 @@ table.events_table tr:hover {
723 background-color: var(--surface-tint); 703 background-color: var(--surface-tint);
724} 704}
725 705
726#admin_sitemap_table .node_id:before {
727 content: "• ";
728}
729
730table.revisions_table { 706table.revisions_table {
731 border-collapse: collapse; 707 border-collapse: collapse;
732} 708}
@@ -844,37 +820,47 @@ table.revisions_table tr:hover {
844 margin-bottom: 1.5rem; 820 margin-bottom: 1.5rem;
845} 821}
846 822
847.user_table td.user_actions { 823.user_table td.actions,
824.node_table td.actions {
848 width: 1px; 825 width: 1px;
849 white-space: nowrap; 826 white-space: nowrap;
827}
828
829/* Only where the action column is last. In the node lists rev. follows it,
830 and the 25px gutter is a real gap between columns there. */
831.user_table td.actions {
850 padding-right: 0; 832 padding-right: 0;
851} 833}
852 834
853.user_table .user_action_grid { 835.action_grid {
854 display: grid; 836 display: grid;
855 grid-template-columns: repeat(3, auto);
856 gap: 0.4rem; 837 gap: 0.4rem;
857 justify-content: end; 838 justify-content: end;
858} 839}
859 840
860.user_table .user_action { 841.user_table .action_grid { grid-template-columns: repeat(3, auto); }
842.node_table .action_grid { grid-template-columns: repeat(2, auto); }
843
844/* min-height keeps a row the same height whether or not the viewer may see
845 the lower controls. */
846.action_item {
861 display: inline-flex; 847 display: inline-flex;
862 min-height: 2rem; 848 min-height: 2rem;
863} 849}
864 850
865.user_table .user_action svg { 851.action_item svg {
866 width: 1.25rem; 852 width: 1.25rem;
867 height: 1.25rem; 853 height: 1.25rem;
868} 854}
869 855
870.user_table .user_action form.button_to { 856.action_item form.button_to {
871 display: inline-flex; 857 display: inline-flex;
872 margin: 0; 858 margin: 0;
873} 859}
874 860
875.user_table .user_action a, 861.action_item a,
876.user_table .user_action button[type="submit"], 862.action_item form.button_to button[type="submit"],
877.user_table .user_action .disabled_action { 863.action_item .disabled_action {
878 display: inline-flex; 864 display: inline-flex;
879 align-items: center; 865 align-items: center;
880 justify-content: center; 866 justify-content: center;
@@ -886,22 +872,22 @@ table.revisions_table tr:hover {
886 text-decoration: none; 872 text-decoration: none;
887} 873}
888 874
889.user_table .user_action a:hover { 875.action_item a:hover {
890 color: var(--surface); 876 color: var(--surface);
891 background-color: var(--text); 877 background-color: var(--text);
892} 878}
893 879
894.user_table .user_action form.button_to.destructive button:hover { 880.action_item form.button_to.destructive button:hover {
895 color: var(--on-solid); 881 color: var(--on-solid);
896 background-color: var(--danger-solid); 882 background-color: var(--danger-solid);
897} 883}
898 884
899.user_table .user_action form.button_to.state_changing button:hover { 885.action_item form.button_to.state_changing button:hover {
900 color: var(--on-solid); 886 color: var(--on-solid);
901 background-color: var(--action-solid); 887 background-color: var(--action-solid);
902} 888}
903 889
904.user_table .user_action .disabled_action { 890.action_item .disabled_action {
905 color: var(--text-muted); 891 color: var(--text-muted);
906 background-color: transparent; 892 background-color: transparent;
907 border-color: var(--border); 893 border-color: var(--border);
@@ -915,22 +901,39 @@ table.revisions_table tr:hover {
915.flag_stack { 901.flag_stack {
916 display: inline-flex; 902 display: inline-flex;
917 flex-direction: column; 903 flex-direction: column;
918 align-items: center; 904 align-items: flex-start;
919 min-width: 1rem;
920 gap: 0.25rem; 905 gap: 0.25rem;
921 vertical-align: -0.15em; 906 vertical-align: -0.15em;
922 margin-right: 0.5rem; 907 margin-right: 0.5rem;
908 width: 1.5rem;
923} 909}
924 910
925.flag_stack svg { 911.flag_stack svg {
926 width: 1rem; 912 width: 1.25rem;
927 height: 1rem; 913 height: 1.25rem;
928 color: var(--text-muted); 914 color: var(--text-muted);
929} 915}
930 916
931.flag_stack .flag_attention svg { color: var(--accent); } 917.flag_stack .flag_attention svg { color: var(--accent); }
932.flag_stack .flag_alert svg { color: var(--danger); } 918.flag_stack .flag_alert svg { color: var(--danger); }
933 919
920.title_with_flags {
921 display: flex;
922 align-items: flex-start;
923}
924
925.title_body {
926 min-width: 0;
927}
928
929.node_table .node_title {
930 font-weight: bold;
931}
932
933.node_table .node_path {
934 margin-top: 0.15rem;
935}
936
934.user_group_heading { 937.user_group_heading {
935 margin-top: 1.5rem; 938 margin-top: 1.5rem;
936} 939}
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
index aeff9bc7..d06b9298 100644
--- a/test/controllers/users_controller_test.rb
+++ b/test/controllers/users_controller_test.rb
@@ -50,7 +50,7 @@ class UsersControllerTest < ActionController::TestCase
50 end 50 end
51 51
52 assert_redirected_to user_path(User.last) 52 assert_redirected_to user_path(User.last)
53 assert !User.last.admin 53 assert_not User.last.is_admin?
54 end 54 end
55 55
56 test "creating a Redaktion account" do 56 test "creating a Redaktion account" do