Merge pull request #23393 from opf/wiki-page-search

Search pages query (for internal wiki)
This commit is contained in:
Jan Sandbrink
2026-05-29 14:39:00 +02:00
committed by GitHub
11 changed files with 323 additions and 29 deletions
@@ -0,0 +1,41 @@
# 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.
#++
module Wikis
module Adapters
module Input
class SearchPagesContract < DryApplicationContract
params do
required(:query).filled(:string)
end
end
end
end
end
@@ -0,0 +1,39 @@
# 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.
#++
module Wikis::Adapters::Input
SearchPages = Data.define(:query) do
private_class_method :new
def self.build(query:, contract: SearchPagesContract.new)
contract.call(query:).to_monad.fmap { new(**it.to_h) }
end
end
end
@@ -34,33 +34,37 @@ module Wikis
module Internal
module Queries
class PageInfo < BaseQuery
class << self
def wiki_page_to_page_info(wiki_page, provider:)
Results::PageInfo.new(
identifier: wiki_page.id.to_s,
title: wiki_page.title,
provider:,
href: url_for(only_path: true,
controller: "/wiki",
action: "show",
project_id: wiki_page.project.identifier,
id: wiki_page.slug)
)
end
private
delegate :url_for, to: :url_helpers
def url_helpers
@url_helpers ||= OpenProject::StaticRouting::StaticRouter.new.url_helpers
end
end
def call(input_data:, auth_strategy:)
Adapters::Authentication[auth_strategy].call do |user|
wiki_page = WikiPage.visible(user).find_by(id: input_data.identifier)
return failure(code: :not_found) if wiki_page.nil?
success(
Results::PageInfo.new(
identifier: input_data.identifier,
title: wiki_page.title,
provider:,
href: url_for(only_path: true,
controller: "/wiki",
action: "show",
project_id: wiki_page.project.identifier,
id: wiki_page.slug)
)
)
success(self.class.wiki_page_to_page_info(wiki_page, provider:))
end
end
private
delegate :url_for, to: :url_helpers
def url_helpers
OpenProject::StaticRouting::StaticRouter.new.url_helpers
end
end
end
end
@@ -0,0 +1,52 @@
# 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.
#++
module Wikis
module Adapters
module Providers
module Internal
module Queries
class SearchPages < BaseQuery
MAXIMUM_RESULTS = 50
def call(input_data:, auth_strategy:)
success(
WikiPage.visible(auth_strategy.user)
.where("title ILIKE ?", "%#{input_data.query}%")
.limit(MAXIMUM_RESULTS)
.map { PageInfo.wiki_page_to_page_info(it, provider:) }
)
end
end
end
end
end
end
end
@@ -53,6 +53,7 @@ module Wikis
register(:page_info, Queries::PageInfo)
register(:referencing_pages, Queries::ReferencingPages)
register(:relation_page_links, Queries::RelationPageLinks)
register(:search_pages, Queries::SearchPages)
end
end
end
@@ -0,0 +1,53 @@
# 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.
#++
module Wikis
module Adapters
module Providers
module XWiki
module Queries
class SearchPages < BaseQuery
def call(input_data:, **)
# TODO: use real API endpoints once available
titles = [
"#{input_data.query} makes XWiki special",
"API documentation of #{input_data.query}",
"A brief introduction on configuring your own #{input_data.query}."
]
success(titles.map { Results::PageInfo.new(identifier: "1338", title: it, href: "#", provider:) })
end
end
end
end
end
end
end
@@ -64,6 +64,7 @@ module Wikis
register(:page_info, Queries::PageInfo)
register(:referencing_pages, Queries::ReferencingPages)
register(:relation_page_links, Queries::RelationPageLinks)
register(:search_pages, Queries::SearchPages)
end
namespace("validators") do
@@ -35,7 +35,7 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::PageInfo do
let(:provider) { create(:internal_wiki_provider) }
let(:input_data) { Wikis::Adapters::Input::PageInfo.build(identifier:).value! }
let(:auth_strategy) { provider.auth_strategy_for(current_user).value! }
let(:auth_strategy) { provider.auth_strategy_for(user).value! }
let(:identifier) { wiki_page.id.to_s }
let(:wiki_page) { create(:wiki_page) }
@@ -43,10 +43,10 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::PageInfo do
let(:other_wiki_page) { create(:wiki_page) }
let(:permissions) { %i[view_work_packages view_wiki_pages] }
current_user { create(:user) }
let(:user) { create(:user) }
before do
create(:member, project:, user: current_user, roles: [create(:project_role, permissions:)])
create(:member, project:, user:, roles: [create(:project_role, permissions:)])
end
it { is_expected.to be_success }
@@ -35,7 +35,7 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::ReferencingPages d
let(:provider) { create(:internal_wiki_provider) }
let(:input_data) { Wikis::Adapters::Input::ReferencingPages.build(linkable:).value! }
let(:auth_strategy) { provider.auth_strategy_for(current_user).value! }
let(:auth_strategy) { provider.auth_strategy_for(user).value! }
let(:linkable) { create(:work_package) }
let(:wiki_page) { create(:wiki_page) }
@@ -48,11 +48,11 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::ReferencingPages d
]
end
current_user { create(:user) }
let(:user) { create(:user) }
before do
create(:member, project: wiki_project,
user: current_user,
user:,
roles: [create(:project_role, permissions: wiki_project_permissions)])
reverse_page_links.each(&:save!)
@@ -35,7 +35,7 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::RelationPageLinks
let(:provider) { create(:internal_wiki_provider) }
let(:input_data) { Wikis::Adapters::Input::RelationPageLinks.build(linkable: work_package).value! }
let(:auth_strategy) { provider.auth_strategy_for(current_user).value! }
let(:auth_strategy) { provider.auth_strategy_for(user).value! }
let(:wiki_page) { create(:wiki_page) }
let(:project) { wiki_page.project }
@@ -48,10 +48,10 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::RelationPageLinks
create(:relation_wiki_page_link, provider:, linkable: work_package, identifier: "THIS IS NO MOON")
end
current_user { create(:user) }
let(:user) { create(:user) }
before do
create(:member, project:, user: current_user, roles: [create(:project_role, permissions:)])
create(:member, project:, user:, roles: [create(:project_role, permissions:)])
link_to_existing_page
link_to_non_existing_page
@@ -0,0 +1,103 @@
# 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.
#++
require "spec_helper"
RSpec.describe Wikis::Adapters::Providers::Internal::Queries::SearchPages do
subject { described_class.new(model: provider).call(input_data:, auth_strategy:) }
let(:provider) { create(:internal_wiki_provider) }
let(:input_data) { Wikis::Adapters::Input::SearchPages.build(query:).value! }
let(:auth_strategy) { provider.auth_strategy_for(user).value! }
let(:query) { wiki_page.title }
let(:wiki_page) { create(:wiki_page, title: "Wiki Page with a Title you will love") }
let(:wiki_project) { wiki_page.project }
let(:wiki_project_permissions) { %i[view_wiki_pages] }
let(:user) { create(:user) }
before do
create(:member, project: wiki_project,
user:,
roles: [create(:project_role, permissions: wiki_project_permissions)])
wiki_page
end
it { is_expected.to be_success }
it "returns pages matching the search term exactly" do
expect(subject.value!).not_to be_empty
expect(subject.value!.first.title).to eq(wiki_page.title)
end
context "when the search term only matches partially" do
let(:query) { "a Title" }
it { is_expected.to be_success }
it "returns matching pages" do
expect(subject.value!).not_to be_empty
expect(subject.value!.first.title).to eq(wiki_page.title)
end
end
context "when the search term has wrong casing" do
let(:query) { wiki_page.title.downcase }
it { is_expected.to be_success }
it "returns matching pages" do
expect(subject.value!).not_to be_empty
expect(subject.value!.first.title).to eq(wiki_page.title)
end
end
context "when there are no matching pages" do
let(:query) { "the title" }
it { is_expected.to be_success }
it "returns an empty result" do
expect(subject.value!).to eq([])
end
end
context "when user can't see a matching wiki page" do
let(:wiki_project_permissions) { %i[] }
it { is_expected.to be_success }
it "returns an empty result" do
expect(subject.value!).to eq([])
end
end
end