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 app/models/work_package/journalized.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,44 +45,44 @@
}

# This one is here only to ease reading
module JournalizedProcs
def self.event_title
Proc.new do |o|
title = o.to_s
title += " (#{o.status.name})" if o.status.present?

title
end
end

def self.event_name
Proc.new do |o|
I18n.t(o.event_type.underscore, scope: "events")
end
end

def self.event_type
Proc.new do |o|
journal = o.last_journal
t = "work_package"

t += if journal && journal.details.empty? && !journal.initial?
"-note"
else
status = Status.find_by(id: o.status_id)

status.try(:is_closed?) ? "-closed" : "-edit"
end
t
end
end

def self.event_url
Proc.new do |o|
{ controller: :work_packages, action: :show, id: o.id }
{ controller: :work_packages, action: :show, id: o.display_id }
end
end
end

Check warning on line 85 in app/models/work_package/journalized.rb

View workflow job for this annotation

GitHub Actions / rubocop

[rubocop] app/models/work_package/journalized.rb#L48-L85 <Lint/ConstantDefinitionInBlock>

Do not define constants this way within a block.
Raw output
app/models/work_package/journalized.rb:48:5: W: Lint/ConstantDefinitionInBlock: Do not define constants this way within a block.

acts_as_event title: JournalizedProcs.event_title,
type: JournalizedProcs.event_type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@
} @else {
<a
class="global-search--option"
[href]="wpPath(item.id)"
(click)="redirectToWp(item.id, $event)"
[href]="wpPath(item.displayId)"
(click)="redirectToWp(item.displayId, $event)"
style="line-height: 1"
>
<div class="global-search--option-meta">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//-- 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.
//++

import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { GlobalSearchInputComponent } from './global-search-input.component';

// followItem is verified through the prototype against a stand-in context, avoiding
// a real component instance whose many injected dependencies this branch never uses.
describe('GlobalSearchInputComponent#followItem', () => {
let wpPathArgs:string[];
let searchInScopeArgs:string[];
let context:Pick<GlobalSearchInputComponent, 'wpPath'|'selectedItem'> & { searchInScope:(scope:string) => void };

function callFollowItem(item:Parameters<GlobalSearchInputComponent['followItem']>[0]):void {
GlobalSearchInputComponent.prototype.followItem.call(context, item);
}

beforeEach(() => {
wpPathArgs = [];
searchInScopeArgs = [];
context = {
wpPath: (id:string):string => {
wpPathArgs.push(id);
// A fragment keeps followItem's window.location assignment from navigating the runner.
return '#stub';
},
selectedItem: undefined,
searchInScope: (scope:string):void => {
searchInScopeArgs.push(scope);
},
};
});

describe('when item is a work package resource', () => {
// Build a real WorkPackageResource off its prototype and feed it a HAL $source,
// so followItem exercises the production displayId getter rather than a stub.
function buildWorkPackage(source:{ id:number, displayId?:string }):WorkPackageResource {
const item = Object.create(WorkPackageResource.prototype) as WorkPackageResource;
item.$source = source;
return item;
}

it('is recognised as a HalResource', () => {
expect(buildWorkPackage({ id: 42 }) instanceof HalResource).toBe(true);
});

describe('in semantic mode (source carries a semantic displayId)', () => {
let item:WorkPackageResource;

beforeEach(() => {
item = buildWorkPackage({ id: 42, displayId: 'PROJ-42' });
});

it('navigates via the semantic displayId, not the numeric id', () => {
callFollowItem(item);
expect(wpPathArgs).toEqual(['PROJ-42']);
expect(wpPathArgs).not.toContain('42');
});

it('sets selectedItem to the item', () => {
callFollowItem(item);
expect(context.selectedItem).toBe(item);
});
});

describe('in classic mode (source has only the numeric id)', () => {
it('falls back to the numeric id through displayId', () => {
callFollowItem(buildWorkPackage({ id: 42 }));
expect(wpPathArgs).toEqual(['42']);
});
});
});

describe('when item is a scope option (not a HalResource)', () => {
it('delegates to searchInScope and does not call wpPath', () => {
callFollowItem({ projectScope: 'current_project', text: 'In this project ↵' });
expect(searchInScopeArgs).toEqual(['current_project']);
expect(wpPathArgs).toEqual([]);
});
});

describe('when item is undefined', () => {
it('does nothing', () => {
callFollowItem(undefined);
expect(wpPathArgs).toEqual([]);
expect(searchInScopeArgs).toEqual([]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,8 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {

public followItem(item:WorkPackageResource|SearchOptionItem|undefined):void {
this.selectedItem = item;
if (item instanceof HalResource) {
window.location.href = this.wpPath(item.id!);
if (item instanceof WorkPackageResource) {
window.location.href = this.wpPath(item.displayId);
} else if (item) {
this.searchInScope(item.projectScope);
}
Expand Down
6 changes: 3 additions & 3 deletions lib_static/plugins/acts_as_event/lib/acts_as_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
# See COPYRIGHT and LICENSE files for more details.
#++

# This now only seems to be used when rendering atom responses.
# Search as well as activities do not rely on it.
# Thus, whenever an atom link is removed for a resource, acts_as_event within that model can also be removed.
# The event_* methods defined here power the server-rendered search results page
# and atom feeds. The Activities subsystem renders through its own providers, so
# a model can drop acts_as_event only once neither search nor atom references it.

module Redmine
module Acts
Expand Down
23 changes: 23 additions & 0 deletions spec/features/search/search_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,29 @@ def expect_range(param_a, param_b)
end
end

describe "when semantic work package IDs are active",
with_settings: { work_packages_identifier: "semantic" } do
let(:run_visit) { false }
let(:semantic_project) { create(:project, :semantic) }
let(:semantic_wp) do
create(:work_package, subject: "SemanticIdentifierTest WP", project: semantic_project)
end

before do
semantic_wp
visit search_path(scope: "all", q: "SemanticIdentifierTest")
end

it "links results to the semantic identifier URL, not the numeric ID" do
identifier = semantic_wp.reload.identifier

within("dt.work_package-edit") do
expect(page).to have_link(href: %r{/work_packages/#{Regexp.escape(identifier)}(?:$|[#?])})
expect(page).to have_no_link(href: %r{/work_packages/#{semantic_wp.id}(?:$|[#?])})
end
end
end

describe "search for notes" do
let(:work_package) { work_packages[0] }
let!(:note_one) do
Expand Down
16 changes: 14 additions & 2 deletions spec/models/work_package/work_package_acts_as_event_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,21 @@
let(:stub_work_package) { build_stubbed(:work_package) }

describe "#event_url" do
let(:expected_url) { { controller: :work_packages, action: :show, id: stub_work_package.id } }
context "in classic mode" do
let(:expected_url) { { controller: :work_packages, action: :show, id: stub_work_package.id } }

it { expect(stub_work_package.event_url).to eq(expected_url) }
it { expect(stub_work_package.event_url).to eq(expected_url) }
end

context "in semantic mode", with_settings: { work_packages_identifier: "semantic" } do
let(:project) { create(:project, :semantic) }
let(:work_package) { create(:work_package, project:) }

it "links to the semantic identifier rather than the numeric id" do
expect(work_package.event_url)
.to eq(controller: :work_packages, action: :show, id: work_package.reload.identifier)
end
end
end
end
end
Loading