Release attribute highlighting to community

This commit is contained in:
Oliver Günther
2026-01-30 09:56:15 +01:00
parent 6207fccc57
commit 896072d698
19 changed files with 144 additions and 252 deletions
-4
View File
@@ -69,8 +69,6 @@ module Query::Highlighting
end
def highlighted_attributes
return [] unless EnterpriseToken.allows_to?(:conditional_highlighting)
val = super
if val.present?
@@ -81,8 +79,6 @@ module Query::Highlighting
end
def highlighting_mode
return :none unless EnterpriseToken.allows_to?(:conditional_highlighting)
val = super
if val.present?
@@ -1,4 +1,4 @@
<%#-- copyright
<%# -- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
@@ -25,7 +25,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
++# %>
<% html_title t(:label_administration), t(:label_work_package_plural), t(:label_general) -%>
@@ -66,19 +66,12 @@ See COPYRIGHT and LICENSE files for more details.
end,
Hash.new.tap do |options|
options[:container_class] = "-middle"
options[:disabled] = "disabled" unless EnterpriseToken.allows_to?(:conditional_highlighting)
options[:data] = {
show_when_value_selected_target: "cause",
target_name: "work_package_list_default_highlighting_mode"
}
end
) %>
<div class="form--field-extra-actions">
<%=
render(EnterpriseEdition::BannerComponent.new(:conditional_highlighting, mt: 2))
%>
</div>
<% if EnterpriseToken.allows_to? :conditional_highlighting %>
<div
class="form--field -indented -vertical"
data-show-when-value-selected-target="effect"
@@ -92,7 +85,6 @@ See COPYRIGHT and LICENSE files for more details.
end
) %>
</div>
<% end %>
</div>
</section>
+2 -3
View File
@@ -1261,9 +1261,8 @@ module Settings
},
work_package_list_default_highlighting_mode: {
format: :string,
default: -> { EnterpriseToken.allows_to?(:conditional_highlighting) ? "inline" : "none" },
allowed: -> { Query::QUERY_HIGHLIGHTING_MODES.map(&:to_s) },
writable: -> { EnterpriseToken.allows_to?(:conditional_highlighting) }
default: -> { "inline" },
allowed: -> { Query::QUERY_HIGHLIGHTING_MODES.map(&:to_s) }
},
work_package_list_default_columns: {
default: %w[id subject type status assigned_to priority],
-3
View File
@@ -2557,7 +2557,6 @@ en:
board_view: Advanced Boards
calculated_values: Calculated values
capture_external_links: Capture External Links
conditional_highlighting: Conditional Highlighting
internal_comments: Internal Comments
custom_actions: Custom Actions
custom_field_hierarchies: Hierarchies
@@ -2612,8 +2611,6 @@ en:
description: "Create automatically generated subjects using referenced attributes and text."
customize_life_cycle:
description: "Create and organize different project phases than the ones provided by PM2 project cycle planning."
conditional_highlighting:
description: "Need certain work packages to stand out from the mass? Use conditional highlighting in the work packages table."
capture_external_links:
description: "Prevent social engineering attacks by capturing and warning about external links before users visit them."
work_package_query_relation_columns:
-4
View File
@@ -42,14 +42,10 @@ docker_installation:
documents_docs:
href: https://www.openproject.org/docs/user-guide/documents/
enterprise_features:
attribute_highlighting:
href: https://www.openproject.org/docs/user-guide/work-packages/work-package-table-configuration/#attribute-highlighting-enterprise-add-on
board_view:
href: https://www.openproject.org/docs/user-guide/agile-boards/#action-boards-enterprise-add-on
boards:
href: https://www.openproject.org/docs/user-guide/agile-boards/#action-boards-enterprise-add-on
conditional_highlighting:
href: https://www.openproject.org/enterprise-edition/?op_referrer=settings-wp-attribute-highlighting#attribute-highlighting
custom_field_hierarchies:
href: https://www.openproject.org/docs/system-admin-guide/custom-fields/#hierarchy-custom-field-enterprise-add-on
custom_field_multiselect:
@@ -21,9 +21,7 @@ You can adjust the following:
4. **Default highlighting mode** defines which should be the default [attribute highlighting](../../../user-guide/work-packages/work-package-table-configuration/#attribute-highlighting-enterprise-add-on) mode, e.g. to highlight the following criteria in the work package table.
[feature: conditional_highlighting ]
![default highlighting mode](openproject_system_guide_default_highlighting_mode.png)
![default highlighting mode](openproject_system_guide_default_highlighting_mode.png)
5. Customize the appearance of the work package lists to **define which work package attributes are displayed in the work package lists by default and in what order**.
@@ -235,8 +235,6 @@ If you group the work package table, sums will be shown for each group.
## Attribute highlighting (Enterprise add-on)
[feature: conditional_highlighting ]
You can highlight attributes in the work package table to emphasize the importance of certain attributes and have important topics at a glance. To activate attribute highlighting open the work package configuration table and select the **Highlighting** tab.
The following attributes can be highlighted in the table:
@@ -1,14 +1,9 @@
<div>
<op-enterprise-banner-frame feature="conditional_highlighting"
[dismissable]="false"
/>
<form>
<p [textContent]="text.highlighting_mode.description"></p>
<div class="form--field">
<label class="form--label">
<input type="radio"
[attr.disabled]="disabledValue(eeAvailable)"
[(ngModel)]="highlightingMode"
(change)="updateMode($event.target.value)"
value="inline"
@@ -39,7 +34,6 @@
<div class="form--field">
<label class="form--label">
<input type="radio"
[attr.disabled]="disabledValue(eeAvailable)"
[(ngModel)]="entireRowMode"
(change)="updateMode('entire-row')"
[value]="true"
@@ -52,7 +46,6 @@
<ng-select #rowHighlightNgSelect
[items]="availableRowHighlightedAttributes"
[(ngModel)]="lastEntireRowAttribute"
[disabled]="disabledValue(eeAvailable)"
[clearable]="false"
(open)="onOpen(rowHighlightNgSelect)"
(change)="updateMode($event.value)"
@@ -69,7 +62,6 @@
<div class="form--field">
<label class="form--label">
<input type="radio"
[attr.disabled]="disabledValue(eeAvailable)"
[(ngModel)]="highlightingMode"
(change)="updateMode($event.target.value)"
value="none"
@@ -29,8 +29,6 @@ export class WpTableConfigurationHighlightingTabComponent implements TabComponen
public lastEntireRowAttribute:HighlightingMode = 'status';
public eeAvailable = false;
public availableInlineHighlightedAttributes:HalResource[] = [];
public selectedAttributes:any[] = [];
@@ -53,8 +51,6 @@ export class WpTableConfigurationHighlightingTabComponent implements TabComponen
priority: this.I18n.t('js.work_packages.table_configuration.highlighting_mode.priority'),
entire_row_by: this.I18n.t('js.work_packages.table_configuration.highlighting_mode.entire_row_by'),
},
upsellAttributeHighlighting: this.I18n.t('js.work_packages.table_configuration.upsell.attribute_highlighting'),
upsellCheckOutLink: this.I18n.t('js.work_packages.table_configuration.upsell.check_out_link'),
more_info_link: enterpriseDocsUrl.tableHighlighting,
};
@@ -74,13 +70,7 @@ export class WpTableConfigurationHighlightingTabComponent implements TabComponen
];
this.setSelectedValues();
this.eeAvailable = this.Banners.allowsTo('conditional_highlighting');
this.updateMode(this.wpTableHighlight.current.mode);
if (!this.eeAvailable) {
this.updateMode('none');
}
}
public onSave() {
@@ -107,10 +97,6 @@ export class WpTableConfigurationHighlightingTabComponent implements TabComponen
this.selectedAttributes = model;
}
public disabledValue(allowed:boolean):string | null {
return allowed ? null : 'disabled';
}
public get availableHighlightedAttributes():HalResource[] {
const { schema } = this.querySpace.queryForm.value!;
return schema.highlightedAttributes.allowedValues;
@@ -32,7 +32,7 @@ export class WorkPackageViewHighlightingService extends WorkPackageQueryStateSer
*/
public shouldHighlightInline(name:string):boolean {
// 1. Are we in inline mode or unable to render?
if (!this.isInline || !this.Banners.allowsTo('conditional_highlighting')) {
if (!this.isInline) {
return false;
}
@@ -86,14 +86,6 @@ export class WorkPackageViewHighlightingService extends WorkPackageQueryStateSer
value.selectedAttributes = undefined;
}
void this.Banners.conditional(
'conditional_highlighting',
() => {
value.mode = 'none';
value.selectedAttributes = undefined;
},
);
return value;
}
}
@@ -32,7 +32,7 @@ require_relative "../../support/pages/ifc_models/show_default"
RSpec.describe "Switching work package view",
:js,
:selenium,
with_config: { edition: "bim" }, with_ee: %i[conditional_highlighting] do
with_config: { edition: "bim" } do
let(:user) { create(:admin) }
let(:project) { create(:project, enabled_module_names: %i[bim work_package_tracking]) }
let(:wp_page) { Pages::IfcModels::ShowDefault.new(project) }
+1
View File
@@ -424,6 +424,7 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -2,10 +2,7 @@
require "spec_helper"
RSpec.describe "Work Package highlighting fields",
:js,
:selenium,
with_ee: %i[conditional_highlighting] do
RSpec.describe "Work Package highlighting fields", :js, :selenium do
let(:user) { create(:admin) }
let(:project) { create(:project) }
@@ -88,7 +88,7 @@ RSpec.describe API::V3::Queries::QueryRepresenter, "parsing" do
end
end
describe "highlighted_attributes", with_ee: %i[conditional_highlighting] do
describe "highlighted_attributes" do
let(:request_body) do
{
"_links" => {
@@ -51,18 +51,18 @@ RSpec.describe API::V3::Queries::QueryRepresenter do
allow(QueryPolicy)
.to receive(:new)
.with(current_user)
.and_return(policy_stub)
.with(current_user)
.and_return(policy_stub)
allow(policy_stub)
.to receive(:allowed?)
.and_return(false)
.and_return(false)
permissions.each do |permission|
allow(policy_stub)
.to receive(:allowed?)
.with(query, permission)
.and_return(true)
.with(query, permission)
.and_return(true)
end
end
@@ -395,7 +395,7 @@ RSpec.describe API::V3::Queries::QueryRepresenter do
it "has an empty columns array" do
expect(subject)
.to be_json_eql([].to_json)
.at_path("_links/columns")
.at_path("_links/columns")
end
end
@@ -426,7 +426,7 @@ RSpec.describe API::V3::Queries::QueryRepresenter do
expect(subject)
.to be_json_eql(expected.to_json)
.at_path("_links/columns")
.at_path("_links/columns")
end
end
@@ -458,7 +458,7 @@ RSpec.describe API::V3::Queries::QueryRepresenter do
it "has an empty sortBy array" do
expect(subject)
.to be_json_eql([].to_json)
.at_path("_links/sortBy")
.at_path("_links/sortBy")
end
end
@@ -482,7 +482,7 @@ RSpec.describe API::V3::Queries::QueryRepresenter do
expect(subject)
.to be_json_eql(expected.to_json)
.at_path("_links/sortBy")
.at_path("_links/sortBy")
end
end
@@ -492,7 +492,7 @@ RSpec.describe API::V3::Queries::QueryRepresenter do
before do
allow(query)
.to receive(:starred)
.and_return(false)
.and_return(false)
end
it_behaves_like "has an untitled link" do
@@ -521,7 +521,7 @@ RSpec.describe API::V3::Queries::QueryRepresenter do
before do
allow(query)
.to receive(:starred)
.and_return(true)
.and_return(true)
end
it_behaves_like "has an untitled link" do
@@ -638,85 +638,69 @@ RSpec.describe API::V3::Queries::QueryRepresenter do
end
describe "highlighting" do
context "with EE", with_ee: %i[conditional_highlighting] do
let :status do
{
href: "/api/v3/queries/columns/status",
title: "Status"
}
end
let :type do
{
href: "/api/v3/queries/columns/type",
title: "Type"
}
end
let :priority do
{
href: "/api/v3/queries/columns/priority",
title: "Priority"
}
end
let :due_date do
{
href: "/api/v3/queries/columns/dueDate",
title: "Finish date"
}
end
let(:query) do
query = build_stubbed(:query, project: workspace)
query.highlighted_attributes = %w[status type priority due_date]
query
end
let(:highlighted_attributes) do
[status, priority, due_date]
end
it "renders when the value is set" do
query.highlighting_mode = "status"
expect(subject).to be_json_eql("status".to_json).at_path("highlightingMode")
end
it "renders the default" do
query.highlighting_mode = nil
query.highlighted_attributes = nil
expect(subject).to be_json_eql("inline".to_json).at_path("highlightingMode")
expect(subject).not_to have_json_path("highlightedAttributes")
end
it "links an array of highlighted attributes" do
expect(subject)
.to be_json_eql(highlighted_attributes.to_json).at_path("_links/highlightedAttributes")
end
it "embeds selected inline attributes" do
query.highlighted_attributes[0..0].each_with_index do |attr, index|
expect(subject)
.to be_json_eql("/api/v3/queries/columns/#{attr}".to_json)
.at_path("_embedded/highlightedAttributes/#{index}/_links/self/href")
end
end
let :status do
{
href: "/api/v3/queries/columns/status",
title: "Status"
}
end
context "without EE" do
it "renders when the value is set" do
query.highlighting_mode = "status"
let :type do
{
href: "/api/v3/queries/columns/type",
title: "Type"
}
end
expect(subject).to be_json_eql("none".to_json).at_path("highlightingMode")
end
let :priority do
{
href: "/api/v3/queries/columns/priority",
title: "Priority"
}
end
it "renders none when not set" do
query.highlighting_mode = nil
let :due_date do
{
href: "/api/v3/queries/columns/dueDate",
title: "Finish date"
}
end
expect(subject).to be_json_eql("none".to_json).at_path("highlightingMode")
let(:query) do
query = build_stubbed(:query, project: workspace)
query.highlighted_attributes = %w[status type priority due_date]
query
end
let(:highlighted_attributes) do
[status, priority, due_date]
end
it "renders when the value is set" do
query.highlighting_mode = "status"
expect(subject).to be_json_eql("status".to_json).at_path("highlightingMode")
end
it "renders the default" do
query.highlighting_mode = nil
query.highlighted_attributes = nil
expect(subject).to be_json_eql("inline".to_json).at_path("highlightingMode")
expect(subject).not_to have_json_path("highlightedAttributes")
end
it "links an array of highlighted attributes" do
expect(subject)
.to be_json_eql(highlighted_attributes.to_json).at_path("_links/highlightedAttributes")
end
it "embeds selected inline attributes" do
query.highlighted_attributes[0..0].each_with_index do |attr, index|
expect(subject)
.to be_json_eql("/api/v3/queries/columns/#{attr}".to_json)
.at_path("_embedded/highlightedAttributes/#{index}/_links/self/href")
end
end
end
@@ -741,11 +725,11 @@ RSpec.describe API::V3::Queries::QueryRepresenter do
query.add_filter("status_id", "=", [filter_status.id.to_s])
allow(query.filters.last)
.to receive(:value_objects)
.and_return([filter_status])
.and_return([filter_status])
query.add_filter("assigned_to_id", "!", [filter_user.id.to_s])
allow(query.filters.last)
.to receive(:value_objects)
.and_return([filter_user])
.and_return([filter_user])
query
end
@@ -895,7 +879,7 @@ RSpec.describe API::V3::Queries::QueryRepresenter do
it "embeds the results" do
expect(subject)
.to be_json_eql("BogusResultType".to_json)
.at_path("_embedded/results/_type")
.at_path("_embedded/results/_type")
end
end
+1 -1
View File
@@ -34,7 +34,7 @@ RSpec.describe "default query" do
let(:query) { Query.new_default }
describe "highlighting mode" do
context "with highlighting mode setting", with_ee: %i[conditional_highlighting] do
context "with highlighting mode setting" do
describe "not set" do
it "is inline" do
expect(query.highlighting_mode).to eq :inline
+52 -67
View File
@@ -31,7 +31,7 @@
require "spec_helper"
RSpec.describe Query,
with_ee: %i[baseline_comparison conditional_highlighting work_package_query_relation_columns] do
with_ee: %i[baseline_comparison work_package_query_relation_columns] do
let(:query) { build(:query) }
let(:project) { create(:project) }
let(:project_member) { create(:user, member_with_permissions: { project => [:view_project] }) }
@@ -231,73 +231,58 @@ RSpec.describe Query,
end
describe "highlighting" do
context "with EE" do
it "#highlighted_attributes accepts valid values" do
query.highlighted_attributes = %w(status priority due_date)
expect(query).to be_valid
end
it "#highlighted_attributes rejects invalid values" do
query.highlighted_attributes = %w(status bogus)
expect(query).not_to be_valid
end
it "#hightlighting_mode accepts non-present values" do
query.highlighting_mode = nil
expect(query).to be_valid
query.highlighting_mode = ""
expect(query).to be_valid
end
it "#hightlighting_mode rejects invalid values" do
query.highlighting_mode = "bogus"
expect(query).not_to be_valid
end
it "#available_highlighting_columns returns highlightable columns" do
available_columns = {
highlightable1: {
highlightable: true
},
highlightable2: {
highlightable: true
},
no_highlight: {}
}
allow(Queries::WorkPackages::Selects::PropertySelect).to receive(:property_selects)
.and_return(available_columns)
expect(query.available_highlighting_columns.map(&:name)).to eq(%i{highlightable1 highlightable2})
end
describe "#highlighted_columns returns a valid subset of Columns" do
let(:highlighted_attributes) { %i{status priority due_date foo} }
before do
query.highlighted_attributes = highlighted_attributes
end
it "removes the offending values" do
query.valid_subset!
expect(query.highlighted_columns.map(&:name))
.to match_array %i{status priority due_date}
end
end
it "#highlighted_attributes accepts valid values" do
query.highlighted_attributes = %w(status priority due_date)
expect(query).to be_valid
end
context "without EE", with_ee: false do
it "always returns :none as highlighting_mode" do
query.highlighting_mode = "status"
expect(query.highlighting_mode).to eq(:none)
it "#highlighted_attributes rejects invalid values" do
query.highlighted_attributes = %w(status bogus)
expect(query).not_to be_valid
end
it "#hightlighting_mode accepts non-present values" do
query.highlighting_mode = nil
expect(query).to be_valid
query.highlighting_mode = ""
expect(query).to be_valid
end
it "#hightlighting_mode rejects invalid values" do
query.highlighting_mode = "bogus"
expect(query).not_to be_valid
end
it "#available_highlighting_columns returns highlightable columns" do
available_columns = {
highlightable1: {
highlightable: true
},
highlightable2: {
highlightable: true
},
no_highlight: {}
}
allow(Queries::WorkPackages::Selects::PropertySelect).to receive(:property_selects)
.and_return(available_columns)
expect(query.available_highlighting_columns.map(&:name)).to eq(%i{highlightable1 highlightable2})
end
describe "#highlighted_columns returns a valid subset of Columns" do
let(:highlighted_attributes) { %i{status priority due_date foo} }
before do
query.highlighted_attributes = highlighted_attributes
end
it "always returns nil as highlighted_attributes" do
query.highlighting_mode = "inline"
query.highlighted_attributes = ["status"]
expect(query.highlighted_attributes).to be_empty
it "removes the offending values" do
query.valid_subset!
expect(query.highlighted_columns.map(&:name))
.to match_array %i{status priority due_date}
end
end
end
@@ -370,11 +355,11 @@ RSpec.describe Query,
# its own expectation again. Hence, we must set up a double.
allow(WorkPackageCustomField)
.to receive(:all)
.and_return empty_wp_relation
.and_return empty_wp_relation
allow(Type)
.to receive(:all)
.and_return []
.and_return []
query.displayable_columns
@@ -777,7 +762,7 @@ RSpec.describe Query,
context "without EE", with_ee: false do
it "removes the forbidden values" do
expect(query.timestamps)
.to match_array %w{oneDayAgo@12:00+00:00 PT0S}
.to match_array %w{oneDayAgo@12:00+00:00 PT0S}
end
end
+2 -10
View File
@@ -316,16 +316,8 @@ RSpec.describe Setting do
# Check that when reading certain setting values that they get overwritten if needed.
describe "filter saved settings" do
describe "with EE token", with_ee: %i[conditional_highlighting] do
it "returns the value for 'work_package_list_default_highlighting_mode' without changing it" do
expect(described_class.work_package_list_default_highlighting_mode).to eq("inline")
end
end
describe "without EE" do
it "return 'none' as 'work_package_list_default_highlighting_mode'" do
expect(described_class.work_package_list_default_highlighting_mode).to eq("none")
end
it "returns the value for 'work_package_list_default_highlighting_mode' without changing it" do
expect(described_class.work_package_list_default_highlighting_mode).to eq("inline")
end
end
@@ -42,7 +42,7 @@ RSpec.describe UpdateQueryFromParamsService,
describe "#call" do
subject { instance.call(params) }
context "group_by" do
describe "group_by" do
context "for an existing value" do
let(:params) { { group_by: "status" } }
@@ -66,7 +66,7 @@ RSpec.describe UpdateQueryFromParamsService,
end
end
context "filters" do
describe "filters" do
let(:params) do
{ filters: [{ field: "status_id", operator: "=", values: ["1", "2"] }] }
end
@@ -87,7 +87,7 @@ RSpec.describe UpdateQueryFromParamsService,
end
end
context "sort_by" do
describe "sort_by" do
let(:params) do
{ sort_by: [["status_id", "desc"]] }
end
@@ -100,7 +100,7 @@ RSpec.describe UpdateQueryFromParamsService,
end
end
context "columns" do
describe "columns" do
let(:params) do
{ columns: ["assigned_to", "author", "category", "subject"] }
end
@@ -113,7 +113,7 @@ RSpec.describe UpdateQueryFromParamsService,
end
end
context "display representation" do
describe "display representation" do
let(:params) do
{ display_representation: "list" }
end
@@ -126,7 +126,7 @@ RSpec.describe UpdateQueryFromParamsService,
end
end
context "highlighting mode", with_ee: %i[conditional_highlighting] do
describe "highlighting mode" do
let(:params) do
{ highlighting_mode: "status" }
end
@@ -139,7 +139,7 @@ RSpec.describe UpdateQueryFromParamsService,
end
end
context "default highlighting mode", with_ee: %i[conditional_highlighting] do
describe "default highlighting mode" do
let(:params) do
{}
end
@@ -152,19 +152,6 @@ RSpec.describe UpdateQueryFromParamsService,
end
end
context "highlighting mode without EE" do
let(:params) do
{ highlighting_mode: "status" }
end
it "sets the highlighting_mode" do
subject
expect(query.highlighting_mode)
.to eq(:none)
end
end
context "when using include subprojects" do
let(:params) do
{ include_subprojects: }