Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -433,5 +433,5 @@ end
source "https://rubygems.org", cooldown: 0 do
gem "openproject-octicons", "~>19.35.0"
gem "openproject-octicons_helper", "~>19.35.0"
gem "openproject-primer_view_components", "~>0.88.0"
gem "openproject-primer_view_components", "~>0.89.0"
end
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,7 @@ GEM
actionview
openproject-octicons (= 19.35.0)
railties
openproject-primer_view_components (0.88.0)
openproject-primer_view_components (0.89.0)
actionview (>= 7.2.0)
activesupport (>= 7.2.0)
openproject-octicons (>= 19.34.0)
Expand Down Expand Up @@ -1688,7 +1688,7 @@ DEPENDENCIES
openproject-octicons (~> 19.35.0)!
openproject-octicons_helper (~> 19.35.0)!
openproject-openid_connect!
openproject-primer_view_components (~> 0.88.0)!
openproject-primer_view_components (~> 0.89.0)!
openproject-recaptcha!
openproject-reporting!
openproject-resource_management!
Expand Down Expand Up @@ -2070,7 +2070,7 @@ CHECKSUMS
openproject-octicons (19.35.0) sha256=a5033550d0961b4a8cb0993512a899716d633e17c2b5147bc6a9ed74f3952b38
openproject-octicons_helper (19.35.0) sha256=c32d142a4bb7fda739b16768aa8846fd88ffc1750509d8056f516056e8767361
openproject-openid_connect (1.0.0)
openproject-primer_view_components (0.88.0) sha256=be97f5e96258a738c4d95abbcb2909ed570dbb151d42a5a253c3d1de9c171e35
openproject-primer_view_components (0.89.0) sha256=de73dac8c53f39efbc0aa639eff0de2d84cb90ad6778be4a71eff72be2830d17
openproject-recaptcha (1.0.0)
openproject-reporting (1.0.0)
openproject-resource_management (1.0.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
++#
%>

<%= render(Primer::OpenProject::SubHeader.new(data: sub_header_data_attributes)) do |subheader| %>
<%= render(Primer::OpenProject::SubHeader.new(collapsed_search: false, data: sub_header_data_attributes)) do |subheader| %>
<% subheader.with_filter_input(
name: "name",
label: t("admin.reserved_identifiers.filter_label"),
Expand Down
5 changes: 3 additions & 2 deletions app/components/filter/filter_button_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<%= component_wrapper tag: "turbo-frame" do %>
<%= render(
Primer::Beta::Button.new(
scheme: :secondary,
scheme: :invisible,
disabled:,
data: { "filter--filters-form-target": "filterFormToggle",
action: "filter--filters-form#toggleDisplayFilters" },
test_selector: "filter-component-toggle"
)
) do |button| %>
<% button.with_trailing_visual_counter(count: filters_count, test_selector: "filters-button-counter") %>
<%= t(:label_filter) %>
<% button.with_leading_visual_icon(icon: :filter) %>
<%= t(:button_all_filters) %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= render(
Primer::OpenProject::FilterableTreeView.new(
src: tree_src,
show_search_highlighting: false,
include_sub_items_check_box_arguments: { hidden: true },
filter_mode_control_arguments: logged? ? {} : { hidden: true },
filter_input_arguments: { autofocus: true,
Expand Down
3 changes: 2 additions & 1 deletion app/components/header/projects/node_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ See COPYRIGHT and LICENSE files for more details.
node: child_node,
current_project_id: @current_project_id,
favorited_ids: @favorited_ids,
jump: @jump
jump: @jump,
query_terms: @query_terms
) %>
<% end %>
<% end %>
Expand Down
5 changes: 3 additions & 2 deletions app/components/header/projects/node_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@
module Header
module Projects
class NodeComponent < ApplicationComponent
def initialize(component:, node:, current_project_id:, favorited_ids:, jump:)
def initialize(component:, node:, current_project_id:, favorited_ids:, jump:, query_terms: [])
super()
@component = component
@node = node
@current_project_id = current_project_id
@favorited_ids = favorited_ids
@jump = jump
@query_terms = query_terms
end

private
Expand All @@ -54,7 +55,7 @@ def href
end

def label
helpers.project_node_label(project, favorited: favorited?)
helpers.project_node_label(project, favorited: favorited?, query_terms: @query_terms)
end
end
end
Expand Down
45 changes: 23 additions & 22 deletions app/components/members/index_sub_header_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
<%= render(Primer::OpenProject::SubHeader.new) do |subheader|
subheader.with_filter_button(
id: "filter-member-button",
class: "toggle-member-filter-link",
data: filter_button_data_attributes
) do
I18n.t(:description_filter)
end
subheader.with_filter_button(
id: "filter-member-button",
class: "toggle-member-filter-link",
data: filter_button_data_attributes,
scheme: :secondary # Remove once there is a search in the SubHeader
) do
I18n.t(:button_all_filters)
end

if authorize_for(:members, :new)
subheader.with_action_button(
scheme: :primary,
leading_icon: :plus,
label: t(:button_add_member),
id: "add-member-button",
data: add_button_data_attributes
) do
t("activerecord.models.member")
end
end
if authorize_for(:members, :new)
subheader.with_action_button(
scheme: :primary,
leading_icon: :plus,
label: t(:button_add_member),
id: "add-member-button",
data: add_button_data_attributes
) do
t("activerecord.models.member")
end
end

subheader.with_bottom_pane_component do
render ::Members::UserFilterComponent.new(params, **@members_filter_options)
end
end %>
subheader.with_bottom_pane_component do
render ::Members::UserFilterComponent.new(params, **@members_filter_options)
end
end %>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<%=
flex_layout(data: wrapper_data_attributes) do |flex|
flex.with_row do
render(Primer::OpenProject::SubHeader.new) do |subheader|
render(Primer::OpenProject::SubHeader.new(collapsed_search: false)) do |subheader|
subheader.with_filter_input(
name: "border-box-filter",
label: t("projects.settings.life_cycle.filter.label"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
component_wrapper(data: wrapper_data_attributes) do
flex_layout do |flex|
flex.with_row do
render(Primer::OpenProject::SubHeader.new) do |subheader|
render(Primer::OpenProject::SubHeader.new(collapsed_search: false)) do |subheader|
subheader.with_filter_input(
name: "border-box-filter",
label: t("projects.settings.project_custom_fields.filter.label"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ See COPYRIGHT and LICENSE files for more details.

if allowed_to_customize_life_cycle?
flex.with_row do
render(Primer::OpenProject::SubHeader.new) do |subheader|
render(Primer::OpenProject::SubHeader.new(collapsed_search: false)) do |subheader|
subheader.with_filter_input(
name: "border-box-filter",
label: t("settings.project_phase_definitions.filter.label"),
Expand Down
2 changes: 1 addition & 1 deletion app/components/users/index_sub_header_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%=
render(Primer::OpenProject::SubHeader.new(collapsed_search: true, data: sub_header_data_attributes)) do |subheader|
render(Primer::OpenProject::SubHeader.new(data: sub_header_data_attributes)) do |subheader|
subheader.with_action_button(
scheme: :primary,
leading_icon: :plus,
Expand Down
1 change: 1 addition & 0 deletions app/controllers/header/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class Header::ProjectsController < ApplicationController
def index
@current_project_id = params[:current_project_id].presence&.to_i
@jump = params[:jump].presence
@query_terms = query.split
@projects = load_projects
@favorited_ids = load_favorited_ids
@tree = build_tree(@projects)
Expand Down
54 changes: 52 additions & 2 deletions app/helpers/header/projects_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@

module Header
module ProjectsHelper
def project_node_label(project, favorited: false)
parts = [project.name]
def project_node_label(project, favorited: false, query_terms: [])
name_html = query_terms.any? ? content_tag(:span, highlight_name(project.name, query_terms)) : project.name
parts = [name_html]
parts << favorite_icon if favorited
parts << workspace_type_badge(project) if show_workspace_type_badge?(project)

Expand All @@ -41,6 +42,55 @@ def project_node_label(project, favorited: false)

private

def highlight_name(name, query_terms)
ranges = find_highlight_ranges(name, query_terms)
return h(name) if ranges.empty?

build_highlighted_segments(name, merge_highlight_ranges(ranges))
end

def find_highlight_ranges(name, query_terms)
query_terms.flat_map { |term| occurrences_of(term, in_string: name) }
end

# Returns all character ranges where +term+ appears case-insensitively in +in_string+.
def occurrences_of(term, in_string:)
regex = Regexp.new(Regexp.escape(term), Regexp::IGNORECASE)
ranges = []
start = 0
while (match = regex.match(in_string, start))
ranges << (match.begin(0)...match.end(0))
start = match.begin(0) + 1
end
ranges
end

# Splits +name+ into plain-text and highlighted segments according to +ranges+,
# then joins them into a single HTML-safe string.
def build_highlighted_segments(name, ranges)
pos = 0
segments = ranges.flat_map do |range|
before = pos < range.begin ? h(name[pos...range.begin]) : nil
highlighted = content_tag(:span, name[range], class: "op-search-highlight")
pos = range.end
[before, highlighted].compact
end
segments << h(name[pos..]) if pos < name.length
safe_join(segments)
end

# Merges overlapping or adjacent ranges into a minimal set of non-overlapping ranges.
def merge_highlight_ranges(ranges)
ranges.sort_by(&:begin).each_with_object([]) do |range, merged|
if merged.empty? || range.begin >= merged.last.end
merged << range
else
last = merged.last
merged[-1] = (last.begin...[last.end, range.end].max)
end
end
end

def favorite_icon
render(Primer::Beta::Octicon.new(icon: :"star-fill", size: :small, classes: "op-primer--star-icon", ml: 2))
end
Expand Down
3 changes: 2 additions & 1 deletion app/views/header/projects/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ See COPYRIGHT and LICENSE files for more details.
node:,
current_project_id: @current_project_id,
favorited_ids: @favorited_ids,
jump: @jump
jump: @jump,
query_terms: @query_terms
) %>
<% end %>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/workflows/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details.
<%=
flex_layout data: { controller: "filter--filter-list", "filter--filter-list-clear-button-id-value": "type-filter-clear-button" } do |flex|
flex.with_row do
render(Primer::OpenProject::SubHeader.new) do |subheader|
render(Primer::OpenProject::SubHeader.new(collapsed_search: false)) do |subheader|
subheader.with_filter_input(
name: "type-filter-input",
label: t(".type_filter.label"),
Expand Down
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2902,6 +2902,7 @@
button_add_sub_item: "Add sub-item"
button_add_member: Add member
button_add_watcher: "Add watcher"
button_all_filters: "All filters"

Check failure on line 2905 in config/locales/en.yml

View workflow job for this annotation

GitHub Actions / Yamllint

[yamllint] reported by reviewdog 🐶 [error] wrong ordering of key "button_all_filters" in mapping (key-ordering) Raw Output: config/locales/en.yml:2905:3: [error] wrong ordering of key "button_all_filters" in mapping (key-ordering)
button_annotate: "Annotate"
button_apply: "Apply"
button_apply_changes: "Apply changes"
Expand Down
16 changes: 8 additions & 8 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@
"@ng-select/ng-select": "^23.0.0",
"@ngneat/content-loader": "^7.0.0",
"@openproject/octicons-angular": "^19.35.0",
"@openproject/primer-view-components": "^0.88.0",
"@openproject/primer-view-components": "^0.89.0",
"@openproject/reactivestates": "^3.0.1",
"@primer/css": "^22.1.0",
"@primer/live-region-element": "^0.8.0",
"@primer/primitives": "^11.5.1",
"@primer/view-components": "npm:@openproject/primer-view-components@^0.88.0",
"@primer/view-components": "npm:@openproject/primer-view-components@^0.89.0",
"@rails/request.js": "^0.0.13",
"@stimulus-components/auto-submit": "^6.0.0",
"@stimulus-components/reveal": "^5.0.0",
Expand Down
13 changes: 11 additions & 2 deletions lookbook/docs/components/sub-header.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The SubHeader accepts a number of additional components. **All buttons** within

On the left side:

- **Search filter**: (Optional) A basic [text input](https://primer.style/components/text-input) with a [search icon](https://primer.style/components/text-input#with-leading-and-trailing-visuals) which is mostly used on index pages to filter the elements below.
- **Search filter**: (Optional) A basic [text input](https://primer.style/components/text-input) with a [search icon](https://primer.style/components/text-input#with-leading-and-trailing-visuals) which is mostly used on index pages to filter the elements below. Per default, the search input is collapsed to an icon button and changes on click to a text input. To avoid this, use `collapsed_search: false`.
- **Sort action** (Optional) A generic slot for a sort button component. It is recommended to use IconButtons here.
- **Group action** (Optional) A generic slot for a group button component. It is recommended to use IconButtons here.
- **Quick filters** (Optional) Up to 5 QuickFilter can be rendered here. When using more then one quick filter, an `All Filter` button is required.
Expand Down Expand Up @@ -45,6 +45,7 @@ The SubHeader automatically adapts to mobile screens:
- Use IconButtons instead of Buttons for actions only when the icon is clear enough to describe the action.
- When more actions are necessary, or if a menu is needed, use an ActionMenu instead of the ActionButtons.
- Use IconButtons for the sort and group components.
- Use `collapsed_search: false` when the left side only shows the search input and no further filter actions.

**Don't**

Expand Down Expand Up @@ -161,9 +162,17 @@ The `with_quick_filter` slot only offers a generic slot for a quick filter takin


# The all filters button is required when using more than one quick filter
component.with_filter_button(mobile_label: "All filters") do
component.with_filter_component(mobile_label: "All filters") do
render(Foo::AllFilterComponent.new(query: @query, project: @project))
end
end
%&gt;
```

### All filters button

The "All filters" button is required when using more than one quick filter. It can be rendered either via the `with_filter_button` slot or as a component via `with_filter_component`. When using the latter, you have to manually make sure that the button fulfills the following requirements:

* Use `invisible` scheme
* Use `icon: :filter`
* Use `label: "All filters"`
4 changes: 2 additions & 2 deletions lookbook/previews/open_project/common/sub_header_preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def default
component.with_filter_input(name: "filter", label: "Filter")
component.with_filter_button do |button|
button.with_trailing_visual_counter(count: "15")
"Filter"
"All Filters"
end
component.with_action_button(
leading_icon: :plus,
Expand All @@ -60,7 +60,7 @@ def playground(show_filter_input: true, show_filter_button: true, show_action_bu
if show_filter_button
component.with_filter_button do |button|
button.with_trailing_visual_counter(count: "15")
"Filter"
"All filters"
end
end

Expand Down
Loading
Loading