-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[#55581] popover for user information on hover #17255
Changes from 76 commits
3249503
b1e38ec
7e72f62
fe106c3
89587fd
e7c16e7
8354574
ef22234
3f2ab1d
ca90dbe
277b9dc
1f554f2
4f6fd0d
8aff877
928ab41
811fbd3
b096073
c37731e
8ef3486
869636e
3bef3eb
d334568
ceb96aa
3246793
792e1dd
eb7bd5a
3a9b424
87eeae3
3e7d857
da077fa
450f2cb
3dc40b5
ed0ff7f
f1ebb2e
d83fb1a
baf07d4
dc77227
4269e4a
8f54110
e6d893e
af3b720
458bea9
81df01e
9a9e7b2
ec9ffdf
30a4481
f7d6d83
9e98465
e5e8f60
b9d4fa5
575a5c0
2501760
52cdcdd
90acc21
52f64f8
4b5eba5
3f22bd3
5d18642
c3d8532
2ad5f34
9d9faba
b063953
b1f0004
8fe734b
3db68a4
5496530
69a2eee
b4fed15
7661b57
f60f744
ba84c3f
3c20a5f
8a1f7b9
01d9bde
ab9ea70
f6ddf7f
2462494
4c17609
818876a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
<%#-- copyright | ||
OpenProject is an open source project management software. | ||
Copyright (C) the OpenProject GmbH | ||
|
||
This program is free software; you can redistribute it and/or | ||
modify it under the terms of the GNU General Public License version 3. | ||
|
||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
Copyright (C) 2006-2013 Jean-Philippe Lang | ||
Copyright (C) 2010-2013 the ChiliProject Team | ||
|
||
This program is free software; you can redistribute it and/or | ||
modify it under the terms of the GNU General Public License | ||
as published by the Free Software Foundation; either version 2 | ||
of the License, or (at your option) any later version. | ||
|
||
This program is distributed in the hope that it will be useful, | ||
but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
GNU General Public License for more details. | ||
|
||
You should have received a copy of the GNU General Public License | ||
along with this program; if not, write to the Free Software | ||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
|
||
See COPYRIGHT and LICENSE files for more details. | ||
|
||
++#%> | ||
|
||
<%= | ||
if @user.present? | ||
flex_layout(classes: 'op-user-hover-card', data: { test_selector: "user-hover-card-#{@user.id}" }) do |flex| | ||
flex.with_row do | ||
render(Users::AvatarComponent.new(user: @user, show_name: false, link: false, hover_card: { active: false })) | ||
end | ||
|
||
flex.with_row do | ||
flex_layout(classes: 'op-user-hover-card--info') do |f| | ||
f.with_column(classes: 'op-user-hover-card--name') do | ||
render(Primer::Beta::Text.new(font_weight: :semibold, data: { test_selector: 'user-hover-card-name' })) do | ||
@user.name | ||
end | ||
end | ||
|
||
if show_email? | ||
f.with_column(classes: 'op-user-hover-card--email') do | ||
render(Primer::Beta::Text.new(font_size: :small, | ||
color: :muted, | ||
data: { test_selector: 'user-hover-card-email' })) do | ||
@user.mail | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
flex.with_row do | ||
flex_layout(classes: 'op-user-hover-card--group-list') do |f| | ||
f.with_column do | ||
render(Primer::Beta::Octicon.new(icon: :people)) | ||
end | ||
|
||
f.with_column do | ||
render(Primer::Beta::Text.new(color: :muted, data: { test_selector: 'user-hover-card-groups' })) do | ||
group_membership_summary | ||
end | ||
end | ||
end | ||
end | ||
|
||
flex.with_row do | ||
render(Primer::Beta::Button.new(tag: :a, | ||
href: helpers.allowed_management_user_profile_path(@user), | ||
data: { test_selector: 'user-hover-card-profile-btn' })) do | ||
I18n.t("users.open_profile") | ||
end | ||
end | ||
end | ||
else | ||
render Primer::Beta::Blankslate.new(border: false, narrow: true) do |component| | ||
component.with_visual_icon(icon: "x-circle") | ||
# Show a generic error message to avoid leaking information | ||
component.with_heading(tag: :h3).with_content(I18n.t("http.response.unexpected")) | ||
end | ||
end | ||
%> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
# frozen_string_literal: true | ||
|
||
#-- copyright | ||
# OpenProject is an open source project management software. | ||
# Copyright (C) the OpenProject GmbH | ||
# | ||
# This program is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License version 3. | ||
# | ||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
# Copyright (C) 2006-2013 Jean-Philippe Lang | ||
# Copyright (C) 2010-2013 the ChiliProject Team | ||
# | ||
# This program is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License | ||
# as published by the Free Software Foundation; either version 2 | ||
# of the License, or (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program; if not, write to the Free Software | ||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
# | ||
# See COPYRIGHT and LICENSE files for more details. | ||
#++ | ||
|
||
class Users::HoverCardComponent < ApplicationComponent | ||
include OpPrimer::ComponentHelpers | ||
|
||
def initialize(id:) | ||
super | ||
|
||
@id = id | ||
@user = User.find_by(id: @id) | ||
end | ||
|
||
def show_email? | ||
(@user == User.current) || User.current.allowed_globally?(:view_user_email) | ||
end | ||
|
||
# Constructs a string in the form of: | ||
# "Member of group4, group5" | ||
# or | ||
# "Member of group1, group2 and 3 more" | ||
# The latter string is cut off since the complete list of group names would exceed the allowed `max_length`. | ||
def group_membership_summary(max_length = 40) | ||
groups = @user.groups.visible | ||
return no_group_text if groups.empty? | ||
|
||
group_links = linked_group_names(groups) | ||
|
||
cutoff_index = calculate_cutoff_index(groups.map(&:name), max_length) | ||
build_summary(group_links, cutoff_index) | ||
end | ||
|
||
private | ||
|
||
def linked_group_names(groups) | ||
groups.map { |group| link_to(h(group.name), show_group_path(group)) } | ||
end | ||
|
||
def no_group_text | ||
t("users.groups.no_results_title_text") | ||
end | ||
|
||
# Calculate the index at which to cut off the group names, based on plain text length | ||
def calculate_cutoff_index(names, max_length) | ||
current_length = 0 | ||
|
||
names.each_with_index do |name, index| | ||
new_length = current_length + name.length + (index > 0 ? 2 : 0) # 2 for ", " separator | ||
return index if new_length > max_length | ||
|
||
current_length = new_length | ||
end | ||
|
||
names.size # No cutoff needed -> return the total size | ||
end | ||
|
||
def build_summary(links, cutoff_index) | ||
summary_links = safe_join(links[0...cutoff_index], ", ") | ||
remaining_count = links.size - cutoff_index | ||
remaining_count_link = link_to(t("users.groups.more", count: remaining_count), user_path(@user)) | ||
|
||
if remaining_count > 0 | ||
t("users.groups.summary_with_more", names: summary_links, count_link: remaining_count_link).html_safe | ||
else | ||
t("users.groups.summary", names: summary_links).html_safe | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,24 @@ | ||||||
// Correct the z-index of the regular hover card container so that it is above the dropdown of user auto completers | ||||||
.spot-modal-overlay:has(.op-user-hover-card) | ||||||
z-index: 9600 | ||||||
|
||||||
// On the project list page, there is a bug when a hover card is invoked within the share dialog. The global spot | ||||||
// modal overlay will darken the background while the hover card is active, since its semi-transparent bg shading is | ||||||
// added on top of the other dialog background shaders. We don't want an additional spot modal background here, | ||||||
// so we disable it for this edge case. | ||||||
.controller-projects.action-index | ||||||
.spot-modal-overlay:not(:has(.op-user-hover-card)) | ||||||
background: transparent | ||||||
|
||||||
.op-user-hover-card | ||||||
gap: 1rem | ||||||
overflow: hidden | ||||||
|
||||||
.op-user-hover-card--info | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
this could be just &--info. This ensures that you're using the BEM style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very nice, thanks! The previous class definitions were very cumbersome. Much better like this. |
||||||
gap: 0.5rem | ||||||
|
||||||
.op-user-hover-card--name, .op-user-hover-card--email | ||||||
EinLama marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
@include text-shortener() | ||||||
|
||||||
.op-user-hover-card--group-list | ||||||
EinLama marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
gap: 0.5rem |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# frozen_string_literal: true | ||
|
||
#-- copyright | ||
# OpenProject is an open source project management software. | ||
# Copyright (C) the OpenProject GmbH | ||
# | ||
# This program is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License version 3. | ||
# | ||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
# Copyright (C) 2006-2013 Jean-Philippe Lang | ||
# Copyright (C) 2010-2013 the ChiliProject Team | ||
# | ||
# This program is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License | ||
# as published by the Free Software Foundation; either version 2 | ||
# of the License, or (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program; if not, write to the Free Software | ||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
# | ||
# See COPYRIGHT and LICENSE files for more details. | ||
#++ | ||
class Users::HoverCardController < ApplicationController | ||
no_authorization_required! :show | ||
|
||
def show | ||
@id = params[:id] | ||
render layout: nil | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is the value 40 coming from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A true magic number 🔮🧙🏻♂️ found by, uh... guessing and trying it out! 🪄
Seemed sensible when used with the English translation and should look okay with most other languages. The caveat is that only the group names are considered when deciding where to shorten the list. To make it perfect, we'd have to load the translation first, factor it's length into the calculation and THEN decide where to cut off the group list. I figured the value
40
is good enough in most cases and strikes a balance between pleasant looks and code complexity :)