FEATURE: add a global setting to support custom docs url path (#107)

* FEATURE: add a global setting to support custom docs url path

This commit adds a GlobalSetting `docs_path` to support custom docs url
path for sites that do not want docs page to live at `/docs` and have a
customized path.

* Fixed the route declaration

* Test and linting

* Update server.en.yml

* Fixed doc test

* Fixed linting.

* Testing qunit test fix

* Fixed tests

* Prettified tests

* Changed the implementation from SiteSetting to GlobalSetting instead.

* Fixed tests

* Cleanup

* Using Site instead of .js.erb to pass GlobalSetting.docs_url to the front end.

Also fixed front end tests

* Remove references to obsolete site setting

* remove unused fixture file

* Rename `docs_url` to `docs_path` and use camelCase in JavaScript

* Add serializer tests

Co-authored-by: Arpit Jalan <er.ajalan@gmail.com>
This commit is contained in:
Frank 2022-11-17 14:03:38 +08:00 committed by GitHub
parent f1dd03f0bc
commit 6b3f2576c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 104 additions and 41 deletions

View File

@ -1,9 +1,11 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { getDocs } from "../../lib/get-docs";
export default Component.extend({ export default Component.extend({
classNames: "docs-topic-list", classNames: "docs-topic-list",
urlPath: getDocs(),
@discourseComputed("order") @discourseComputed("order")
sortTitle(order) { sortTitle(order) {

View File

@ -1,5 +1,9 @@
import { getDocs } from "../lib/get-docs";
export default function () { export default function () {
this.route("docs", { path: "/docs" }, function () { const docsPath = getDocs();
this.route("docs", { path: "/" + docsPath }, function () {
this.route("index", { path: "/" }); this.route("index", { path: "/" });
}); });
} }

View File

@ -1,16 +1,20 @@
import { withPluginApi } from "discourse/lib/plugin-api"; import { withPluginApi } from "discourse/lib/plugin-api";
import I18n from "I18n"; import I18n from "I18n";
import { getDocs } from "../../lib/get-docs";
function initialize(api, container) { function initialize(api, container) {
const siteSettings = container.lookup("site-settings:main"); const siteSettings = container.lookup("site-settings:main");
const docsPath = getDocs();
api.addKeyboardShortcut("g e", "", { path: "/docs" }); api.addKeyboardShortcut("g e", "", {
path: "/" + docsPath,
});
if (siteSettings.docs_add_to_top_menu) { if (siteSettings.docs_add_to_top_menu) {
api.addNavigationBarItem({ api.addNavigationBarItem({
name: "docs", name: "docs",
displayName: I18n.t("docs.title"), displayName: I18n.t("docs.title"),
href: "/docs", href: "/" + docsPath,
}); });
} }
} }

View File

@ -1,8 +1,10 @@
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import Topic from "discourse/models/topic"; import Topic from "discourse/models/topic";
import { getDocs } from "../../lib/get-docs";
const Docs = EmberObject.extend({}); const Docs = EmberObject.extend({});
const docsPath = getDocs();
Docs.reopenClass({ Docs.reopenClass({
list(params) { list(params) {
@ -34,7 +36,7 @@ Docs.reopenClass({
filters.push("track_visit=true"); filters.push("track_visit=true");
} }
return ajax(`/docs.json?${filters.join("&")}`).then((data) => { return ajax(`/${docsPath}.json?${filters.join("&")}`).then((data) => {
data.topics.topic_list.topics = data.topics.topic_list.topics.map( data.topics.topic_list.topics = data.topics.topic_list.topics.map(
(topic) => Topic.create(topic) (topic) => Topic.create(topic)
); );

View File

@ -28,7 +28,7 @@
<tbody class="topic-list-body"> <tbody class="topic-list-body">
{{#each topics as |topic|}} {{#each topics as |topic|}}
{{raw "docs-topic-list-item" topic=topic}} {{raw "docs-topic-list-item" topic=topic urlPath=urlPath}}
{{/each}} {{/each}}
</tbody> </tbody>
</table> </table>

View File

@ -1 +1 @@
<a href="/docs?topic={{topic.id}}" class="docs-topic-link">{{{topic.fancyTitle}}}</a> <a href="/{{urlPath}}?topic={{topic.id}}" class="docs-topic-link">{{{topic.fancyTitle}}}</a>

View File

@ -2,7 +2,7 @@
<td class="main-link topic-list-data"> <td class="main-link topic-list-data">
<span class="link-top-line"> <span class="link-top-line">
{{~raw "topic-status" topic=topic}} {{~raw "topic-status" topic=topic}}
{{~raw "docs-topic-link" topic=topic}} {{~raw "docs-topic-link" topic=topic urlPath=urlPath}}
</span> </span>
<span class="link-bottom-line"> <span class="link-bottom-line">
{{category-link topic.category}} {{category-link topic.category}}

View File

@ -0,0 +1,5 @@
import Site from "discourse/models/site";
export function getDocs() {
return Site.currentProp("docs_path") || "docs";
}

View File

@ -6,8 +6,8 @@ module ::Docs
config.after_initialize do config.after_initialize do
Discourse::Application.routes.append do Discourse::Application.routes.append do
mount ::Docs::Engine, at: '/docs' mount ::Docs::Engine, at: "/#{GlobalSetting.docs_path}"
get '/knowledge-explorer', to: redirect("/docs") get '/knowledge-explorer', to: redirect("/#{GlobalSetting.docs_path}")
end end
end end
end end

View File

@ -170,7 +170,7 @@ module Docs
filters.push('page=1') filters.push('page=1')
end end
"/docs.json?#{filters.join('&')}" "/#{GlobalSetting.docs_path}.json?#{filters.join('&')}"
end end
end end
end end

View File

@ -20,6 +20,8 @@ register_svg_icon 'sort-numeric-down'
load File.expand_path('lib/docs/engine.rb', __dir__) load File.expand_path('lib/docs/engine.rb', __dir__)
load File.expand_path('lib/docs/query.rb', __dir__) load File.expand_path('lib/docs/query.rb', __dir__)
GlobalSetting.add_default :docs_path, "docs"
after_initialize do after_initialize do
require_dependency 'search' require_dependency 'search'
@ -55,6 +57,10 @@ after_initialize do
end end
any_user_agent[:disallow] ||= [] any_user_agent[:disallow] ||= []
any_user_agent[:disallow] << "/docs/" any_user_agent[:disallow] << "/#{GlobalSetting.docs_path}/"
end
add_to_serializer(:site, :docs_path) do
GlobalSetting.docs_path
end end
end end

View File

@ -13,12 +13,13 @@ describe Docs::DocsController do
SiteSetting.docs_enabled = true SiteSetting.docs_enabled = true
SiteSetting.docs_categories = category.id.to_s SiteSetting.docs_categories = category.id.to_s
SiteSetting.docs_tags = 'test' SiteSetting.docs_tags = 'test'
GlobalSetting.stubs(:docs_path).returns('docs')
end end
describe 'docs data' do describe 'docs data' do
context 'when any user' do context 'when any user' do
it 'should return the right response' do it 'should return the right response' do
get '/docs.json' get "/#{GlobalSetting.docs_path}.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -31,7 +32,7 @@ describe Docs::DocsController do
end end
it 'should return a topic count' do it 'should return a topic count' do
get '/docs.json' get "/#{GlobalSetting.docs_path}.json"
json = response.parsed_body json = response.parsed_body
topic_count = json['topic_count'] topic_count = json['topic_count']
@ -50,7 +51,7 @@ describe Docs::DocsController do
end end
it 'should not show topics in private categories without permissions' do it 'should not show topics in private categories without permissions' do
get '/docs.json' get "/#{GlobalSetting.docs_path}.json"
json = JSON.parse(response.body) json = JSON.parse(response.body)
topics = json['topics']['topic_list']['topics'] topics = json['topics']['topic_list']['topics']
@ -62,7 +63,7 @@ describe Docs::DocsController do
admin = Fabricate(:admin) admin = Fabricate(:admin)
sign_in(admin) sign_in(admin)
get '/docs.json' get "/#{GlobalSetting.docs_path}.json"
json = JSON.parse(response.body) json = JSON.parse(response.body)
topics = json['topics']['topic_list']['topics'] topics = json['topics']['topic_list']['topics']
@ -76,7 +77,7 @@ describe Docs::DocsController do
fab!(:tag3) { Fabricate(:tag, topics: [topic], name: 'test3') } fab!(:tag3) { Fabricate(:tag, topics: [topic], name: 'test3') }
it 'should return a list filtered by tag' do it 'should return a list filtered by tag' do
get '/docs.json?tags=test' get "/#{GlobalSetting.docs_path}.json?tags=test"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -87,7 +88,7 @@ describe Docs::DocsController do
end end
it 'should properly filter with more than two tags' do it 'should properly filter with more than two tags' do
get '/docs.json?tags=test%7ctest2%7ctest3' get "/#{GlobalSetting.docs_path}.json?tags=test%7ctest2%7ctest3"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -109,7 +110,7 @@ describe Docs::DocsController do
end end
it 'should return a list filtered by category' do it 'should return a list filtered by category' do
get "/docs.json?category=#{category2.id}" get "/#{GlobalSetting.docs_path}.json?category=#{category2.id}"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -122,7 +123,7 @@ describe Docs::DocsController do
end end
it 'ignores category filter when incorrect argument' do it 'ignores category filter when incorrect argument' do
get "/docs.json?category=hack" get "/#{GlobalSetting.docs_path}.json?category=hack"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -139,7 +140,7 @@ describe Docs::DocsController do
context 'when ordering results' do context 'when ordering results' do
describe 'by title' do describe 'by title' do
it 'should return the list ordered descending' do it 'should return the list ordered descending' do
get "/docs.json?order=title" get "/#{GlobalSetting.docs_path}.json?order=title"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -151,7 +152,7 @@ describe Docs::DocsController do
end end
it 'should return the list ordered ascending with an additional parameter' do it 'should return the list ordered ascending with an additional parameter' do
get "/docs.json?order=title&ascending=true" get "/#{GlobalSetting.docs_path}.json?order=title&ascending=true"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -169,7 +170,7 @@ describe Docs::DocsController do
end end
it 'should return the list ordered descending' do it 'should return the list ordered descending' do
get "/docs.json?order=activity" get "/#{GlobalSetting.docs_path}.json?order=activity"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -181,7 +182,7 @@ describe Docs::DocsController do
end end
it 'should return the list ordered ascending with an additional parameter' do it 'should return the list ordered ascending with an additional parameter' do
get "/docs.json?order=activity&ascending=true" get "/#{GlobalSetting.docs_path}.json?order=activity&ascending=true"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -211,7 +212,7 @@ describe Docs::DocsController do
end end
it 'should correctly filter topics' do it 'should correctly filter topics' do
get "/docs.json?search=banana" get "/#{GlobalSetting.docs_path}.json?search=banana"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -225,7 +226,7 @@ describe Docs::DocsController do
expect(topics.size).to eq(2) expect(topics.size).to eq(2)
get "/docs.json?search=walk" get "/#{GlobalSetting.docs_path}.json?search=walk"
json = JSON.parse(response.body) json = JSON.parse(response.body)
topics = json['topics']['topic_list']['topics'] topics = json['topics']['topic_list']['topics']
@ -239,13 +240,13 @@ describe Docs::DocsController do
let!(:non_ke_topic) { Fabricate(:topic) } let!(:non_ke_topic) { Fabricate(:topic) }
it 'should correctly grab the topic' do it 'should correctly grab the topic' do
get "/docs.json?topic=#{topic.id}" get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}"
expect(response.parsed_body['topic']['id']).to eq(topic.id) expect(response.parsed_body['topic']['id']).to eq(topic.id)
end end
it 'should get topics matching a selected docs tag or category' do it 'should get topics matching a selected docs tag or category' do
get "/docs.json?topic=#{non_ke_topic.id}" get "/#{GlobalSetting.docs_path}.json?topic=#{non_ke_topic.id}"
expect(response.parsed_body['topic']).to be_blank expect(response.parsed_body['topic']).to be_blank
end end
@ -253,7 +254,7 @@ describe Docs::DocsController do
it 'should return a docs topic when only tags are added to settings' do it 'should return a docs topic when only tags are added to settings' do
SiteSetting.docs_categories = nil SiteSetting.docs_categories = nil
get "/docs.json?topic=#{topic.id}" get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}"
expect(response.parsed_body['topic']['id']).to eq(topic.id) expect(response.parsed_body['topic']['id']).to eq(topic.id)
end end
@ -261,7 +262,7 @@ describe Docs::DocsController do
it 'should return a docs topic when only categories are added to settings' do it 'should return a docs topic when only categories are added to settings' do
SiteSetting.docs_tags = nil SiteSetting.docs_tags = nil
get "/docs.json?topic=#{topic.id}" get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}"
expect(response.parsed_body['topic']['id']).to eq(topic.id) expect(response.parsed_body['topic']['id']).to eq(topic.id)
end end
@ -270,19 +271,19 @@ describe Docs::DocsController do
admin = Fabricate(:admin) admin = Fabricate(:admin)
sign_in(admin) sign_in(admin)
expect do expect do
get "/docs.json?topic=#{topic.id}" get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}"
end.to change { TopicViewItem.count }.by(1) end.to change { TopicViewItem.count }.by(1)
end end
it 'should create TopicUser if authenticated' do it 'should create TopicUser if authenticated' do
expect do expect do
get "/docs.json?topic=#{topic.id}&track_visit=true" get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}&track_visit=true"
end.not_to change { TopicUser.count } end.not_to change { TopicUser.count }
admin = Fabricate(:admin) admin = Fabricate(:admin)
sign_in(admin) sign_in(admin)
expect do expect do
get "/docs.json?topic=#{topic.id}&track_visit=true" get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}&track_visit=true"
end.to change { TopicUser.count }.by(1) end.to change { TopicUser.count }.by(1)
end end
end end

View File

@ -5,12 +5,13 @@ require 'rails_helper'
describe RobotsTxtController do describe RobotsTxtController do
before do before do
SiteSetting.docs_enabled = true SiteSetting.docs_enabled = true
GlobalSetting.stubs(:docs_path).returns('docs')
end end
it 'adds /docs/ to robots.txt' do it 'adds /docs/ to robots.txt' do
get '/robots.txt' get '/robots.txt'
expect(response.body).to include('User-agent: *') expect(response.body).to include('User-agent: *')
expect(response.body).to include('Disallow: /docs/') expect(response.body).to include("Disallow: /#{GlobalSetting.docs_path}/")
end end
end end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'rails_helper'
describe SiteSerializer do
fab!(:user) { Fabricate(:user) }
let(:guardian) { Guardian.new(user) }
before do
SiteSetting.docs_enabled = true
GlobalSetting.stubs(:docs_path).returns('docs')
end
it 'returns correct default value' do
data = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
expect(data[:docs_path]).to eq("docs")
end
it 'returns custom path based on global setting' do
GlobalSetting.stubs(:docs_path).returns('custom_path')
data = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
expect(data[:docs_path]).to eq("custom_path")
end
end

View File

@ -9,9 +9,11 @@ import {
import docsFixtures from "../fixtures/docs"; import docsFixtures from "../fixtures/docs";
import { cloneJSON } from "discourse-common/lib/object"; import { cloneJSON } from "discourse-common/lib/object";
let DOCS_URL_PATH = "docs";
acceptance("Docs - Sidebar with docs disabled", function (needs) { acceptance("Docs - Sidebar with docs disabled", function (needs) {
needs.user(); needs.user();
needs.site({ docs_path: DOCS_URL_PATH });
needs.settings({ needs.settings({
docs_enabled: false, docs_enabled: false,
enable_experimental_sidebar_hamburger: true, enable_experimental_sidebar_hamburger: true,
@ -34,7 +36,7 @@ acceptance("Docs - Sidebar with docs disabled", function (needs) {
acceptance("Docs - Sidebar with docs enabled", function (needs) { acceptance("Docs - Sidebar with docs enabled", function (needs) {
needs.user(); needs.user();
needs.site({ docs_path: DOCS_URL_PATH });
needs.settings({ needs.settings({
docs_enabled: true, docs_enabled: true,
enable_experimental_sidebar_hamburger: true, enable_experimental_sidebar_hamburger: true,
@ -42,7 +44,9 @@ acceptance("Docs - Sidebar with docs enabled", function (needs) {
}); });
needs.pretender((server, helper) => { needs.pretender((server, helper) => {
server.get("/docs.json", () => helper.response(cloneJSON(docsFixtures))); server.get("/" + DOCS_URL_PATH + ".json", () =>
helper.response(cloneJSON(docsFixtures))
);
}); });
test("clicking on docs link", async function (assert) { test("clicking on docs link", async function (assert) {
@ -66,6 +70,10 @@ acceptance("Docs - Sidebar with docs enabled", function (needs) {
await click(".sidebar-section-link-docs"); await click(".sidebar-section-link-docs");
assert.strictEqual(currentURL(), "/docs", "it navigates to the right page"); assert.strictEqual(
currentURL(),
"/" + DOCS_URL_PATH,
"it navigates to the right page"
);
}); });
}); });

View File

@ -8,14 +8,17 @@ import { test } from "qunit";
import docsFixtures from "../fixtures/docs"; import docsFixtures from "../fixtures/docs";
import { click, visit } from "@ember/test-helpers"; import { click, visit } from "@ember/test-helpers";
let DOCS_URL_PATH = "docs";
acceptance("Docs", function (needs) { acceptance("Docs", function (needs) {
needs.user(); needs.user();
needs.site({ docs_path: DOCS_URL_PATH });
needs.settings({ needs.settings({
docs_enabled: true, docs_enabled: true,
}); });
needs.pretender((server, helper) => { needs.pretender((server, helper) => {
server.get("/docs.json", (request) => { server.get("/" + DOCS_URL_PATH + ".json", (request) => {
if (request.queryParams.category === "1") { if (request.queryParams.category === "1") {
const fixture = JSON.parse(JSON.stringify(docsFixtures)); const fixture = JSON.parse(JSON.stringify(docsFixtures));
@ -54,7 +57,7 @@ acceptance("Docs", function (needs) {
}); });
test("selecting a category", async function (assert) { test("selecting a category", async function (assert) {
await visit("/docs"); await visit("/" + DOCS_URL_PATH);
assert.equal(count(".docs-category.selected"), 0); assert.equal(count(".docs-category.selected"), 0);
await click(".docs-item.docs-category"); await click(".docs-item.docs-category");
@ -64,12 +67,13 @@ acceptance("Docs", function (needs) {
acceptance("Docs - empty state", function (needs) { acceptance("Docs - empty state", function (needs) {
needs.user(); needs.user();
needs.site({ docs_path: DOCS_URL_PATH });
needs.settings({ needs.settings({
docs_enabled: true, docs_enabled: true,
}); });
needs.pretender((server, helper) => { needs.pretender((server, helper) => {
server.get("/docs.json", () => { server.get("/" + DOCS_URL_PATH + ".json", () => {
const response = { const response = {
tags: [], tags: [],
categories: [], categories: [],
@ -90,7 +94,7 @@ acceptance("Docs - empty state", function (needs) {
}); });
test("shows the empty state panel when there are no docs", async function (assert) { test("shows the empty state panel when there are no docs", async function (assert) {
await visit("/docs"); await visit("/" + DOCS_URL_PATH);
assert.ok(exists("div.empty-state")); assert.ok(exists("div.empty-state"));
}); });
}); });