Skip to content
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

[50254] Filtering on share modal #14075

Merged
merged 18 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8da8e25
Add (dummy) action menus for filtering the list of shares in the modal
HDinger Nov 1, 2023
9b4b631
Toggle between filters and bulkActions in the share modal
HDinger Nov 1, 2023
dc5d8e6
Add test cases for filtering the shareWP modal
HDinger Nov 2, 2023
3a955ce
Extend MemberQuery by filters for the entity and use that in the shar…
HDinger Nov 3, 2023
a40f039
Add role filter for share modal
HDinger Nov 3, 2023
5ea3b5e
Replace form by direct links as it enables us to reset a filter witho…
HDinger Nov 7, 2023
9ec35b9
Add (empty) type filter options to the share modal
HDinger Nov 8, 2023
0518522
Add principal type filter for the sharing modal
HDinger Nov 8, 2023
613e987
Add additional test case for using both filters at the same time
HDinger Nov 8, 2023
43545ef
Add AlsoProjectMemberFilter for members
klaustopher Nov 9, 2023
7f58d29
Add "AND" keyword to sql query && always set the entity to the curren…
HDinger Nov 9, 2023
96c118f
Add check for currenltly active type filter
HDinger Nov 9, 2023
5f5eacf
Kepp currently selected role filter when filtering the type (and the …
HDinger Nov 10, 2023
3c480d2
Adapt tests to new members filters
HDinger Nov 10, 2023
9cb3724
Merge branch 'dev' into implementation/50254-filtering-on-share-modal
aaron-contreras Nov 13, 2023
e461b35
Fix: Register `WorkPackageMemberQuery`
aaron-contreras Nov 13, 2023
1c478e3
Fix: Ammend stream actions to enable functional responsiveness behavior
aaron-contreras Nov 13, 2023
a51be98
Cleanup: Remove extracted border box custom patches
aaron-contreras Nov 13, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
anchor_align: :end,
color: :subtle,
data: { test_selector: 'op-share-wp-bulk-update-role'})) do |menu|
menu.with_show_button(data: { 'work-packages--share--bulk-selection-target': 'bulkUpdateRoleLabel' }) do |button|
menu.with_show_button(scheme: :invisible, color: :subtle, data: { 'work-packages--share--bulk-selection-target': 'bulkUpdateRoleLabel' }) do |button|
HDinger marked this conversation as resolved.
Show resolved Hide resolved
button.with_trailing_action_icon(icon: "triangle-down")
'Placeholder'
end
Expand Down
60 changes: 51 additions & 9 deletions app/components/work_packages/share/modal_body_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
render(WorkPackages::Share::InviteUserFormComponent.new(work_package: @work_package))
end

if shared_principals.none?
if @shares.blank?
modal_content.with_row(mt: 3) do
render(Primer::Beta::Blankslate.new(border: true)) do |component|
component.with_visual_icon(icon: :people, size: :medium)
Expand All @@ -28,13 +28,59 @@
border_box.with_header(color: :subtle, data: { 'test-selector': 'op-share-wp-header' }) do
grid_layout('op-share-wp-modal-body--header', tag: :div, align_items: :center) do |header_grid|
header_grid.with_area(:counter, tag: :div) do
render(WorkPackages::Share::CounterComponent.new(work_package: @work_package, count: shared_principals.size))
render(WorkPackages::Share::CounterComponent.new(work_package: @work_package, count: @shares.size))
end

header_grid.with_area(:actions,
tag: :div,
data: { 'work-packages--share--bulk-selection-target': 'defaultActions' }) do
flex_layout do |header_actions|
header_actions.with_column(mr: 2) do
render(Primer::Alpha::ActionMenu.new(anchor_align: :end,
select_variant: :single,
color: :subtle,
data: { 'test-selector': 'op-share-wp-filter-type' })) do |menu|
menu.with_show_button(scheme: :invisible, color: :subtle) do |button|
button.with_trailing_action_icon(icon: "triangle-down")
I18n.t('work_package.sharing.filter.type')
end
type_filter_options.each do |option|
menu.with_item(label: option[:label],
href: filter_url(type_option: option),
method: :get,
tag: :a,
active: type_filter_option_active?(option),
role: "menuitem")
end
end
end

header_actions.with_column do
render(Primer::Alpha::ActionMenu.new(anchor_align: :end,
select_variant: :single,
color: :subtle,
data: { 'test-selector': 'op-share-wp-filter-role' })) do |menu|
menu.with_show_button(scheme: :invisible, color: :subtle) do |button|
button.with_trailing_action_icon(icon: "triangle-down")
I18n.t('work_package.sharing.filter.role')
end
options.each do |option|
menu.with_item(label: option[:label],
href: filter_url(role_option: option),
method: :get,
tag: :a,
active: role_filter_option_active?(option),
role: "menuitem")
end
end
end
end
end

header_grid.with_area(:actions,
tag: :div,
hidden: true, # Prevent flicker on initial render
data: { 'work-packages--share--bulk-selection-target': 'actions' }) do
data: { 'work-packages--share--bulk-selection-target': 'bulkActions' }) do
if sharing_manageable?
concat(
render(WorkPackages::Share::BulkPermissionButtonComponent.new(work_package: @work_package))
Expand All @@ -52,12 +98,8 @@
end
end

shared_principals.each do |principal|
share = principal.work_package_shares
.where(entity: @work_package)
.first

render(WorkPackages::Share::ShareRowComponent.new(share:, container: border_box))
@shares.each do |share|
render(WorkPackages::Share::ShareRowComponent.new(share: share, container: border_box))
end
end
end
Expand Down
114 changes: 106 additions & 8 deletions app/components/work_packages/share/modal_body_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ module WorkPackages
module Share
class ModalBodyComponent < ApplicationComponent
include ApplicationHelper
include MemberHelper
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
include WorkPackages::Share::Concerns::Authorization
include WorkPackages::Share::Concerns::DisplayableRoles

def initialize(work_package:)
def initialize(work_package:, shares:)
super

@work_package = work_package
@shares = shares
end

def self.wrapper_key
Expand All @@ -54,13 +57,108 @@ def insert_target_modifier_id
'op-share-wp-active-shares'
end

def shared_principals
@shared_principals ||= Principal
.having_entity_membership(@work_package)
.includes(work_package_shares: :roles)
.where(work_package_shares: { entity: @work_package })
.merge(MemberRole.only_non_inherited)
.ordered_by_name
def type_filter_options
[
{ label: I18n.t('work_package.sharing.filter.project_member'),
value: { principal_type: 'User', project_member: true } },
{ label: I18n.t('work_package.sharing.filter.not_project_member'),
value: { principal_type: 'User', project_member: false } },
{ label: I18n.t('work_package.sharing.filter.project_group'),
value: { principal_type: 'Group', project_member: true } },
{ label: I18n.t('work_package.sharing.filter.not_project_group'),
value: { principal_type: 'Group', project_member: false } }
]
end

def type_filter_option_active?(_option)
principal_type_filter_value = current_filter_value(params[:filters], 'principal_type')
project_member_filter_value = current_filter_value(params[:filters], 'also_project_member')

return false if principal_type_filter_value.nil? || project_member_filter_value.nil?

principal_type_checked =
_option[:value][:principal_type] == principal_type_filter_value
membership_selected =
_option[:value][:project_member] == ActiveRecord::Type::Boolean.new.cast(project_member_filter_value)

principal_type_checked && membership_selected
end

def role_filter_option_active?(_option)
role_filter_value = current_filter_value(params[:filters], 'role_id')

return false if role_filter_value.nil?

find_role_ids(_option[:value]).first == role_filter_value.to_i
end

def filter_url(type_option: nil, role_option: nil)
return work_package_shares_path if type_option.nil? && role_option.nil?

args = {}
filter = []

filter += apply_role_filter(role_option)
filter += apply_type_filter(type_option)

args[:filters] = filter.to_json unless filter.empty?

work_package_shares_path(args)
end

def apply_role_filter(_option)
current_role_filter_value = current_filter_value(params[:filters], 'role_id')
filter = []

if _option.nil? && current_role_filter_value.present?
# When there is already a role filter set and no new value passed, we want to keep that filter
filter = role_filter_for({ value: current_role_filter_value }, builtin_role: false)
elsif _option.present? && !role_filter_option_active?(_option)
# Only when the passed filter option is not the currently selected one, we apply the filter
filter = role_filter_for(_option)
end

filter
end

def role_filter_for(_option, builtin_role: true)
[{ role_id: { operator: "=", values: builtin_role ? find_role_ids(_option[:value]) : [_option[:value]] } }]
end

def apply_type_filter(_option)
current_type_filter_value = current_filter_value(params[:filters], 'principal_type')
current_member_filter_value = current_filter_value(params[:filters], 'also_project_member')
filter = []

if _option.nil? && current_type_filter_value.present? && current_member_filter_value.present?
# When there is already a type filter set and no new value passed, we want to keep that filter
value = { value: { principal_type: current_type_filter_value, project_member: current_member_filter_value } }
filter = type_filter_for(value)
elsif _option.present? && !type_filter_option_active?(_option)
# Only when the passed filter option is not the currently selected one, we apply the filter
filter = type_filter_for(_option)
end

filter
end

def type_filter_for(_option)
filter = []
if ActiveRecord::Type::Boolean.new.cast(_option[:value][:project_member])
filter.push({ also_project_member: { operator: "=", values: [OpenProject::Database::DB_VALUE_TRUE] } })
else
filter.push({ also_project_member: { operator: "=", values: [OpenProject::Database::DB_VALUE_FALSE] } })
end

filter.push({ principal_type: { operator: "=", values: [_option[:value][:principal_type]] } })
filter
end

def current_filter_value(filters, filter_key)
return nil if filters.nil?

given_filters = JSON.parse(filters).find { |key| key.key?(filter_key) }
given_filters ? given_filters[filter_key]['values'].first : nil
end
end
end
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/concerns/member_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
module MemberHelper
module_function

def find_role_ids(builtin_value)
# Role has a left join on permissions included leading to multiple ids being returned which
# is why we unscope.
WorkPackageRole.unscoped.where(builtin: builtin_value).pluck(:id)
end

def find_or_create_users(send_notification: true)
@send_notification = send_notification

Expand Down
32 changes: 19 additions & 13 deletions app/controllers/work_packages/shares/bulk_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@

class WorkPackages::Shares::BulkController < ApplicationController
include OpTurbo::ComponentStream
include MemberHelper

before_action :find_work_package
before_action :find_shares
before_action :find_role_ids, only: :update
before_action :find_selected_shares
before_action :find_role_ids_from_params, only: :update
before_action :find_project
before_action :authorize

def update
@shares.each do |share|
@selected_shares.each do |share|
WorkPackageMembers::CreateOrUpdateService
.new(user: current_user)
.call(entity: @work_package,
Expand All @@ -50,7 +51,7 @@ def update
end

def destroy
@shares.each do |share|
@selected_shares.each do |share|
WorkPackageMembers::DeleteService
.new(user: current_user, model: share)
.call
Expand All @@ -66,7 +67,7 @@ def destroy
private

def respond_with_update_permission_buttons
@shares.each do |share|
@selected_shares.each do |share|
replace_via_turbo_stream(
component: WorkPackages::Share::PermissionButtonComponent.new(share:,
data: { 'test-selector': 'op-share-wp-update-role' })
Expand All @@ -78,14 +79,14 @@ def respond_with_update_permission_buttons

def respond_with_replace_modal
replace_via_turbo_stream(
component: WorkPackages::Share::ModalBodyComponent.new(work_package: @work_package)
component: WorkPackages::Share::ModalBodyComponent.new(work_package: @work_package, shares: find_shares)
)

respond_with_turbo_streams
end

def respond_with_remove_shares
@shares.each do |share|
@selected_shares.each do |share|
remove_via_turbo_stream(
component: WorkPackages::Share::ShareRowComponent.new(share:)
)
Expand All @@ -107,15 +108,20 @@ def find_project
end

def find_shares
@shares = Member.includes(:principal)
@shares = Member.includes(:principal, :member_roles)
.references(:member_roles)
.of_work_package(@work_package)
.where(id: params[:share_ids])
.merge(MemberRole.only_non_inherited)
end

def find_role_ids
@role_ids = WorkPackageRole.unscoped
.where(builtin: params[:role_ids])
.pluck(:id)
def find_selected_shares
@selected_shares = Member.includes(:principal)
.of_work_package(@work_package)
.where(id: params[:share_ids])
end

def find_role_ids_from_params
@role_ids = find_role_ids(params[:role_ids])
end

def current_visible_member_count
Expand Down
Loading
Loading