Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions app/controllers/people_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -721,13 +721,16 @@ def format_duplicate(person, exact: true, entered_email: nil, entered_emails: ni
end

# Where to send the admin after a successful update. Defaults to the person's
# profile, but returns to Data health or the registration org-linking page when
# the edit was opened from there (preserving that page's own return_to).
# profile, but returns to Data health, the registration org-linking page, or the
# comments & communications feed when the edit was opened from there
# (preserving that page's own return_to).
def person_update_return_path
if params[:return_to] == "data_health"
helpers.data_health_return_path(params[:check])
elsif params[:return_to] == "dedupe"
dedupe_index_people_path(anchor: "dedupe-record-#{@person.id}")
elsif params[:return_to] == CommentsHelper::FEED_RETURN_TO
helpers.feed_return_path
elsif params[:return_to] == "registration_link" && params[:event_registration_id].present?
link_organization_event_registration_path(params[:event_registration_id], return_to: params[:link_org_return_to].presence)
else
Expand Down
21 changes: 21 additions & 0 deletions app/decorators/affiliation_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,31 @@ def period_label
"Dates not recorded"
end

# E.g. "Started March 2023, ending next month" — relative wording for months
# close to now, so an imminent lapse reads at a glance.
def period_note
parts = []
parts << "#{start_date > Date.current ? 'starting' : 'started'} #{relative_month(start_date)}" if start_date
parts << "#{end_date < Date.current ? 'ended' : 'ending'} #{relative_month(end_date)}" if end_date
parts.join(", ").upcase_first.presence || "Dates not recorded"
end

# Where a back link should land on the person/organization editor. An inactive
# row sits on the Inactive tab, so jumping to the row itself would scroll to
# something the page isn't showing — land on the section instead.
def return_anchor
active? ? h.dom_id(object) : "affiliations"
end

private

def relative_month(date)
today = Date.current
case (date.year * 12 + date.month) - (today.year * 12 + today.month)
when -1 then "last month"
when 0 then "this month"
when 1 then "next month"
else date.strftime("%B %Y")
end
end
end
16 changes: 16 additions & 0 deletions app/decorators/comment_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ def dedupe_label
body.to_s.squish.truncate(ModelDeduper::SNIPPET_LENGTH, separator: " ")
end

# The person this comment is about: whoever its source chip names, or the person
# behind any other record it's on (affiliation, author, …). nil for records that
# belong to no one person, like an event or organization.
def about_person
h.record_chip_person(commentable) || person_behind_commentable
end

def author_name
(updated_by || created_by)&.full_name || "System"
end
Expand Down Expand Up @@ -57,6 +64,15 @@ def event

private

def person_behind_commentable
case commentable
when User, Affiliation, TopicSubscription, FormSubmission then commentable.person
when StaffTagging then commentable.staff_taggable if commentable.staff_taggable.is_a?(Person)
when Grant then commentable.funder if commentable.funder.is_a?(Person)
else commentable.author_person if commentable.respond_to?(:author_person)
end
end

# Root record the shared event chip resolves a registration/event from.
def event_chip_record
commentable
Expand Down
13 changes: 13 additions & 0 deletions app/decorators/person_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,19 @@ def facilitator_status_label
"Inactive"
end

# Facilitator rows running now, earliest first.
def active_facilitator_affiliations
affiliations.select { |affiliation| affiliation.facilitator? && affiliation.active? }
.sort_by { |affiliation| affiliation.start_date || Date.current }
end

# Their latest facilitator-training registrations, newest event first, whatever
# the attendance outcome.
def recent_facilitator_training_registrations(limit: 3)
event_registrations.joins(:event).merge(Event.facilitator_trainings)
.includes(:event).order(events: { start_date: :desc }).limit(limit)
end

def facilitator_since_range
date_range_display(facilitator_since_date, facilitation_end_date, ended_title: "No active facilitator affiliations")
end
Expand Down
26 changes: 26 additions & 0 deletions app/helpers/comments_helper.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module CommentsHelper
FEED_FILTER_FORM_ID = "comments-and-communications-filters"
FEED_RETURN_TO = "comments_and_communications".freeze

# The full feed narrowed to one comment or communication, carrying the person
# it's about and the record to return to for the eyebrow.
Expand All @@ -13,6 +14,31 @@ def feed_entry_path(entry, person: nil, return_to: nil)
}.compact)
end

# A person chip's edit link, remembering which feed (a person's, or everyone's)
# to return to.
def feed_person_edit_path(person, feed_person: nil)
edit_person_path(person, { return_to: FEED_RETURN_TO, feed_person_id: feed_person&.id }.compact)
end

def feed_return_path
comments_and_communications_path({ person_id: params[:feed_person_id].presence }.compact)
end

# Names who a row on the everyone feed is about. A communication's contact
# falls back to their raw email when no person is on file.
def feed_person_chip(entry)
if entry.is_a?(Comment)
person = entry.decorate.about_person
email = nil
else
person = entry.decorate.contact_person
email = entry.recipient_email
end
return if person.nil? && email.blank?

render "comments_and_communications/about_person_chip", person: person, email: email
end

# Stimulus wiring for a feed row the chevron and "Expand/Collapse all" toggle.
# clamp: a one-line row unclamped to show the full entry; otherwise a full row
# whose body hides on collapse. key lets expandable-cards remember a row toggled
Expand Down
11 changes: 9 additions & 2 deletions app/helpers/person_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def person_profile_button(person, truncate_at: nil, subtitle: nil, display_name:
# its own line below, mirroring person_profile_button. Admin-facing, so it shows
# the full name — the person's display-name preference (first-name-only, etc.)
# governs public credits, not what an admin sees here.
def person_edit_button(person, layout: :prefix, truncate_at: nil, subtitle: nil, display_name: nil, data: {}, inactive: false, tint: nil, path_params: {}, width_class: "w-full", compact: false)
def person_edit_button(person, layout: :prefix, truncate_at: nil, subtitle: nil, display_name: nil, data: {}, inactive: false, tint: nil, path_params: {}, width_class: "w-full", compact: false, show_title: true)
padding = compact ? "px-2 py-1" : "px-4 py-2"
name_text_size = compact ? "text-xs" : "text-sm"

Expand All @@ -98,7 +98,7 @@ def person_edit_button(person, layout: :prefix, truncate_at: nil, subtitle: nil,

link_to edit_person_path(person, **path_params),
data: { turbo_prefetch: false }.merge(data),
title: hover_title,
title: (hover_title if show_title),
class: "group relative flex items-center gap-2
#{width_class} #{padding}
border #{palette[:border]} #{palette[:bg]} #{palette[:hover_bg]} rounded-lg
Expand Down Expand Up @@ -156,6 +156,13 @@ def user_link(user, data: {})
# A piece the person hid (a `profile_show_*` flag turned off) stays visible to
# them and to admins under the blue admin-or-owner wash. `flag` is a
# `profile_show_*` attribute name (symbol).
# Memoized per person so a feed naming the same person on every row renders
# (and queries) their card once.
def facilitator_hover_card(person, header: false)
@facilitator_hover_cards ||= {}
@facilitator_hover_cards[[ person.id, header ]] ||= render("people/facilitator_hover_card", person: person, header: header)
end

def profile_piece_shown?(person, flag)
person.public_send(:"#{flag}?") || profile_piece_hidden_for_viewer?(person, flag)
end
Expand Down
18 changes: 18 additions & 0 deletions app/views/comments_and_communications/_about_person_chip.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<%# Who an everyone-feed row is about: a link to edit that person, with their
facilitator hover card, or a plain email chip when no person is on
file. Locals: person (may be nil), email. %>
<% chip_class = "inline-flex max-w-48 shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium #{DomainTheme.border_class_for(:people, intensity: 200)} #{DomainTheme.bg_class_for(:people, intensity: 50)} #{DomainTheme.text_class_for(:people)}" %>
<% if person %>
<span class="group/person relative inline-flex">
<%= link_to feed_person_edit_path(person), target: "_blank", rel: "noopener", class: "#{chip_class} hover:underline" do %>
<i class="fa-solid fa-user text-2xs" aria-hidden="true"></i>
<span class="truncate"><%= person.full_name %></span>
<% end %>
<%= facilitator_hover_card(person, header: true) %>
</span>
<% else %>
<span class="<%= chip_class %>" title="No person on file for this email">
<i class="fa-solid fa-envelope text-2xs" aria-hidden="true"></i>
<span class="truncate"><!--email_off--><%= email %><!--email_on--></span>
</span>
<% end %>
4 changes: 3 additions & 1 deletion app/views/comments_and_communications/_comment_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
person feed's records the comment can be moved to). %>
<% person = local_assigns[:person] %>
<% source_chip = feed_source_chip(entry.commentable, icon: content_tag(:i, "", class: "fa-solid fa-comment text-gray-500", "aria-hidden": "true", title: "Comment")) %>
<%# Only the everyone feed names who each row is about. %>
<% leading = local_assigns[:person] ? source_chip : safe_join([ feed_person_chip(entry), source_chip || entry.decorate.type_chip ].compact, " ") %>
<% edit_button = content_tag(:button, safe_join([ content_tag(:i, "", class: "fa-solid fa-pen text-2xs"), " Edit" ]),
type: "button",
class: "ml-auto shrink-0 text-xs #{eyebrow_link_class}",
Expand All @@ -16,7 +18,7 @@
data: { edit_toggle_target: "view", **expandable_feed_row_data(clamp: false, key: dom_id(entry)) } do %>
<div class="flex flex-wrap items-start gap-x-1 gap-y-1">
<span class="mt-0.5 inline-flex w-4 shrink-0 justify-center leading-none"><%= render "comments/flag_toggle", comment: entry %></span>
<%= render "comments/comment_line", comment: entry, truncate: false, leading: source_chip, trailing: edit_button, expandable: true,
<%= render "comments/comment_line", comment: entry, truncate: false, leading: leading, trailing: edit_button, expandable: true,
date_path: feed_entry_path(entry, person: person) %>
</div>
<% end %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
on the save so the re-rendered row stays scoped to them). %>
<% person = local_assigns[:person] %>
<% source_chip = feed_source_chip(entry.noticeable, icon: entry.decorate.channel_icon) %>
<%# Only the everyone feed names who each row is about. %>
<% leading = local_assigns[:person] ? source_chip : safe_join([ feed_person_chip(entry), source_chip || entry.decorate.channel_chip ].compact, " ") %>
<% edit_button = content_tag(:button, safe_join([ content_tag(:i, "", class: "fa-solid fa-pen text-2xs"), " Edit" ]),
type: "button",
class: "shrink-0 text-xs #{eyebrow_link_class}",
Expand All @@ -17,8 +19,8 @@
<div id="<%= dom_id(entry) %>" data-controller="edit-toggle">
<%= tag.div data: { edit_toggle_target: "view", **expandable_feed_row_data(clamp: false, key: dom_id(entry)) } do %>
<%= render "notifications/communication_line", notification: entry, admin: true, truncate: false, expandable: true,
date_path: feed_entry_path(entry, person: person),
leading: source_chip, trailing: trailing, flag: render("notifications/flag_toggle", notification: entry) %>
date_path: feed_entry_path(entry, person: person), feed_person: person,
leading: leading, trailing: trailing, flag: render("notifications/flag_toggle", notification: entry) %>
<% end %>

<div data-edit-toggle-target="edit" class="hidden rounded-md border <%= communication_card_class %> px-3 py-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
Notification where entry.manual_log? is false). Optional: person (scopes the
date link's feed). %>
<% source_chip = feed_source_chip(entry.noticeable, icon: entry.decorate.channel_icon) %>
<%# Only the everyone feed names who each row is about. %>
<% leading = local_assigns[:person] ? source_chip : safe_join([ feed_person_chip(entry), source_chip || entry.decorate.channel_chip ].compact, " ") %>
<% trailing = content_tag(:span, render("notifications/responded_checkbox", notification: entry), class: "ml-auto shrink-0") %>
<%= tag.div id: dom_id(entry), data: expandable_feed_row_data(clamp: false, key: dom_id(entry)) do %>
<%= render "notifications/communication_line", notification: entry, admin: true, truncate: false, expandable: true,
date_path: feed_entry_path(entry, person: local_assigns[:person]),
leading: source_chip, trailing: trailing, flag: render("notifications/flag_toggle", notification: entry) %>
date_path: feed_entry_path(entry, person: local_assigns[:person]), feed_person: local_assigns[:person],
leading: leading, trailing: trailing, flag: render("notifications/flag_toggle", notification: entry) %>
<% end %>
10 changes: 3 additions & 7 deletions app/views/comments_and_communications/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@
<% subtitle = capture do %>
<%= pluralize(@total_count, "entry", plural: "entries") %><% if @flagged_count.positive? %> · <span class="font-medium text-orange-600"><%= @flagged_count %> flagged</span><% end %>
<% end %>
<%# Title: the person's name (or "Everyone"). On a person's feed their activity,
profile, and the composer triggers sit beside it; the composer panels open in
<%# Title: the person's profile chip (or "Everyone"). On a person's feed their
activity and the composer triggers sit beside it; the composer panels open in
the body, sharing the card-level panel-toggle scope (content_data). The person
picker that scopes the feed lives in the filter row below. %>
<% title = capture do %>
<span class="flex flex-wrap items-center gap-3">
<%= @person ? tag.span(@person.full_name, title: @person.preferred_email) : "Everyone" %>
<%= @person ? render("people/person_chip", person: @person) : "Everyone" %>
<% if @person %>
<span class="flex flex-wrap items-center gap-3 font-sans">
<% if allowed_to?(:index?, with: Admin::AhoyActivityPolicy) %>
Expand All @@ -42,10 +42,6 @@
<%= @person.first_name %>'s activity
<% end %>
<% end %>
<%= link_to person_path(@person), class: button_classes(:primary_outline) do %>
<i class="fa-solid fa-user"></i>
<%= @person.first_name %>'s profile
<% end %>
<%= render "comments_and_communications/add_buttons" %>
</span>
<% end %>
Expand Down
20 changes: 16 additions & 4 deletions app/views/notifications/_communication_line.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
neutral source chip, pointing at that record's edit page, with the channel icon
inside it). Optional expandable adds a chevron that unclamps a truncated row
(outside the card link so it doesn't navigate) or hides a full row's body;
date_path (+ date_new_tab) links the timestamp to the feed narrowed to this entry. %>
date_path (+ date_new_tab) links the timestamp to the feed narrowed to this entry;
feed_person is the person whose feed the row sits on, for the sender chip's back link. %>
<% admin = local_assigns.fetch(:admin, true) %>
<% mark_admin_only = local_assigns.fetch(:mark_admin_only, false) %>
<% truncate_body = local_assigns.fetch(:truncate, true) %>
Expand Down Expand Up @@ -39,9 +40,20 @@
path: local_assigns[:date_path], new_tab: local_assigns[:date_new_tab] %>
<% end %>
<% main = capture do %>
<span class="inline-flex w-20 shrink-0 items-center">
<span class="<%= chip[0] %> <%= chip[1] %> max-w-full truncate rounded px-1.5 py-0.5 text-left text-xs" title="<%= decorated.from_title.presence || from_name %>"><%= truncate(from_first.to_s, length: 10, omission: "..") %></span>
</span>
<% from_chip_class = "#{chip[0]} #{chip[1]} max-w-full truncate rounded px-1.5 py-0.5 text-left text-xs" %>
<% from_label = truncate(from_first.to_s, length: 10, omission: "..") %>
<% if !truncate_body && decorated.from_person %>
<%# Only the untruncated row can link the person: a card-wide link can't nest one. %>
<span class="group/person relative inline-flex w-20 shrink-0 items-center">
<%= link_to from_label, feed_person_edit_path(decorated.from_person, feed_person: local_assigns[:feed_person]), target: "_blank", rel: "noopener",
class: "#{from_chip_class} hover:underline" %>
<%= facilitator_hover_card(decorated.from_person, header: true) %>
</span>
<% else %>
<span class="inline-flex w-20 shrink-0 items-center">
<span class="<%= from_chip_class %>" title="<%= decorated.from_title.presence || from_name %>"><%= from_label %></span>
</span>
<% end %>
<%# Subject and body share one column so the body starts flush with the subject. %>
<% if truncate_body %>
<div class="flex min-w-0 flex-1 items-start gap-1 text-sm">
Expand Down
Loading
Loading