├── .gitignore
├── .npmrc
├── .streerc
├── .rubocop.yml
├── .prettierrc.cjs
├── .template-lintrc.cjs
├── stylelint.config.mjs
├── eslint.config.mjs
├── assets
├── javascripts
│ ├── discourse
│ │ ├── routes
│ │ │ ├── docs.js
│ │ │ └── docs
│ │ │ │ └── index.js
│ │ ├── docs-route-map.js
│ │ ├── controllers
│ │ │ ├── docs.js
│ │ │ └── docs
│ │ │ │ └── index.js
│ │ ├── components
│ │ │ ├── docs-tag.gjs
│ │ │ ├── docs-category.gjs
│ │ │ ├── docs-search.gjs
│ │ │ └── docs-topic.gjs
│ │ ├── templates
│ │ │ ├── docs.gjs
│ │ │ └── docs
│ │ │ │ └── index.gjs
│ │ ├── initializers
│ │ │ └── setup-docs.js
│ │ └── models
│ │ │ └── docs.js
│ └── lib
│ │ └── get-docs.js
└── stylesheets
│ ├── mobile
│ └── docs.scss
│ └── common
│ └── docs.scss
├── lib
├── docs_constraint.rb
├── onebox
│ └── templates
│ │ └── discourse_docs_list.mustache
└── docs
│ ├── engine.rb
│ └── query.rb
├── Gemfile
├── README.md
├── spec
├── system
│ ├── core_features_spec.rb
│ └── docs_index_spec.rb
├── requests
│ ├── robots_txt_controller_spec.rb
│ └── docs_controller_spec.rb
├── serializers
│ └── site_serializer_spec.rb
└── plugin_spec.rb
├── app
├── views
│ └── docs
│ │ └── docs
│ │ └── get_topic.html.erb
└── controllers
│ └── docs
│ └── docs_controller.rb
├── config
├── locales
│ ├── server.be.yml
│ ├── server.bg.yml
│ ├── server.ca.yml
│ ├── server.da.yml
│ ├── server.el.yml
│ ├── server.et.yml
│ ├── server.gl.yml
│ ├── server.hr.yml
│ ├── server.hy.yml
│ ├── server.id.yml
│ ├── server.ko.yml
│ ├── server.lt.yml
│ ├── server.lv.yml
│ ├── server.pt.yml
│ ├── server.ro.yml
│ ├── server.sk.yml
│ ├── server.sl.yml
│ ├── server.sq.yml
│ ├── server.sr.yml
│ ├── server.sw.yml
│ ├── server.te.yml
│ ├── server.th.yml
│ ├── server.uk.yml
│ ├── server.ur.yml
│ ├── server.vi.yml
│ ├── server.bs_BA.yml
│ ├── server.en_GB.yml
│ ├── server.nb_NO.yml
│ ├── server.zh_TW.yml
│ ├── client.en_GB.yml
│ ├── client.sr.yml
│ ├── client.sw.yml
│ ├── client.ur.yml
│ ├── client.da.yml
│ ├── client.th.yml
│ ├── client.vi.yml
│ ├── client.be.yml
│ ├── client.bg.yml
│ ├── client.ca.yml
│ ├── client.et.yml
│ ├── client.hy.yml
│ ├── client.lt.yml
│ ├── client.lv.yml
│ ├── client.sl.yml
│ ├── client.bs_BA.yml
│ ├── client.gl.yml
│ ├── client.nb_NO.yml
│ ├── client.pt.yml
│ ├── client.ro.yml
│ ├── client.sq.yml
│ ├── client.el.yml
│ ├── client.hr.yml
│ ├── client.ko.yml
│ ├── client.id.yml
│ ├── client.sk.yml
│ ├── client.uk.yml
│ ├── client.te.yml
│ ├── client.zh_TW.yml
│ ├── server.fa_IR.yml
│ ├── server.zh_CN.yml
│ ├── server.sv.yml
│ ├── server.en.yml
│ ├── server.ja.yml
│ ├── server.he.yml
│ ├── server.ar.yml
│ ├── server.ug.yml
│ ├── server.pl_PL.yml
│ ├── server.ru.yml
│ ├── server.cs.yml
│ ├── server.fi.yml
│ ├── server.nl.yml
│ ├── server.es.yml
│ ├── server.fr.yml
│ ├── server.pt_BR.yml
│ ├── server.tr_TR.yml
│ ├── server.hu.yml
│ ├── server.de.yml
│ ├── server.it.yml
│ ├── client.zh_CN.yml
│ ├── client.en.yml
│ ├── client.ja.yml
│ ├── client.ug.yml
│ ├── client.he.yml
│ ├── client.fi.yml
│ ├── client.fa_IR.yml
│ ├── client.sv.yml
│ ├── client.es.yml
│ ├── client.nl.yml
│ ├── client.de.yml
│ ├── client.tr_TR.yml
│ ├── client.pt_BR.yml
│ ├── client.fr.yml
│ ├── client.hu.yml
│ ├── client.pl_PL.yml
│ ├── client.it.yml
│ ├── client.cs.yml
│ ├── client.ru.yml
│ └── client.ar.yml
├── routes.rb
└── settings.yml
├── translator.yml
├── .github
└── workflows
│ └── discourse-plugin.yml
├── package.json
├── .discourse-compatibility
├── db
└── migrate
│ └── 20210114161508_rename_knowledge_explorer_settings.rb
├── LICENSE
├── test
└── javascripts
│ ├── unit
│ └── controllers
│ │ └── docs-index-test.js
│ ├── acceptance
│ ├── docs-user-status-test.js
│ ├── docs-sidebar-test.js
│ └── docs-test.js
│ └── fixtures
│ ├── docs.js
│ └── docs-show-tag-groups.js
├── Gemfile.lock
└── plugin.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict = true
2 | auto-install-peers = false
3 |
--------------------------------------------------------------------------------
/.streerc:
--------------------------------------------------------------------------------
1 | --print-width=100
2 | --plugins=plugin/trailing_comma
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_gem:
2 | rubocop-discourse: stree-compat.yml
3 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = require("@discourse/lint-configs/prettier");
2 |
--------------------------------------------------------------------------------
/.template-lintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = require("@discourse/lint-configs/template-lint");
2 |
--------------------------------------------------------------------------------
/stylelint.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | extends: ["@discourse/lint-configs/stylelint"],
3 | };
4 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import DiscourseRecommended from "@discourse/lint-configs/eslint";
2 |
3 | export default [...DiscourseRecommended];
4 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/routes/docs.js:
--------------------------------------------------------------------------------
1 | import DiscourseRoute from "discourse/routes/discourse";
2 |
3 | export default class Docs extends DiscourseRoute {}
4 |
--------------------------------------------------------------------------------
/lib/docs_constraint.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DocsConstraint
4 | def matches?(_request)
5 | SiteSetting.docs_enabled
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | group :development do
6 | gem "rubocop-discourse"
7 | gem "syntax_tree"
8 | end
9 |
--------------------------------------------------------------------------------
/assets/javascripts/lib/get-docs.js:
--------------------------------------------------------------------------------
1 | import Site from "discourse/models/site";
2 |
3 | export function getDocs() {
4 | return Site.currentProp("docs_path") || "docs";
5 | }
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Discourse Docs Plugin
2 |
3 | Find and filter knowledge base topics.
4 |
5 | For more information, please see: https://meta.discourse.org/t/discourse-docs-documentation-management-plugin/130172/
6 |
--------------------------------------------------------------------------------
/spec/system/core_features_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe "Core features", type: :system do
4 | before { enable_current_plugin }
5 |
6 | it_behaves_like "having working core features"
7 | end
8 |
--------------------------------------------------------------------------------
/lib/onebox/templates/discourse_docs_list.mustache:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/views/docs/docs/get_topic.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :head do %>
2 | <%= raw crawlable_meta_data(title: @topic["title"], description: @excerpt, ignore_canonical: true) if @topic %>
3 | <% end %>
4 |
5 | <% content_for(:title) { @title } %>
6 |
--------------------------------------------------------------------------------
/config/locales/server.be.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | be:
8 |
--------------------------------------------------------------------------------
/config/locales/server.bg.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | bg:
8 |
--------------------------------------------------------------------------------
/config/locales/server.ca.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ca:
8 |
--------------------------------------------------------------------------------
/config/locales/server.da.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | da:
8 |
--------------------------------------------------------------------------------
/config/locales/server.el.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | el:
8 |
--------------------------------------------------------------------------------
/config/locales/server.et.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | et:
8 |
--------------------------------------------------------------------------------
/config/locales/server.gl.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | gl:
8 |
--------------------------------------------------------------------------------
/config/locales/server.hr.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | hr:
8 |
--------------------------------------------------------------------------------
/config/locales/server.hy.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | hy:
8 |
--------------------------------------------------------------------------------
/config/locales/server.id.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | id:
8 |
--------------------------------------------------------------------------------
/config/locales/server.ko.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ko:
8 |
--------------------------------------------------------------------------------
/config/locales/server.lt.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | lt:
8 |
--------------------------------------------------------------------------------
/config/locales/server.lv.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | lv:
8 |
--------------------------------------------------------------------------------
/config/locales/server.pt.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | pt:
8 |
--------------------------------------------------------------------------------
/config/locales/server.ro.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ro:
8 |
--------------------------------------------------------------------------------
/config/locales/server.sk.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sk:
8 |
--------------------------------------------------------------------------------
/config/locales/server.sl.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sl:
8 |
--------------------------------------------------------------------------------
/config/locales/server.sq.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sq:
8 |
--------------------------------------------------------------------------------
/config/locales/server.sr.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sr:
8 |
--------------------------------------------------------------------------------
/config/locales/server.sw.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sw:
8 |
--------------------------------------------------------------------------------
/config/locales/server.te.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | te:
8 |
--------------------------------------------------------------------------------
/config/locales/server.th.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | th:
8 |
--------------------------------------------------------------------------------
/config/locales/server.uk.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | uk:
8 |
--------------------------------------------------------------------------------
/config/locales/server.ur.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ur:
8 |
--------------------------------------------------------------------------------
/config/locales/server.vi.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | vi:
8 |
--------------------------------------------------------------------------------
/translator.yml:
--------------------------------------------------------------------------------
1 | # Configuration file for discourse-translator-bot
2 |
3 | files:
4 | - source_path: config/locales/client.en.yml
5 | destination_path: client.yml
6 | - source_path: config/locales/server.en.yml
7 | destination_path: server.yml
8 |
--------------------------------------------------------------------------------
/.github/workflows/discourse-plugin.yml:
--------------------------------------------------------------------------------
1 | name: Discourse Plugin
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | ci:
11 | uses: discourse/.github/.github/workflows/discourse-plugin.yml@v1
12 |
--------------------------------------------------------------------------------
/config/locales/server.bs_BA.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | bs_BA:
8 |
--------------------------------------------------------------------------------
/config/locales/server.en_GB.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | en_GB:
8 |
--------------------------------------------------------------------------------
/config/locales/server.nb_NO.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | nb_NO:
8 |
--------------------------------------------------------------------------------
/config/locales/server.zh_TW.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | zh_TW:
8 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_dependency "docs_constraint"
4 |
5 | Docs::Engine.routes.draw do
6 | get "/" => "docs#index", :constraints => DocsConstraint.new
7 | get ".json" => "docs#index", :constraints => DocsConstraint.new
8 | end
9 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/docs-route-map.js:
--------------------------------------------------------------------------------
1 | import { getDocs } from "../lib/get-docs";
2 |
3 | export default function () {
4 | const docsPath = getDocs();
5 |
6 | this.route("docs", { path: "/" + docsPath }, function () {
7 | this.route("index", { path: "/" });
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/config/locales/client.en_GB.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | en_GB:
8 | js:
9 | docs:
10 | categories: "Categories"
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "devDependencies": {
4 | "@discourse/lint-configs": "2.32.0",
5 | "ember-template-lint": "7.9.1",
6 | "eslint": "9.37.0",
7 | "prettier": "3.6.2",
8 | "stylelint": "16.25.0"
9 | },
10 | "engines": {
11 | "node": ">= 22",
12 | "npm": "please-use-pnpm",
13 | "yarn": "please-use-pnpm",
14 | "pnpm": "9.x"
15 | },
16 | "packageManager": "pnpm@9.15.5"
17 | }
18 |
--------------------------------------------------------------------------------
/config/locales/client.sr.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sr:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Tema"
12 | activity: "Aktivnosti"
13 | categories: "Kategorije"
14 | search:
15 | clear: "Čisto"
16 |
--------------------------------------------------------------------------------
/config/locales/client.sw.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sw:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Mada"
12 | activity: "Kitendo"
13 | categories: "Kategoria"
14 | tags: "Lebo"
15 | search:
16 | clear: "Futa"
17 |
--------------------------------------------------------------------------------
/config/locales/client.ur.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ur:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "ٹاپک"
12 | activity: "سرگرمی"
13 | categories: "اقسام"
14 | tags: "ٹیگز"
15 | search:
16 | clear: "صاف کریں"
17 |
--------------------------------------------------------------------------------
/config/locales/client.da.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | da:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Emne"
12 | activity: "Aktivitet"
13 | categories: "Kategorier"
14 | tags: "Mærker"
15 | search:
16 | clear: "Ryd"
17 |
--------------------------------------------------------------------------------
/config/locales/client.th.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | th:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "หัวข้อ"
12 | activity: "กิจกรรม"
13 | categories: "หมวดหมู่"
14 | tags: "ป้าย"
15 | search:
16 | clear: "ล้าง"
17 |
--------------------------------------------------------------------------------
/config/locales/client.vi.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | vi:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Chủ đề"
12 | activity: "Hoạt động"
13 | categories: "Danh mục"
14 | tags: "Thẻ"
15 | search:
16 | clear: "Xóa"
17 |
--------------------------------------------------------------------------------
/config/locales/client.be.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | be:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "тэма"
12 | activity: "актыўнасць"
13 | categories: "катэгорыі"
14 | tags: "тэгі"
15 | search:
16 | clear: "ачысціць"
17 |
--------------------------------------------------------------------------------
/config/locales/client.bg.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | bg:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Тема"
12 | activity: "Активност"
13 | categories: "Категории"
14 | tags: "Тагове"
15 | search:
16 | clear: "Изчисти"
17 |
--------------------------------------------------------------------------------
/config/locales/client.ca.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ca:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Tema"
12 | activity: "Activitat"
13 | categories: "Categories"
14 | tags: "Etiquetes"
15 | search:
16 | clear: "Neteja"
17 |
--------------------------------------------------------------------------------
/config/locales/client.et.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | et:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Teema"
12 | activity: "Aktiivsus"
13 | categories: "Liigid"
14 | tags: "Sildid"
15 | search:
16 | clear: "Tühjenda"
17 |
--------------------------------------------------------------------------------
/config/locales/client.hy.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | hy:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Թեմա"
12 | activity: "Ակտիվության"
13 | categories: "Կատեգորիաներ"
14 | tags: "Թեգեր"
15 | search:
16 | clear: "Ջնջել"
17 |
--------------------------------------------------------------------------------
/config/locales/client.lt.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | lt:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Tema"
12 | activity: "Aktyvumas"
13 | categories: "Kategorijos"
14 | tags: "Žymos"
15 | search:
16 | clear: "Išvalyti"
17 |
--------------------------------------------------------------------------------
/config/locales/client.lv.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | lv:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Tēmas"
12 | activity: "Aktivitāte"
13 | categories: "Sadaļas"
14 | tags: "Birkas"
15 | search:
16 | clear: "Noņemt"
17 |
--------------------------------------------------------------------------------
/config/locales/client.sl.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sl:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Tema"
12 | activity: "Aktivnost"
13 | categories: "Kategorije"
14 | tags: "Oznake"
15 | search:
16 | clear: "Počisti"
17 |
--------------------------------------------------------------------------------
/config/locales/client.bs_BA.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | bs_BA:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Topic"
12 | activity: "Activity"
13 | categories: "Kategorije"
14 | tags: "Oznake"
15 | search:
16 | clear: "Clear"
17 |
--------------------------------------------------------------------------------
/config/locales/client.gl.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | gl:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Tema"
12 | activity: "Actividade"
13 | categories: "Categorías"
14 | tags: "Etiquetas"
15 | search:
16 | clear: "Borrar"
17 |
--------------------------------------------------------------------------------
/config/locales/client.nb_NO.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | nb_NO:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Emne"
12 | activity: "Aktivitet"
13 | categories: "Kategorier"
14 | tags: "Stikkord"
15 | search:
16 | clear: "Tøm"
17 |
--------------------------------------------------------------------------------
/config/locales/client.pt.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | pt:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Tópico"
12 | activity: "Actividade"
13 | categories: "Categorias"
14 | tags: "Etiquetas"
15 | search:
16 | clear: "Remover"
17 |
--------------------------------------------------------------------------------
/config/locales/client.ro.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ro:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Discuție"
12 | activity: "Activitate"
13 | categories: "Categorii"
14 | tags: "Etichete"
15 | search:
16 | clear: "Șterge"
17 |
--------------------------------------------------------------------------------
/config/locales/client.sq.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sq:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Topic"
12 | activity: "Aktiviteti"
13 | categories: "Categories"
14 | tags: "Etiketat"
15 | search:
16 | clear: "Pastro"
17 |
--------------------------------------------------------------------------------
/spec/requests/robots_txt_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe RobotsTxtController do
6 | before do
7 | SiteSetting.docs_enabled = true
8 | GlobalSetting.stubs(:docs_path).returns("docs")
9 | end
10 |
11 | it "adds /docs/ to robots.txt" do
12 | get "/robots.txt"
13 |
14 | expect(response.body).to include("User-agent: *")
15 | expect(response.body).to include("Disallow: /#{GlobalSetting.docs_path}/")
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/config/locales/client.el.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | el:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Νήμα"
12 | activity: "Δραστηριότητα"
13 | categories: "Κατηγορίες"
14 | tags: "Ετικέτες"
15 | search:
16 | clear: "Καθάρισμα φίλτρου"
17 | filter_button: "Φίλτρα"
18 |
--------------------------------------------------------------------------------
/config/locales/client.hr.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | hr:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Tema"
12 | activity: "Aktivnosti"
13 | categories: "Kategorije"
14 | categories_filter_placeholder: "Filtrirajte kategorije"
15 | tags: "Oznake"
16 | search:
17 | clear: "Izbriši"
18 |
--------------------------------------------------------------------------------
/config/locales/client.ko.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ko:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "글"
12 | activity: "활동"
13 | categories: "카테고리"
14 | categories_filter_placeholder: "필터 카테고리"
15 | tags_filter_placeholder: "필터 태그"
16 | tags: "태그"
17 | search:
18 | clear: "지우기"
19 |
--------------------------------------------------------------------------------
/config/locales/client.id.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | id:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Topik"
12 | activity: "Activity"
13 | categories: "Kategori"
14 | categories_filter_placeholder: "Kategori filter"
15 | tags_filter_placeholder: "Filter tag"
16 | tags: "Label"
17 | search:
18 | clear: "Bersihkan"
19 |
--------------------------------------------------------------------------------
/config/locales/client.sk.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sk:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Témy"
12 | activity: "Aktivity"
13 | categories: "Kategórie"
14 | categories_filter_placeholder: "Filtrovanie kategórií"
15 | tags_filter_placeholder: "Značky filtra"
16 | tags: "Štítky"
17 | search:
18 | clear: "Vyčistiť"
19 |
--------------------------------------------------------------------------------
/config/locales/client.uk.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | uk:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "Тема"
12 | activity: "Активність"
13 | categories: "Розділи"
14 | categories_filter_placeholder: "Фільтр категорій"
15 | tags_filter_placeholder: "Фільтрація тегів"
16 | tags: "Теґи"
17 | search:
18 | clear: "Очистити"
19 |
--------------------------------------------------------------------------------
/config/locales/client.te.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | te:
8 | js:
9 | docs:
10 | column_titles:
11 | topic: "విషయం"
12 | activity: "కలాపం"
13 | categories: "వర్గాలు"
14 | categories_filter_placeholder: "వర్గాలను ఫిల్టర్ చేయండి"
15 | tags_filter_placeholder: "ఫిల్టర్ ట్యాగ్లు"
16 | tags: "ట్యాగులు"
17 | search:
18 | clear: "శుభ్రపరుచు"
19 |
--------------------------------------------------------------------------------
/config/locales/client.zh_TW.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | zh_TW:
8 | js:
9 | filters:
10 | docs:
11 | help: "瀏覽文件話題"
12 | docs:
13 | title: "文件"
14 | column_titles:
15 | topic: "話題"
16 | activity: "活動"
17 | categories: "分類"
18 | tags: "標記"
19 | search:
20 | placeholder: "搜尋話題"
21 | clear: "清除"
22 | sidebar:
23 | docs_link_text: "文件"
24 |
--------------------------------------------------------------------------------
/.discourse-compatibility:
--------------------------------------------------------------------------------
1 | < 3.6.0.beta1-dev: ff5d738a9f9d85847e6fc226f8324ad9cf466007
2 | < 3.5.0.beta8-dev: 17909a90a9062d11e1ec9f5974e138c54a6507e4
3 | < 3.5.0.beta5-dev: 92e29f51d5f7f8058895ceae681b0f0cfee157b2
4 | < 3.5.0.beta1-dev: 4e42539cda9a54d7827bcdf51b6dfbcf56d24cc9
5 | < 3.4.0.beta2-dev: 12dfb332bf830b1c8c9a24b86f5327504e9ab672
6 | < 3.4.0.beta1-dev: 7721b1646dead4719c02868ef7965f1b27c74eb3
7 | < 3.3.0.beta3-dev: 11dcab84669462b05eba3f1a59401727cafe8188
8 | < 3.3.0.beta1-dev: 94c7b7da216c66d773f800a714493f087affaac9
9 | 3.1.999: a4b203274b88c5277d0b5b936de0bc0e0016726c
10 | 2.8.0.beta9: 05678c451caf2ceb192501da91cf0d24ea44c8e8
11 |
--------------------------------------------------------------------------------
/config/settings.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | docs_enabled:
3 | default: false
4 | client: true
5 | docs_categories:
6 | type: category_list
7 | default: ""
8 | client: true
9 | show_tags_by_group:
10 | default: false
11 | client: true
12 | docs_tag_groups:
13 | type: tag_group_list
14 | default: ""
15 | client: true
16 | docs_tags:
17 | type: tag_list
18 | default: ""
19 | client: true
20 | docs_add_solved_filter:
21 | default: false
22 | client: true
23 | docs_add_to_top_menu:
24 | default: false
25 | client: true
26 | docs_add_search_menu_tip:
27 | default: true
28 | client: true
29 |
--------------------------------------------------------------------------------
/config/locales/server.fa_IR.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | fa_IR:
8 | site_settings:
9 | docs_enabled: "فعال کردن افزونه اسناد"
10 | docs_categories: "فهرستی از دستهبندی کوتاه که در اسناد قرار میگیرد"
11 | docs_tags: "فهرستی از برچسبها که در اسناد قرار میگیرد"
12 | docs_add_solved_filter: "یک فیلتر برای موضوعات حل شده اضافه میکند - برای نصب و فعال کردن دیسکورس Solved نیاز است"
13 | docs_add_to_top_menu: "پیوندی را به منوی بالا اضافه میکند تا به نمای اسناد برود"
14 |
--------------------------------------------------------------------------------
/config/locales/server.zh_CN.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | zh_CN:
8 | site_settings:
9 | docs_enabled: "启用文档插件"
10 | docs_categories: "要包含在文档中的类别缩略名列表"
11 | docs_tags: "要包含在文档中的标签列表"
12 | docs_add_solved_filter: "为已解决的话题添加筛选器 — 需要安装和启用 Discourse Solved"
13 | show_tags_by_group: "使用标签群组组织标签。创建群组以对相关标签进行分类。"
14 | docs_tag_groups: "用于按群组显示标签的标签群组。"
15 | docs_add_to_top_menu: "添加指向顶部菜单的链接以导航到“文档”视图"
16 | docs_add_search_menu_tip: "将提示“in:docs”添加到搜索菜单的随机提示中"
17 |
--------------------------------------------------------------------------------
/config/locales/server.sv.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sv:
8 | site_settings:
9 | docs_enabled: "Aktivera Docs-tillägget"
10 | docs_categories: "En lista över kategorietiketter som ska vara med i Docs"
11 | docs_tags: "En lista över taggar som ska vara med i Docs"
12 | docs_add_solved_filter: "Lägger till ett filter för lösta ämnen – kräver att Discourse Solved har installerats och aktiverats"
13 | docs_add_to_top_menu: "Lägger till en länk i toppmenyn som leder till Docs-vyn"
14 |
--------------------------------------------------------------------------------
/config/locales/server.en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | site_settings:
3 | docs_enabled: "Enable the Docs Plugin"
4 | docs_categories: "A list of category slugs to include in docs"
5 | docs_tags: "A list of tags to include in docs"
6 | docs_add_solved_filter: "Adds a filter for solved topics -- requires Discourse Solved to be installed and enabled"
7 | show_tags_by_group: "Organize tags using Tag Groups. Create groups to categorize related tags."
8 | docs_tag_groups: "The Tag Groups used to show tags by group."
9 | docs_add_to_top_menu: "Adds a link to the top menu to navigate to the Docs view"
10 | docs_add_search_menu_tip: "Adds the tip \"in:docs\" to the search menu random tips"
11 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/controllers/docs.js:
--------------------------------------------------------------------------------
1 | import Controller, { inject as controller } from "@ember/controller";
2 | import { action } from "@ember/object";
3 |
4 | export default class DocsController extends Controller {
5 | @controller("docs.index") indexController;
6 |
7 | @action
8 | updateSelectedCategories(category) {
9 | this.indexController.send("updateSelectedCategories", category);
10 | return false;
11 | }
12 |
13 | @action
14 | updateSelectedTags(tag) {
15 | this.indexController.send("updateSelectedTags", tag);
16 | return false;
17 | }
18 |
19 | @action
20 | performSearch(term) {
21 | this.indexController.send("performSearch", term);
22 | return false;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/config/locales/server.ja.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ja:
8 | site_settings:
9 | docs_enabled: "ドキュメントプラグインを有効にする"
10 | docs_categories: "ドキュメントに含めるカテゴリスラッグのリスト"
11 | docs_tags: "ドキュメントに含まれるタグのリスト"
12 | docs_add_solved_filter: "解決済みトピックのフィルタを追加します。Discourse Solved をインストールして有効にする必要があります"
13 | show_tags_by_group: "タググループを使用してタグを整理します。関連タグを分類するためのグループを作成します。"
14 | docs_tag_groups: "グループごとにタグを表示するために使用されるタググループ。"
15 | docs_add_to_top_menu: "ドキュメントビューに移動するリンクをトップメニューに追加します"
16 | docs_add_search_menu_tip: "検索メニューのランダムなヒントに「in:docs」ヒントを追加します"
17 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/components/docs-tag.gjs:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { on } from "@ember/modifier";
3 | import { tagName } from "@ember-decorators/component";
4 | import icon from "discourse/helpers/d-icon";
5 |
6 | @tagName("")
7 | export default class DocsTag extends Component {
8 |
9 |
15 | {{icon (if this.tag.active "circle-xmark" "plus")}}
16 |
17 | {{this.tag.id}}
18 | {{this.tag.count}}
19 |
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/spec/serializers/site_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe SiteSerializer do
6 | fab!(:user)
7 | let(:guardian) { Guardian.new(user) }
8 |
9 | before do
10 | SiteSetting.docs_enabled = true
11 | GlobalSetting.stubs(:docs_path).returns("docs")
12 | end
13 |
14 | it "returns correct default value" do
15 | data = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
16 |
17 | expect(data[:docs_path]).to eq("docs")
18 | end
19 |
20 | it "returns custom path based on global setting" do
21 | GlobalSetting.stubs(:docs_path).returns("custom_path")
22 | data = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
23 |
24 | expect(data[:docs_path]).to eq("custom_path")
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/config/locales/server.he.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | he:
8 | site_settings:
9 | docs_enabled: "הפעלת תוסף המסמכים"
10 | docs_categories: "רשימה של שמות מופשטים של קטגוריות שיכללו בתיעוד"
11 | docs_tags: "רשימה של תגיות לכלול בתיעוד"
12 | docs_add_solved_filter: "מוסיף מסנן לנושאים שנפתרו -- דורש התקנה והפעלה של התוסף Discourse Solved"
13 | show_tags_by_group: "סידור תגיות באמצעות קבוצות תגיות. אפשר ליצור קבוצות כדי לקבץ תגיות קשורות."
14 | docs_tag_groups: "קבוצות התגיות משמשות להצגת תגיות לפי קבוצה."
15 | docs_add_to_top_menu: "מוסיף קישור לתפריט העליון כדי לנווט לתצוגת התיעוד"
16 | docs_add_search_menu_tip: "מוסיף את העצה „in:docs” לעצות האקראיות של תפריט החיפוש"
17 |
--------------------------------------------------------------------------------
/config/locales/server.ar.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ar:
8 | site_settings:
9 | docs_enabled: "تفعيل المكوِّن الإضافي لـ Docs"
10 | docs_categories: "قائمة بمسارات الفئات لتضمينها في Docs"
11 | docs_tags: "قائمة بالوسوم لتضمينها في Docs"
12 | docs_add_solved_filter: "يضيف عامل تصفية للموضوعات المحلولة -- يتطلب تثبيت Discourse Solved وتفعيله"
13 | show_tags_by_group: "تنظيم الوسوم باستخدام مجموعات الوسوم. أنشئ مجموعات لتصنيف الوسوم ذات الصلة."
14 | docs_tag_groups: "مجموعات الوسوم المستخدمة لعرض الوسوم حسب المجموعة."
15 | docs_add_to_top_menu: "يضيف رابطًا إلى القائمة العلوية للانتقال إلى عرض Docs"
16 | docs_add_search_menu_tip: "يضيف التلميح \"in:docs\" إلى التلميحات العشوائية لقائمة البحث"
17 |
--------------------------------------------------------------------------------
/lib/docs/engine.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ::Docs
4 | class Engine < ::Rails::Engine
5 | isolate_namespace Docs
6 |
7 | config.after_initialize do
8 | Discourse::Application.routes.append do
9 | mount ::Docs::Engine, at: "/#{GlobalSetting.docs_path}"
10 | get "/knowledge-explorer", to: redirect("/#{GlobalSetting.docs_path}")
11 | end
12 | end
13 | end
14 |
15 | def self.onebox_template
16 | @onebox_template ||=
17 | begin
18 | path =
19 | "#{Rails.root}/plugins/discourse-docs/lib/onebox/templates/discourse_docs_list.mustache"
20 | File.read(path)
21 | end
22 | end
23 |
24 | def self.topic_in_docs(category, tags)
25 | category_match = Docs::Query.categories.include?(category.to_s)
26 | tags = tags.pluck(:name)
27 | tag_match = Docs::Query.tags.any? { |tag| tags.include?(tag) }
28 |
29 | category_match || tag_match
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/config/locales/server.ug.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ug:
8 | site_settings:
9 | docs_enabled: "پۈتۈك قىستۇرمىسىنى قوزغىتىدۇ"
10 | docs_categories: "پۈتۈكتىكى سەھىپە قىسقارتىلمىسىنىڭ تىزىمى"
11 | docs_tags: "پۈتۈكتىكى بەلگە تىزىمى"
12 | docs_add_solved_filter: "ھەل قىلىنغان تېمىغا سۈزگۈچ قوشىدۇ -- Discourse Solved ئورنىتىلىپ ۋە قوزغىتىلغان بولۇشى زۆرۈر."
13 | show_tags_by_group: "بەلگە گۇرۇپپىسى ئارقىلىق بەلگىنى تەشكىللەيدۇ. گۇرۇپپا قۇرۇپ مۇناسىۋەتلىك بەلگىلەرنى تۈرگە ئايرىيدۇ."
14 | docs_tag_groups: "بەلگىنى گۇرۇپپا بويىچە كۆرسىتىشكە ئىشلىتىدىغان بەلگە گۇرۇپپىسى."
15 | docs_add_to_top_menu: "پۈتۈك كۆرۈنۈشىگە يۆتكىلىشتە چوققا تىزىملىككە ئۇلانما قوشىدۇ"
16 | docs_add_search_menu_tip: "ئىزدەش تىزىملىكىنىڭ ئىختىيارى ئەسكەرتىشىگە «in:docs» نى قوشىدۇ"
17 |
--------------------------------------------------------------------------------
/config/locales/server.pl_PL.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | pl_PL:
8 | site_settings:
9 | docs_enabled: "Włącz wtyczkę Docs"
10 | docs_categories: "Lista ścieżek kategorii do umieszczenia w dokumentach"
11 | docs_tags: "Lista tagów do uwzględnienia w dokumentach"
12 | docs_add_solved_filter: "Dodaje filtr dla rozwiązanych tematów -- wymaga, aby Discourse Solved był zainstalowany i włączony"
13 | show_tags_by_group: "Organizuj tagi za pomocą grup tagów. Utwórz grupy do kategoryzowania powiązanych tagów."
14 | docs_tag_groups: "Grupy tagów używane do wyświetlania tagów według grupy."
15 | docs_add_to_top_menu: "Dodaje link do górnego menu, aby przejść do widoku dokumentów"
16 | docs_add_search_menu_tip: "Dodaje wskazówkę \"in:docs\" do losowych wskazówek w menu wyszukiwania"
17 |
--------------------------------------------------------------------------------
/config/locales/server.ru.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ru:
8 | site_settings:
9 | docs_enabled: "Включить плагин Docs"
10 | docs_categories: "Список идентификаторов разделов, которые будут включены в документацию"
11 | docs_tags: "Список тегов, которые будут включены в документацию"
12 | docs_add_solved_filter: "Добавить фильтр для решённых тем. Требуется, чтобы плагин Discourse Solved был установлен и включён"
13 | show_tags_by_group: "Группы тегов позволяют упорядочить теги, распределив связанные теги по категориям."
14 | docs_tag_groups: "Группы тегов для отображения тегов по группам."
15 | docs_add_to_top_menu: "Добавить ссылку в верхнее меню для перехода на страницу документации"
16 | docs_add_search_menu_tip: "Добавляет подсказку «in:docs» в случайные советы меню поиска"
17 |
--------------------------------------------------------------------------------
/config/locales/server.cs.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | cs:
8 | site_settings:
9 | docs_enabled: "Povolit plugin Dokumentace"
10 | docs_categories: "Seznam odkazů kategorie, které mají být zahrnuty do dokumentace"
11 | docs_tags: "Seznam štítků, které mají být zahrnuty do dokumentace"
12 | docs_add_solved_filter: "Přidá filtr pro vyřešená témata – vyžaduje instalaci a povolení Discourse Solved"
13 | show_tags_by_group: "Uspořádejte štítky pomocí skupin štítků. Pro kategorizaci souvisejících štítků vytvořte skupiny."
14 | docs_tag_groups: "Skupiny štítků používané pro zobrazení štítků podle skupin."
15 | docs_add_to_top_menu: "Pro přechod do zobrazení dokumentace přidá odkaz do horní nabídky"
16 | docs_add_search_menu_tip: "Přidá tip \"in:docs\" do nabídky náhodných tipů vyhledávacího menu"
17 |
--------------------------------------------------------------------------------
/config/locales/server.fi.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | fi:
8 | site_settings:
9 | docs_enabled: "Ota Docs-lisäosa käyttöön"
10 | docs_categories: "Luettelo ohjeisiin sisällytettävistä polkutunnuksista"
11 | docs_tags: "Luettelo ohjeisiin sisällytettävistä tunnisteista"
12 | docs_add_solved_filter: "Lisää suodattimen ratkaistuihin ketjuihin – edellyttää, että Discourse Solved on asennettu ja käytössä"
13 | show_tags_by_group: "Järjestä tunnisteet käyttämällä tunnisteryhmiä. Luo ryhmiä toisiinsa liittyvien tunnisteiden luokittelemiseksi."
14 | docs_tag_groups: "Tunnisteryhmät, joita käytetään tunnisteiden näyttämiseen ryhmittäin."
15 | docs_add_to_top_menu: "Lisää ylävalikkoon linkin, jolla voi siirtyä ohjenäkymään"
16 | docs_add_search_menu_tip: "Lisää vinkin \"in:docs\" hakuvalikon satunnaisiin vinkkeihin"
17 |
--------------------------------------------------------------------------------
/config/locales/server.nl.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | nl:
8 | site_settings:
9 | docs_enabled: "Documentenplug-in inschakelen"
10 | docs_categories: "Een lijst van categorieslugs om op te nemen in documenten"
11 | docs_tags: "Een lijst van tags om op te nemen in documenten"
12 | docs_add_solved_filter: "Voegt een filter toe voor opgeloste topics -- vereist dat Discourse Opgelost is geïnstalleerd en ingeschakeld"
13 | show_tags_by_group: "Organiseer tags in taggroepen. Maak groepen om gerelateerde tags te categoriseren."
14 | docs_tag_groups: "De taggroepen worden gebruikt om tags per groep weer te geven."
15 | docs_add_to_top_menu: "Voegt een link toe aan het hoofdmenu om naar de Documenten-weergave te gaan"
16 | docs_add_search_menu_tip: "Voegt de tip \"in:docs\" toe aan de willekeurige tips voor het zoekmenu"
17 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/components/docs-category.gjs:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { on } from "@ember/modifier";
3 | import { tagName } from "@ember-decorators/component";
4 | import icon from "discourse/helpers/d-icon";
5 | import discourseComputed from "discourse/lib/decorators";
6 |
7 | @tagName("")
8 | export default class DocsCategory extends Component {
9 | @discourseComputed("category")
10 | categoryName(category) {
11 | return this.site.categories.find((item) => item.id === category.id)?.name;
12 | }
13 |
14 |
15 |
20 | {{icon (if this.category.active "circle-xmark" "far-circle")}}
21 |
22 | {{this.categoryName}}
23 | {{this.category.count}}
26 |
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/config/locales/server.es.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | es:
8 | site_settings:
9 | docs_enabled: "Habilitar el plugin Docs"
10 | docs_categories: "Una lista de slugs de categoría para incluir en los documentos"
11 | docs_tags: "Una lista de etiquetas para incluir en los documentos"
12 | docs_add_solved_filter: "Añade un filtro para temas resueltos: requiere que Discourse Solved esté instalado y habilitado"
13 | show_tags_by_group: "Organiza las etiquetas mediante Grupos de etiquetas. Crea grupos para categorizar etiquetas relacionadas."
14 | docs_tag_groups: "Los Grupos de etiquetas sirven para mostrar las etiquetas por grupos."
15 | docs_add_to_top_menu: "Añade un enlace al menú superior para navegar a la vista Docs"
16 | docs_add_search_menu_tip: "Añade el consejo «in:docs» a los consejos aleatorios del menú de búsqueda"
17 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/templates/docs.gjs:
--------------------------------------------------------------------------------
1 | import RouteTemplate from "ember-route-template";
2 | import PluginOutlet from "discourse/components/plugin-outlet";
3 | import lazyHash from "discourse/helpers/lazy-hash";
4 | import DocsSearch from "../components/docs-search";
5 |
6 | export default RouteTemplate(
7 |
8 |
9 |
10 |
20 |
21 |
22 |
26 |
27 | {{outlet}}
28 |
29 |
30 | );
31 |
--------------------------------------------------------------------------------
/config/locales/server.fr.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | fr:
8 | site_settings:
9 | docs_enabled: "Activer l'extension Docs"
10 | docs_categories: "Une liste de slugs de catégories à inclure dans Docs"
11 | docs_tags: "Une liste d'étiquettes à inclure dans Docs"
12 | docs_add_solved_filter: "Ajoute un filtre pour les sujets résolus - nécessite l'installation et l'activation de Discourse Solved"
13 | show_tags_by_group: "Organisez les étiquettes à l'aide de groupes d'étiquettes. Créez des groupes pour catégoriser les étiquettes associées."
14 | docs_tag_groups: "Les groupes d'étiquettes utilisés pour afficher les étiquettes par groupe."
15 | docs_add_to_top_menu: "Ajoute un lien vers le menu supérieur pour accéder à la vue Docs"
16 | docs_add_search_menu_tip: "Ajoute l'astuce « in:docs » aux astuces aléatoires du menu de recherche"
17 |
--------------------------------------------------------------------------------
/config/locales/server.pt_BR.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | pt_BR:
8 | site_settings:
9 | docs_enabled: "Ativar o plugin de Documentos"
10 | docs_categories: "Uma lista de slugs de categoria para incluir nos documentos"
11 | docs_tags: "Uma lista de etiquetas para incluir nos documentos"
12 | docs_add_solved_filter: "Adiciona um filtro para tópicos resolvidos - requer que Discourse Solved esteja instalado e ativado"
13 | show_tags_by_group: "Organize etiquetas usando Grupos de Etiquetas. Crie grupos para categorizar as etiquetas relacionadas."
14 | docs_tag_groups: "Os Grupos de Etiquetas usados para mostrar etiquetas por grupo."
15 | docs_add_to_top_menu: "Adiciona um link ao menu superior para navegar até a visualização em Documentos"
16 | docs_add_search_menu_tip: "Adiciona a dica \"in:docs\" às dicas aleatórias no menu de pesquisa"
17 |
--------------------------------------------------------------------------------
/config/locales/server.tr_TR.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | tr_TR:
8 | site_settings:
9 | docs_enabled: "Dokümanlar Eklentisini Etkinleştirin"
10 | docs_categories: "Dokümanlara dahil edilecek kategori slug'larının listesi"
11 | docs_tags: "Dokümanlara dahil edilecek etiketlerin listesi"
12 | docs_add_solved_filter: "Çözülen konular için bir filtre ekler -- Discourse Solved'un yüklenmiş ve etkinleştirilmiş olmasını gerektirir"
13 | show_tags_by_group: "Etiket Gruplarını kullanarak etiketleri düzenleyin. İlgili etiketleri kategorilere ayırmak için gruplar oluşturun."
14 | docs_tag_groups: "Etiketleri gruba göre göstermek için kullanılan Etiket Grupları."
15 | docs_add_to_top_menu: "Dokümanlar görünümüne gitmek için üst menüye bir bağlantı ekler"
16 | docs_add_search_menu_tip: "Arama menüsüne rastgele ipuçları için \"in:docs\" ipucunu ekler"
17 |
--------------------------------------------------------------------------------
/config/locales/server.hu.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | hu:
8 | site_settings:
9 | docs_enabled: "Engedélyezze a Dokumentumok bővítményt"
10 | docs_categories: "A dokumentumokban feltüntetendő kategóriák listája"
11 | docs_tags: "A dokumentumokban feltüntetendő címkék listája"
12 | docs_add_solved_filter: "Hozzáad egy szűrőt a megoldott témákhoz -- ehhez telepíteni és engedélyezni kell a Discourse Solved alkalmazást."
13 | show_tags_by_group: "Címkék rendszerezése címkecsoportok segítségével. Csoportok létrehozása a kapcsolódó címkék kategorizálásához."
14 | docs_tag_groups: "A címkecsoportok korábban csoportonként jelenítették meg a címkéket."
15 | docs_add_to_top_menu: "Hozzáad egy hivatkozást a felső menühöz, amely a Dokumentumok nézetre navigál."
16 | docs_add_search_menu_tip: "Hozzáadja az „in:docs” tippet a keresési menü véletlenszerű tippjeihez."
17 |
--------------------------------------------------------------------------------
/config/locales/server.de.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | de:
8 | site_settings:
9 | docs_enabled: "Docs-Plug-in aktivieren"
10 | docs_categories: "Eine Liste von Kategorie-Kürzel, die in Docs aufgenommen werden sollen"
11 | docs_tags: "Eine Liste von Schlagwörtern, die in Docs aufgenommen werden sollen"
12 | docs_add_solved_filter: "Fügt einen Filter für gelöste Themen hinzu – erfordert, dass Discourse Solved installiert und aktiviert ist"
13 | show_tags_by_group: "Organisiere Schlagwörter mit Schlagwortgruppen. Erstelle Gruppen, um verwandte Schlagwörter zu kategorisieren."
14 | docs_tag_groups: "Die Schlagwortgruppen werden verwendet, um Schlagwörter nach Gruppe anzuzeigen."
15 | docs_add_to_top_menu: "Fügt einen Link zum Menü oben hinzu, um zur Ansicht „Docs“ zu navigieren"
16 | docs_add_search_menu_tip: "Fügt den Tipp „in:docs“ zu den Tipps im Suchmenü hinzu"
17 |
--------------------------------------------------------------------------------
/config/locales/server.it.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | it:
8 | site_settings:
9 | docs_enabled: "Abilita il plug-in Documenti"
10 | docs_categories: "Un elenco di abbreviazioni di categoria da includere nei documenti"
11 | docs_tags: "Un elenco di etichette da includere nei documenti"
12 | docs_add_solved_filter: "Aggiunge un filtro per gli argomenti risolti: richiede l'installazione e l'abilitazione di Discourse Solved"
13 | show_tags_by_group: "Organizza le etichette utilizzando i gruppi di etichette. Crea gruppi per classificare le etichette correlate."
14 | docs_tag_groups: "I gruppi di etichette utilizzati per mostrare le etichette per gruppo."
15 | docs_add_to_top_menu: "Aggiunge un collegamento al menu in alto per passare alla visualizzazione Documenti"
16 | docs_add_search_menu_tip: "Aggiunge il suggerimento \"in:docs\" ai suggerimenti casuali del menu di ricerca"
17 |
--------------------------------------------------------------------------------
/db/migrate/20210114161508_rename_knowledge_explorer_settings.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | #
3 | class RenameKnowledgeExplorerSettings < ActiveRecord::Migration[6.0]
4 | def up
5 | execute "UPDATE site_settings SET name = 'docs_enabled' WHERE name = 'knowledge_explorer_enabled'"
6 | execute "UPDATE site_settings SET name = 'docs_categories' WHERE name = 'knowledge_explorer_categories'"
7 | execute "UPDATE site_settings SET name = 'docs_tags' WHERE name = 'knowledge_explorer_tags'"
8 | execute "UPDATE site_settings SET name = 'docs_add_solved_filter' WHERE name = 'knowledge_explorer_add_solved_filter'"
9 | end
10 |
11 | def down
12 | execute "UPDATE site_settings SET name = 'knowledge_explorer_enabled' WHERE name = 'docs_enabled'"
13 | execute "UPDATE site_settings SET name = 'knowledge_explorer_categories' WHERE name = 'docs_categories'"
14 | execute "UPDATE site_settings SET name = 'knowledge_explorer_tags' WHERE name = 'docs_tags'"
15 | execute "UPDATE site_settings SET name = 'knowledge_explorer_add_solved_filter' WHERE name = 'docs_add_solved_filter'"
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Civilized Discourse Construction Kit, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/config/locales/client.zh_CN.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | zh_CN:
8 | js:
9 | filters:
10 | docs:
11 | help: "浏览文档话题"
12 | docs:
13 | title: "文档"
14 | column_titles:
15 | topic: "话题"
16 | activity: "活动"
17 | no_docs:
18 | title: "还没有“文档”话题"
19 | body: "“文档”提供了一种很好的方式来维护文档集合以供共享参考。"
20 | to_include_topic_in_docs: "要在“文档”中添加话题,请使用特殊类别或标签"
21 | setup_the_plugin: "要开始使用“文档”,请设置文档类别和标签。"
22 | categories: "类别"
23 | categories_filter_placeholder: "筛选类别"
24 | tags_filter_placeholder: "筛选标签"
25 | tags: "标签"
26 | search:
27 | results:
28 | other: "找到 %{count} 个结果"
29 | placeholder: "搜索话题"
30 | clear: "清除"
31 | tip_description: "在文档中搜索"
32 | topic:
33 | back: "返回"
34 | navigate_to_topic: "查看关于此话题的讨论"
35 | filter_button: "筛选器"
36 | filter_solved: "话题已解决?"
37 | sidebar:
38 | docs_link_title: "探索文档话题"
39 | docs_link_text: "文档"
40 |
--------------------------------------------------------------------------------
/config/locales/client.en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | js:
3 | filters:
4 | docs:
5 | help: "browse docs topics"
6 | docs:
7 | title: "Docs"
8 | column_titles:
9 | topic: "Topic"
10 | activity: "Activity"
11 | no_docs:
12 | title: "No Docs topics yet"
13 | body: "Docs provides a great way to maintain a collection of documentation for shared reference."
14 | to_include_topic_in_docs: "To include a topic in Docs, use a special category or tag"
15 | setup_the_plugin: "To start using Docs, please, set up docs categories and tags."
16 | categories: "Categories"
17 | categories_filter_placeholder: "Filter categories"
18 | tags_filter_placeholder: "Filter tags"
19 | tags: "Tags"
20 | search:
21 | results:
22 | one: "%{count} result found"
23 | other: "%{count} results found"
24 | placeholder: "Search for topics"
25 | clear: "Clear"
26 | tip_description: "Search in docs"
27 | topic:
28 | back: "Go back"
29 | navigate_to_topic: "View the discussion on this topic"
30 | filter_button: "Filters"
31 | filter_solved: "Topic Solved?"
32 | sidebar:
33 | docs_link_title: "Explore documentation topics"
34 | docs_link_text: "Docs"
35 |
--------------------------------------------------------------------------------
/config/locales/client.ja.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ja:
8 | js:
9 | filters:
10 | docs:
11 | help: "ドキュメントのトピックを閲覧する"
12 | docs:
13 | title: "ドキュメント"
14 | column_titles:
15 | topic: "トピック"
16 | activity: "アクティビティ"
17 | no_docs:
18 | title: "ドキュメントのトピックはまだありません"
19 | body: "ドキュメントは、共有リファレンスの目的でドキュメントのコレクションを管理するのに最適な方法です。"
20 | to_include_topic_in_docs: "ドキュメントにトピックを含めるには、特別なカテゴリまたはタグを使用します"
21 | setup_the_plugin: "ドキュメントを使用するには、ドキュメントのカテゴリとタグを設定してください。"
22 | categories: "カテゴリ"
23 | categories_filter_placeholder: "カテゴリをフィルタリング"
24 | tags_filter_placeholder: "タグをフィルタリング"
25 | tags: "タグ"
26 | search:
27 | results:
28 | other: "%{count} 件の結果が見つかりました"
29 | placeholder: "トピックを検索"
30 | clear: "クリア"
31 | tip_description: "ドキュメント内を検索"
32 | topic:
33 | back: "戻る"
34 | navigate_to_topic: "このトピックに関するディスカッションを表示する"
35 | filter_button: "フィルタ"
36 | filter_solved: "解決済みのトピック?"
37 | sidebar:
38 | docs_link_title: "ドキュメントのトピックを見る"
39 | docs_link_text: "ドキュメント"
40 |
--------------------------------------------------------------------------------
/assets/stylesheets/mobile/docs.scss:
--------------------------------------------------------------------------------
1 | .mobile-view {
2 | .docs {
3 | .docs-search-wrapper {
4 | display: flex;
5 | justify-content: center;
6 | }
7 |
8 | .docs-search {
9 | font-size: $font-up-2;
10 | padding: 0.5em 0;
11 |
12 | .docs-search-bar {
13 | width: calc(100vw - 2em);
14 | }
15 | }
16 |
17 | .docs-browse {
18 | padding-bottom: 60px; // for DiscourseHub footer nav
19 |
20 | .docs-items {
21 | padding-right: 0;
22 | }
23 | flex-direction: column;
24 |
25 | .docs-results .result-count {
26 | padding-left: 0;
27 | }
28 |
29 | .docs-topic-list {
30 | flex-basis: 100%;
31 | }
32 |
33 | .raw-topic-link {
34 | padding-right: 0.25em;
35 | }
36 |
37 | .docs-topic {
38 | width: calc(100vw - 20px);
39 | }
40 | }
41 | }
42 |
43 | .docs-filters {
44 | background: var(--primary-very-low);
45 | padding: 0 0.5em;
46 |
47 | .docs-items:first-of-type {
48 | margin-top: 1em;
49 | }
50 |
51 | + .docs-results {
52 | margin-top: 2em;
53 | }
54 | }
55 |
56 | .archetype-docs-topic {
57 | .docs-filters {
58 | display: none;
59 | }
60 | }
61 |
62 | .docs-expander {
63 | margin: 1em 0 0 0;
64 | width: 100%;
65 | }
66 |
67 | .docs-solved {
68 | .docs-item {
69 | padding: 0.25em 0;
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/components/docs-search.gjs:
--------------------------------------------------------------------------------
1 | import Component, { Input } from "@ember/component";
2 | import { on } from "@ember/modifier";
3 | import { action } from "@ember/object";
4 | import { classNames } from "@ember-decorators/component";
5 | import DButton from "discourse/components/d-button";
6 | import icon from "discourse/helpers/d-icon";
7 | import { i18n } from "discourse-i18n";
8 |
9 | @classNames("docs-search")
10 | export default class DocsSearch extends Component {
11 | @action
12 | onKeyDown(event) {
13 | if (event.key === "Enter") {
14 | this.set("searchTerm", event.target.value);
15 | this.onSearch(event.target.value);
16 | }
17 | }
18 |
19 | @action
20 | clearSearch() {
21 | this.set("searchTerm", "");
22 | this.onSearch("");
23 | }
24 |
25 |
26 |
27 |
36 |
37 | {{#if this.searchTerm}}
38 |
43 | {{else}}
44 | {{icon "magnifying-glass"}}
45 | {{/if}}
46 |
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/config/locales/client.ug.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ug:
8 | js:
9 | filters:
10 | docs:
11 | help: "پۈتۈك تېمىسىغا كۆز يۈگۈرتىدۇ"
12 | docs:
13 | title: "پۈتۈك"
14 | column_titles:
15 | topic: "تېما"
16 | activity: "پائالىيەت"
17 | no_docs:
18 | title: "تېخى پۈتۈك تېمىسى يوق"
19 | body: "پۈتۈك توپلىمىنى ھەمبەھىرلەپ نەقىل ئېلىپ ئاسراشنىڭ ياخشى ئۇسۇلى بىلەن تەمىنلەيدۇ."
20 | to_include_topic_in_docs: "پۈتۈككە تېما كىرگۈزۈشتە، ئالاھىدە سەھىپە ياكى بەلگە ئىشلىتىڭ"
21 | setup_the_plugin: "پۈتۈك ئىشلىتىشنى باشلاشتا، پۈتۈك سەھىپىسى ۋە بەلگە تەڭشەڭ."
22 | categories: "سەھىپە"
23 | categories_filter_placeholder: "سەھىپە سۈزگۈچ"
24 | tags_filter_placeholder: "بەلگە سۈزگۈچ"
25 | tags: "بەلگە"
26 | search:
27 | results:
28 | one: "%{count} نەتىجە تېپىلدى"
29 | other: "%{count} نەتىجە تېپىلدى"
30 | placeholder: "تېما ئىزدە"
31 | clear: "تازىلا"
32 | tip_description: "پۈتۈكتىن ئىزدە"
33 | topic:
34 | back: "قايت"
35 | navigate_to_topic: "بۇ تېمىدىكى سۆھبەتنى كۆرسىتىدۇ"
36 | filter_button: "سۈزگۈچ"
37 | filter_solved: "تېما ھەل قىلىندىمۇ؟"
38 | sidebar:
39 | docs_link_title: "پۈتۈك تېمىلىرى ھەققىدە ئىزدىنىدۇ"
40 | docs_link_text: "پۈتۈك"
41 |
--------------------------------------------------------------------------------
/config/locales/client.he.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | he:
8 | js:
9 | filters:
10 | docs:
11 | help: "עיון בנושאי תיעוד"
12 | docs:
13 | title: "תיעוד"
14 | column_titles:
15 | topic: "נושא"
16 | activity: "פעילות"
17 | no_docs:
18 | title: "אין נושאי תיעוד עדיין"
19 | body: "תיעוד מספקת דרך נהדרת לתחזק אוסף של מסמכים לסימוכין משותפים."
20 | to_include_topic_in_docs: "כדי לכלול נושא בתיעוד, יש להשתמש בקטגוריה מיוחדת או בתגית."
21 | setup_the_plugin: "כדי להתחיל להשתמש בתיעוד, נא להקים קטגוריות ותגיות של תיעוד."
22 | categories: "קטגוריות"
23 | categories_filter_placeholder: "סינון קטגוריות"
24 | tags_filter_placeholder: "סינון תגיות"
25 | tags: "תגיות"
26 | search:
27 | results:
28 | one: "נמצאה תוצאה %{count}"
29 | two: "נמצאו %{count} תוצאות"
30 | many: "נמצאו %{count} תוצאות"
31 | other: "נמצאו %{count} תוצאות"
32 | placeholder: "חיפוש אחר נושאים"
33 | clear: "מחיקה"
34 | tip_description: "חיפוש בתיעוד"
35 | topic:
36 | back: "חזרה אחורה"
37 | navigate_to_topic: "הצגת הדיון בנושא הזה"
38 | filter_button: "מסננים"
39 | filter_solved: "הנושא נפתר?"
40 | sidebar:
41 | docs_link_title: "עיון בנושאי תיעוד"
42 | docs_link_text: "תיעוד"
43 |
--------------------------------------------------------------------------------
/config/locales/client.fi.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | fi:
8 | js:
9 | filters:
10 | docs:
11 | help: "selaa ohjeaiheita"
12 | docs:
13 | title: "Ohjeet"
14 | column_titles:
15 | topic: "Ketju"
16 | activity: "Toiminta"
17 | no_docs:
18 | title: "Ei vielä ohjeketjuja"
19 | body: "Ohjeet tarjoavat erinomaisen tavan ylläpitää ohjekokoelmaa yhteiseksi viitemateriaaliksi."
20 | to_include_topic_in_docs: "Voit lisätä ketjun Ohjeisiin käyttämällä erityistä aluetta tai tunnistetta"
21 | setup_the_plugin: "Aloita Ohjeiden käyttö määrittämällä ohjealueet ja -tunnisteet."
22 | categories: "Alueet"
23 | categories_filter_placeholder: "Suodata alueita"
24 | tags_filter_placeholder: "Suodata tunnisteita"
25 | tags: "Tunnisteet"
26 | search:
27 | results:
28 | one: "%{count} tulos löytyi"
29 | other: "%{count} tulosta löytyi"
30 | placeholder: "Hae ketjuja"
31 | clear: "Tyhjennä"
32 | tip_description: "Hae ohjeista"
33 | topic:
34 | back: "Palaa takaisin"
35 | navigate_to_topic: "Katso tämän ketjun keskustelu"
36 | filter_button: "Suodattimet"
37 | filter_solved: "Onko ketju ratkaistu?"
38 | sidebar:
39 | docs_link_title: "Tutustu ohjeketjuihin"
40 | docs_link_text: "Ohjeet"
41 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/routes/docs/index.js:
--------------------------------------------------------------------------------
1 | import DiscourseRoute from "discourse/routes/discourse";
2 | import { i18n } from "discourse-i18n";
3 | import Docs from "discourse/plugins/discourse-docs/discourse/models/docs";
4 |
5 | export default class DocsIndex extends DiscourseRoute {
6 | queryParams = {
7 | ascending: { refreshModel: true },
8 | filterCategories: { refreshModel: true },
9 | filterTags: { refreshModel: true },
10 | filterSolved: { refreshModel: true },
11 | orderColumn: { refreshModel: true },
12 | selectedTopic: { refreshModel: true },
13 | searchTerm: {
14 | replace: true,
15 | refreshModel: true,
16 | },
17 | };
18 |
19 | model(params) {
20 | this.controllerFor("docs.index").set("isLoading", true);
21 | return Docs.list(params).then((result) => {
22 | this.controllerFor("docs.index").set("isLoading", false);
23 | return result;
24 | });
25 | }
26 |
27 | titleToken() {
28 | const model = this.currentModel;
29 | const pageTitle = i18n("docs.title");
30 | if (model.topic.title && model.topic.category_id) {
31 | const title = model.topic.unicode_title || model.topic.title;
32 | const categoryName = this.site.categories.find(
33 | (item) => item.id === model.topic.category_id
34 | )?.name;
35 | return `${title} - ${categoryName} - ${pageTitle}`;
36 | } else {
37 | return pageTitle;
38 | }
39 | }
40 |
41 | setupController(controller, model) {
42 | controller.set("topic", model.topic);
43 | controller.set("model", model);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/config/locales/client.fa_IR.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | fa_IR:
8 | js:
9 | filters:
10 | docs:
11 | help: "مرور موضوعات اسناد"
12 | docs:
13 | title: "اسناد"
14 | column_titles:
15 | topic: "موضوع"
16 | activity: "فعالیت"
17 | no_docs:
18 | title: "هنوز هیچ موضوعی برای اسناد وجود ندارد"
19 | body: "اسناد یک راه عالی برای نگهداری مجموعهای از مستندات به عنوان مرجع فراهم میکند."
20 | to_include_topic_in_docs: "برای قرار دادن یک موضوع در اسناد، از یک دستهبندی یا برچسب خاص استفاده کنید"
21 | setup_the_plugin: "برای شروع استفاده از اسناد، لطفا دستهبندیها و برچسبهای اسناد را تنظیم کنید."
22 | categories: "دستهبندیها"
23 | categories_filter_placeholder: "فیلتر دستهبندیها"
24 | tags_filter_placeholder: "فیلتر برچسبها"
25 | tags: "برچسبها"
26 | search:
27 | results:
28 | one: "%{count} نتیجه پیدا شد"
29 | other: "%{count} نتیجه پیدا شد"
30 | placeholder: "جستجو برای موضوعات"
31 | clear: "پاک کردن"
32 | tip_description: "جستجو در اسناد"
33 | topic:
34 | back: "بازگشت"
35 | navigate_to_topic: "بحث در مورد این موضوع را مشاهده کنید"
36 | filter_button: "فیلترها"
37 | filter_solved: "موضوع حل شد؟"
38 | sidebar:
39 | docs_link_title: "کاوش در موضوعات اسناد"
40 | docs_link_text: "اسناد"
41 |
--------------------------------------------------------------------------------
/config/locales/client.sv.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | sv:
8 | js:
9 | filters:
10 | docs:
11 | help: "bläddra i docs-ämnen"
12 | docs:
13 | title: "Docs"
14 | column_titles:
15 | topic: "Ämne"
16 | activity: "Aktivitet"
17 | no_docs:
18 | title: "Inga dokument ämnen ännu"
19 | body: "Dokument tillhandahåller ett bra sätt att upprätthålla en samling av dokumentation för delad referens."
20 | to_include_topic_in_docs: "För att inkludera ett ämne under Dokument använder du en särskild kategori eller tagg"
21 | setup_the_plugin: "För att börja använda Dokument, vänligen konfigurera kategorier och taggar."
22 | categories: "Kategorier"
23 | categories_filter_placeholder: "Filtrera kategorier"
24 | tags_filter_placeholder: "Filtrera taggar"
25 | tags: "Taggar"
26 | search:
27 | results:
28 | one: "%{count} resultat hittades"
29 | other: "%{count} resultat hittades"
30 | placeholder: "Sök efter ämnen"
31 | clear: "Rensa"
32 | tip_description: "Sök i dokument"
33 | topic:
34 | back: "Gå tillbaka"
35 | navigate_to_topic: "Se diskussionen om detta ämne"
36 | filter_button: "Filter"
37 | filter_solved: "Ämne löst?"
38 | sidebar:
39 | docs_link_title: "Utforska dokumentationsämnen"
40 | docs_link_text: "Dokument"
41 |
--------------------------------------------------------------------------------
/config/locales/client.es.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | es:
8 | js:
9 | filters:
10 | docs:
11 | help: "examinar temas de documentos"
12 | docs:
13 | title: "Docs"
14 | column_titles:
15 | topic: "Tema"
16 | activity: "Actividad"
17 | no_docs:
18 | title: "Todavía no hay temas en Docs"
19 | body: "Docs proporciona una excelente manera de mantener una colección de documentación para referencia compartida."
20 | to_include_topic_in_docs: "Para incluir un tema en Docs, utiliza una categoría o etiqueta especial"
21 | setup_the_plugin: "Para empezar a utilizar Docs, configura las categorías y etiquetas de Docs."
22 | categories: "Categorías"
23 | categories_filter_placeholder: "Filtrar categorías"
24 | tags_filter_placeholder: "Filtrar etiquetas"
25 | tags: "Etiquetas"
26 | search:
27 | results:
28 | one: "%{count} resultado encontrado"
29 | other: "%{count} resultados encontrados"
30 | placeholder: "Buscar temas"
31 | clear: "Borrar"
32 | tip_description: "Buscar en documentos"
33 | topic:
34 | back: "Volver"
35 | navigate_to_topic: "Ver la discusión sobre este tema"
36 | filter_button: "Filtros"
37 | filter_solved: "¿Tema resuelto?"
38 | sidebar:
39 | docs_link_title: "Explorar los temas de documentación"
40 | docs_link_text: "Docs"
41 |
--------------------------------------------------------------------------------
/config/locales/client.nl.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | nl:
8 | js:
9 | filters:
10 | docs:
11 | help: "Blader door documenttopics"
12 | docs:
13 | title: "Documenten"
14 | column_titles:
15 | topic: "Topic"
16 | activity: "Activiteit"
17 | no_docs:
18 | title: "Nog geen documenttopics"
19 | body: "Documenten bieden een geweldige manier om een verzameling documentatie bij te houden voor gedeelde referentie."
20 | to_include_topic_in_docs: "Gebruik een speciale categorie of tag om een topics op te nemen in Documenten"
21 | setup_the_plugin: "Stel documentcategorieën en tags in om te beginnen met het gebruik van Documenten."
22 | categories: "Categorieën"
23 | categories_filter_placeholder: "Filtercategorieën"
24 | tags_filter_placeholder: "Filtertags"
25 | tags: "Tags"
26 | search:
27 | results:
28 | one: "%{count} resultaat gevonden"
29 | other: "%{count} resultaten gevonden"
30 | placeholder: "Zoek naar topics"
31 | clear: "Wissen"
32 | tip_description: "Zoek in documenten"
33 | topic:
34 | back: "Terug"
35 | navigate_to_topic: "Bekijk de discussie over dit topic"
36 | filter_button: "Filters"
37 | filter_solved: "Topic opgelost?"
38 | sidebar:
39 | docs_link_title: "Verken documentatieonderwerpen"
40 | docs_link_text: "Documenten"
41 |
--------------------------------------------------------------------------------
/config/locales/client.de.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | de:
8 | js:
9 | filters:
10 | docs:
11 | help: "Docs-Themen durchsuchen"
12 | docs:
13 | title: "Docs"
14 | column_titles:
15 | topic: "Thema"
16 | activity: "Aktivität"
17 | no_docs:
18 | title: "Noch keine Docs-Themen"
19 | body: "Docs bietet eine großartige Möglichkeit, eine Sammlung von Dokumentationen zum gemeinsamen Nachschlagen anzulegen."
20 | to_include_topic_in_docs: "Um ein Thema in Docs aufzunehmen, verwende eine spezielle Kategorie oder ein Schlagwort"
21 | setup_the_plugin: "Um Docs zu nutzen, richte bitte Docs-Kategorien und -Schlagwörter ein."
22 | categories: "Kategorien"
23 | categories_filter_placeholder: "Nach Kategorie filtern"
24 | tags_filter_placeholder: "Nach Schlagwort filtern"
25 | tags: "Schlagwörter"
26 | search:
27 | results:
28 | one: "%{count} Ergebnis gefunden"
29 | other: "%{count} Ergebnisse gefunden"
30 | placeholder: "Nach Themen suchen"
31 | clear: "Löschen"
32 | tip_description: "In Docs suchen"
33 | topic:
34 | back: "Zurück"
35 | navigate_to_topic: "Diskussion zu diesem Thema ansehen"
36 | filter_button: "Filter"
37 | filter_solved: "Thema gelöst?"
38 | sidebar:
39 | docs_link_title: "Dokumentationsthemen erkunden"
40 | docs_link_text: "Docs"
41 |
--------------------------------------------------------------------------------
/config/locales/client.tr_TR.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | tr_TR:
8 | js:
9 | filters:
10 | docs:
11 | help: "doküman konularına göz atın"
12 | docs:
13 | title: "Dokümanlar"
14 | column_titles:
15 | topic: "Konu"
16 | activity: "Aktivite"
17 | no_docs:
18 | title: "Henüz Dokümanlar konusu yok"
19 | body: "Dokümanlar, paylaşılan referans için bir dokümantasyon koleksiyonu tutmanın harika bir yolunu sunar."
20 | to_include_topic_in_docs: "Dokümanlar'a bir konu eklemek için özel bir kategori veya etiket kullanın"
21 | setup_the_plugin: "Dokümanlar'ı kullanmaya başlamak için lütfen doküman kategorilerini ve etiketlerini kurun."
22 | categories: "Kategoriler"
23 | categories_filter_placeholder: "Kategorileri filtrele"
24 | tags_filter_placeholder: "Etiketleri filtrele"
25 | tags: "Etiketler"
26 | search:
27 | results:
28 | one: "%{count} sonuç bulundu"
29 | other: "%{count} sonuç bulundu"
30 | placeholder: "Konu ara"
31 | clear: "Temizle"
32 | tip_description: "Dokümanlarda ara"
33 | topic:
34 | back: "Geri dön"
35 | navigate_to_topic: "Bu konuyla ilgili tartışmayı görüntüleyin"
36 | filter_button: "Filtreler"
37 | filter_solved: "Konu Çözüldü mü?"
38 | sidebar:
39 | docs_link_title: "Doküman konularını keşfedin"
40 | docs_link_text: "Dokümanlar"
41 |
--------------------------------------------------------------------------------
/config/locales/client.pt_BR.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | pt_BR:
8 | js:
9 | filters:
10 | docs:
11 | help: "navegar nos tópicos dos documentos"
12 | docs:
13 | title: "Documentos"
14 | column_titles:
15 | topic: "Tópico"
16 | activity: "Atividade"
17 | no_docs:
18 | title: "Nenhum tópico do Docs ainda"
19 | body: "O Docs fornece uma ótima maneira de manter uma coleção de documentação para referência compartilhada."
20 | to_include_topic_in_docs: "Para incluir um tópico no Docs, use uma etiqueta ou categoria especial"
21 | setup_the_plugin: "Para começar a usar o Docs, defina marcadores e categorias de documentos."
22 | categories: "Categorias"
23 | categories_filter_placeholder: "Filtrar categorias"
24 | tags_filter_placeholder: "Filtrar etiquetas"
25 | tags: "Etiquetas"
26 | search:
27 | results:
28 | one: "%{count} resultado encontrado"
29 | other: "%{count} resultados encontrados"
30 | placeholder: "Pesquisar por tópicos"
31 | clear: "Limpar"
32 | tip_description: "Pesquisar em documentos"
33 | topic:
34 | back: "Voltar"
35 | navigate_to_topic: "Veja a discussão neste tópico"
36 | filter_button: "Filtros"
37 | filter_solved: "Tópico resolvido?"
38 | sidebar:
39 | docs_link_title: "Explorar tópicos de documentação"
40 | docs_link_text: "Docs"
41 |
--------------------------------------------------------------------------------
/test/javascripts/unit/controllers/docs-index-test.js:
--------------------------------------------------------------------------------
1 | import { getOwner } from "@ember/owner";
2 | import { setupTest } from "ember-qunit";
3 | import { module, test } from "qunit";
4 |
5 | module("Unit | Controller | docs-index", function (hooks) {
6 | setupTest(hooks);
7 |
8 | test("docsCategories ignores invalid category ids", function (assert) {
9 | const siteSettings = getOwner(this).lookup("service:site-settings");
10 | siteSettings.docs_categories = "1|2|3|333|999";
11 |
12 | const controller = getOwner(this).lookup("controller:docs.index");
13 | assert.deepEqual(controller.docsCategories, ["bug", "feature", "meta"]);
14 | });
15 |
16 | test("updateSelectedTags correctly removes tags", function (assert) {
17 | const controller = getOwner(this).lookup("controller:docs.index");
18 | controller.filterTags = "foo|bar|baz";
19 |
20 | controller.updateSelectedTags({ id: "bar" });
21 |
22 | assert.deepEqual(controller.filterTags, "foo|baz");
23 | });
24 |
25 | test("updateSelectedTags correctly appends tags to list", function (assert) {
26 | const controller = getOwner(this).lookup("controller:docs.index");
27 | controller.filterTags = "foo|bar";
28 |
29 | controller.updateSelectedTags({ id: "baz" });
30 |
31 | assert.deepEqual(controller.filterTags, "foo|bar|baz");
32 | });
33 |
34 | test("updateSelectedTags correctly appends tags to empty list", function (assert) {
35 | const controller = getOwner(this).lookup("controller:docs.index");
36 | controller.filterTags = null;
37 |
38 | controller.updateSelectedTags({ id: "foo" });
39 |
40 | assert.deepEqual(controller.filterTags, "foo");
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/config/locales/client.fr.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | fr:
8 | js:
9 | filters:
10 | docs:
11 | help: "parcourir les sujets de Docs"
12 | docs:
13 | title: "Docs"
14 | column_titles:
15 | topic: "Sujet"
16 | activity: "Activité"
17 | no_docs:
18 | title: "Aucun sujet Docs pour le moment"
19 | body: "Docs fournit un excellent moyen de gérer un ensemble de documents à des fins de référence partagée."
20 | to_include_topic_in_docs: "Pour inclure un sujet dans Docs, utilisez une catégorie ou une étiquette spéciale"
21 | setup_the_plugin: "Pour commencer à utiliser Docs, veuillez configurer des catégories et des étiquettes de documents."
22 | categories: "Catégories"
23 | categories_filter_placeholder: "Filtrer les catégories"
24 | tags_filter_placeholder: "Filtrer les étiquettes"
25 | tags: "Étiquettes"
26 | search:
27 | results:
28 | one: "%{count} résultat trouvé"
29 | other: "%{count} résultats trouvés"
30 | placeholder: "Rechercher des sujets"
31 | clear: "Effacer"
32 | tip_description: "Rechercher dans Docs"
33 | topic:
34 | back: "Revenir en arrière"
35 | navigate_to_topic: "Voir la discussion sur ce sujet"
36 | filter_button: "Filtres"
37 | filter_solved: "Sujet résolu ?"
38 | sidebar:
39 | docs_link_title: "Explorer les sujets de la documentation"
40 | docs_link_text: "Docs"
41 |
--------------------------------------------------------------------------------
/config/locales/client.hu.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | hu:
8 | js:
9 | filters:
10 | docs:
11 | help: "dokumentumok témáinak böngészése"
12 | docs:
13 | title: "Dokumentumok"
14 | column_titles:
15 | topic: "Téma"
16 | activity: "Tevékenység"
17 | no_docs:
18 | title: "Még nincsenek Dokumentum témák"
19 | body: "A Dokumentumok nagyszerű módot kínál arra, hogy közös hivatkozási alapként szolgáló dokumentációgyűjteményt hozzunk létre."
20 | to_include_topic_in_docs: "Téma Dokumentumokban való felvételéhez használjon speciális kategóriát vagy címkét"
21 | setup_the_plugin: "A Dokumentumok használatának megkezdéséhez kérjük, állítsa be a dokumentumok kategóriáit és címkéit."
22 | categories: "Kategóriák"
23 | categories_filter_placeholder: "Kategóriák szűrése"
24 | tags_filter_placeholder: "Címkék szűrése"
25 | tags: "Címkék"
26 | search:
27 | results:
28 | one: "%{count} találat"
29 | other: "%{count} találat"
30 | placeholder: "Témák keresése"
31 | clear: "Törlés"
32 | tip_description: "Keresés a dokumentumokban"
33 | topic:
34 | back: "Visszalépés"
35 | navigate_to_topic: "A témával kapcsolatos beszélgetés megtekintése"
36 | filter_button: "Szűrők:"
37 | filter_solved: "Téma megoldva?"
38 | sidebar:
39 | docs_link_title: "Dokumentációs témák felfedezése"
40 | docs_link_text: "Dokumentumok"
41 |
--------------------------------------------------------------------------------
/config/locales/client.pl_PL.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | pl_PL:
8 | js:
9 | filters:
10 | docs:
11 | help: "przeglądaj tematy dokumentacji"
12 | docs:
13 | title: "Dokumentacja"
14 | column_titles:
15 | topic: "Temat"
16 | activity: "Aktywność"
17 | no_docs:
18 | title: "Brak tematów dokumentów"
19 | body: "Dokumenty to świetny sposób na utrzymywanie zbioru dokumentacji do wspólnego użytku."
20 | to_include_topic_in_docs: "Aby umieścić temat w dokumentach, użyj specjalnej kategorii lub tagu"
21 | setup_the_plugin: "Aby rozpocząć korzystanie z dokumentów, skonfiguruj kategorie i tagi."
22 | categories: "Kategorie"
23 | categories_filter_placeholder: "Filtruj kategorie"
24 | tags_filter_placeholder: "Filtruj tagi"
25 | tags: "Tagi"
26 | search:
27 | results:
28 | one: "Znaleziono %{count} wynik"
29 | few: "Znaleziono %{count} wyniki"
30 | many: "Znaleziono %{count} wyników"
31 | other: "Znaleziono %{count} wyników"
32 | placeholder: "Szukaj tematów"
33 | clear: "Wyczyść"
34 | tip_description: "Szukaj w dokumentach"
35 | topic:
36 | back: "Wróć"
37 | navigate_to_topic: "Zobacz dyskusję na ten temat"
38 | filter_button: "Filtry"
39 | filter_solved: "Temat rozwiązany?"
40 | sidebar:
41 | docs_link_title: "Przeglądaj tematy dokumentacji"
42 | docs_link_text: "Dokumentacja"
43 |
--------------------------------------------------------------------------------
/config/locales/client.it.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | it:
8 | js:
9 | filters:
10 | docs:
11 | help: "sfoglia argomenti dei documenti"
12 | docs:
13 | title: "Documenti"
14 | column_titles:
15 | topic: "Argomento"
16 | activity: "Attività"
17 | no_docs:
18 | title: "Ancora nessun argomento relativo ai Documenti"
19 | body: "I Documenti sono il miglior modo per mantenere una raccolta di documentazione condivisa per la consultazione."
20 | to_include_topic_in_docs: "Per includere un argomento in Documenti, utilizza una categoria o un'etichetta speciale"
21 | setup_the_plugin: "Per iniziare a utilizzare i Documenti, imposta le categorie e le etichette dei documenti."
22 | categories: "Categorie"
23 | categories_filter_placeholder: "Filtra categorie"
24 | tags_filter_placeholder: "Filtra etichette"
25 | tags: "Etichette"
26 | search:
27 | results:
28 | one: "%{count} risultato trovato"
29 | other: "%{count} risultati trovati"
30 | placeholder: "Cerca argomenti"
31 | clear: "Cancella"
32 | tip_description: "Cerca nei documenti"
33 | topic:
34 | back: "Indietro"
35 | navigate_to_topic: "Visualizza discussione su questo argomento"
36 | filter_button: "Filtri"
37 | filter_solved: "Argomento risolto?"
38 | sidebar:
39 | docs_link_title: "Esplora gli argomenti della documentazione"
40 | docs_link_text: "Documenti"
41 |
--------------------------------------------------------------------------------
/config/locales/client.cs.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | cs:
8 | js:
9 | filters:
10 | docs:
11 | help: "procházet témata dokumentace"
12 | docs:
13 | title: "Dokumentace"
14 | column_titles:
15 | topic: "Téma"
16 | activity: "Aktivita"
17 | no_docs:
18 | title: "Zatím neexistují žádná témata dokumentace"
19 | body: "Dokumentace je skvělým způsobem jak udržovat a sdílet sbírku dokumentace"
20 | to_include_topic_in_docs: "Chcete-li zahrnout téma do dokumentace, použijte speciální kategorii nebo štítek"
21 | setup_the_plugin: "Chcete-li začít používat Dokumentaci, nastavte kategorie a štítky dokumentace."
22 | categories: "Kategorie"
23 | categories_filter_placeholder: "Filtrovat kategorie"
24 | tags_filter_placeholder: "Filtrovat štítky"
25 | tags: "Štítky"
26 | search:
27 | results:
28 | one: "Nalezen %{count} výsledek"
29 | few: "Nalezeny %{count} výsledky"
30 | many: "Nalezeno %{count} výsledků"
31 | other: "Nalezeno %{count} výsledků"
32 | placeholder: "Hledejte témata"
33 | clear: "Zrušit"
34 | tip_description: "Hledat v dokumentaci"
35 | topic:
36 | back: "Jít zpět"
37 | navigate_to_topic: "Zobrazit diskuzi o tomto tématu"
38 | filter_button: "Filtry"
39 | filter_solved: "Téma vyřešeno?"
40 | sidebar:
41 | docs_link_title: "Prozkoumat témata dokumentace"
42 | docs_link_text: "Dokumentace"
43 |
--------------------------------------------------------------------------------
/config/locales/client.ru.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ru:
8 | js:
9 | filters:
10 | docs:
11 | help: "Просмотр тем, относящихся к документации"
12 | docs:
13 | title: "Документация"
14 | column_titles:
15 | topic: "Тема"
16 | activity: "Активность"
17 | no_docs:
18 | title: "Документация ещё не создана"
19 | body: "Этот плагин — отличный способ создания коллекции документов для общего доступа."
20 | to_include_topic_in_docs: "Чтобы включить тему в документацию, используйте специальную категорию или тег."
21 | setup_the_plugin: "Чтобы начать использовать документацию, настройте соответствующие категории и теги."
22 | categories: "Разделы"
23 | categories_filter_placeholder: "Фильтр по категориям"
24 | tags_filter_placeholder: "Фильтр по тегам"
25 | tags: "Теги"
26 | search:
27 | results:
28 | one: "Найден %{count} результат"
29 | few: "Найдено %{count} результата"
30 | many: "Найдено %{count} результатов"
31 | other: "Найдено %{count} результатов"
32 | placeholder: "Введите искомое название темы"
33 | clear: "Очистить"
34 | tip_description: "Поиск в документации"
35 | topic:
36 | back: "Вернуться"
37 | navigate_to_topic: "Просмотреть обсуждение этой темы"
38 | filter_button: "Фильтры"
39 | filter_solved: "Вопрос решён?"
40 | sidebar:
41 | docs_link_title: "Просмотр документации"
42 | docs_link_text: "Документация"
43 |
--------------------------------------------------------------------------------
/config/locales/client.ar.yml:
--------------------------------------------------------------------------------
1 | # WARNING: Never edit this file.
2 | # It will be overwritten when translations are pulled from Crowdin.
3 | #
4 | # To work with us on translations, join this project:
5 | # https://translate.discourse.org/
6 |
7 | ar:
8 | js:
9 | filters:
10 | docs:
11 | help: "تصفُّح الموضوعات في المستندات"
12 | docs:
13 | title: "المستندات"
14 | column_titles:
15 | topic: "الموضوع"
16 | activity: "النشاط"
17 | no_docs:
18 | title: "لا توجد موضوعات في المستندات بعد"
19 | body: "توفِّر المستندات طريقة رائعة للاحتفاظ بمجموعة من الوثائق كمرجع مشترك."
20 | to_include_topic_in_docs: "لتضمين موضوع في المستندات، استخدم فئة أو وسمً خاصًا"
21 | setup_the_plugin: "للبدء في استخدام المستندات، يُرجى إعداد فئات ووسوم المستندات."
22 | categories: "الفئات"
23 | categories_filter_placeholder: "تصفية الفئات"
24 | tags_filter_placeholder: "تصفية الوسوم"
25 | tags: "الوسوم"
26 | search:
27 | results:
28 | zero: "تم العثور على %{count} نتيجة"
29 | one: "تم العثور على نتيجة واحدة (%{count})"
30 | two: "تم العثور على نتيجتين (%{count})"
31 | few: "تم العثور على %{count} نتائج"
32 | many: "تم العثور على %{count} نتيجةً"
33 | other: "تم العثور على %{count} نتيجة"
34 | placeholder: "البحث عن الموضوعات"
35 | clear: "مسح"
36 | tip_description: "البحث في Docs"
37 | topic:
38 | back: "العودة"
39 | navigate_to_topic: "عرض المناقشة بشأن هذا الموضوع"
40 | filter_button: "عوامل التصفية"
41 | filter_solved: "هل تم حل الموضوع؟"
42 | sidebar:
43 | docs_link_title: "استكشاف موضوعات التوثيق"
44 | docs_link_text: "المستندات"
45 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/initializers/setup-docs.js:
--------------------------------------------------------------------------------
1 | import { withPluginApi } from "discourse/lib/plugin-api";
2 | import { i18n } from "discourse-i18n";
3 | import { getDocs } from "../../lib/get-docs";
4 |
5 | function initialize(api, container) {
6 | const siteSettings = container.lookup("service:site-settings");
7 | const docsPath = getDocs();
8 |
9 | api.addKeyboardShortcut("g e", "", {
10 | path: "/" + docsPath,
11 | });
12 |
13 | if (siteSettings.docs_add_to_top_menu) {
14 | api.addNavigationBarItem({
15 | name: "docs",
16 | displayName: i18n("docs.title"),
17 | href: "/" + docsPath,
18 | });
19 | }
20 |
21 | api.registerValueTransformer("topic-list-columns", ({ value: columns }) => {
22 | if (container.lookup("service:router").currentRouteName === "docs.index") {
23 | columns.delete("posters");
24 | columns.delete("replies");
25 | columns.delete("views");
26 | }
27 | return columns;
28 | });
29 |
30 | api.registerValueTransformer("topic-list-item-expand-pinned", ({ value }) => {
31 | if (container.lookup("service:router").currentRouteName === "docs.index") {
32 | return true;
33 | }
34 | return value;
35 | });
36 | }
37 |
38 | export default {
39 | name: "setup-docs",
40 |
41 | initialize(container) {
42 | const siteSettings = container.lookup("service:site-settings");
43 |
44 | if (!siteSettings.docs_enabled) {
45 | return;
46 | }
47 |
48 | withPluginApi((api) => initialize(api, container));
49 |
50 | if (siteSettings.docs_add_search_menu_tip) {
51 | withPluginApi((api) => {
52 | api.addSearchSuggestion("in:docs");
53 |
54 | const tip = {
55 | label: "in:docs",
56 | description: i18n("docs.search.tip_description"),
57 | clickable: true,
58 | searchTopics: true,
59 | };
60 | api.addQuickSearchRandomTip(tip);
61 | });
62 | }
63 |
64 | withPluginApi((api) => {
65 | api.addCommunitySectionLink({
66 | name: "docs",
67 | route: "docs.index",
68 | title: i18n("sidebar.docs_link_title"),
69 | text: i18n("sidebar.docs_link_text"),
70 | });
71 | });
72 | },
73 | };
74 |
--------------------------------------------------------------------------------
/test/javascripts/acceptance/docs-user-status-test.js:
--------------------------------------------------------------------------------
1 | import { visit } from "@ember/test-helpers";
2 | import { test } from "qunit";
3 | import {
4 | acceptance,
5 | publishToMessageBus,
6 | } from "discourse/tests/helpers/qunit-helpers";
7 | import docsFixtures from "../fixtures/docs";
8 |
9 | acceptance("Docs - user status", function (needs) {
10 | needs.user();
11 | needs.site({ docs_path: "docs" });
12 | needs.settings({
13 | docs_enabled: true,
14 | enable_user_status: true,
15 | });
16 |
17 | const mentionedUserId = 1;
18 |
19 | needs.pretender((server, helper) => {
20 | server.get("/docs.json", () => {
21 | docsFixtures.topic = {
22 | post_stream: {
23 | posts: [
24 | {
25 | id: 427,
26 | topic_id: 1,
27 | username: "admin1",
28 | post_number: 2,
29 | cooked:
30 | '
This is a document.
\nI am mentioning another user @andrei4
',
31 | mentioned_users: [
32 | {
33 | id: mentionedUserId,
34 | username: "andrei4",
35 | name: "andrei",
36 | avatar_template:
37 | "/letter_avatar_proxy/v4/letter/a/a87d85/{size}.png",
38 | assign_icon: "user-plus",
39 | assign_path: "/u/andrei4/activity/assigned",
40 | },
41 | ],
42 | },
43 | ],
44 | stream: [427],
45 | },
46 | };
47 |
48 | return helper.response(docsFixtures);
49 | });
50 | });
51 |
52 | test("user status on mentions is live", async function (assert) {
53 | await visit("/docs?topic=1");
54 | assert.dom(".mention .user-status").doesNotExist();
55 |
56 | const newStatus = { emoji: "surfing_man", description: "surfing" };
57 | await publishToMessageBus(`/user-status`, { [mentionedUserId]: newStatus });
58 |
59 | assert
60 | .dom(`.mention .user-status-message .emoji[alt='${newStatus.emoji}']`)
61 | .exists();
62 | await publishToMessageBus(`/user-status`, { [mentionedUserId]: null });
63 | assert.dom(".mention .user-status").doesNotExist();
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/test/javascripts/acceptance/docs-sidebar-test.js:
--------------------------------------------------------------------------------
1 | import { click, currentURL, visit } from "@ember/test-helpers";
2 | import { test } from "qunit";
3 | import { cloneJSON } from "discourse/lib/object";
4 | import {
5 | acceptance,
6 | exists,
7 | query,
8 | } from "discourse/tests/helpers/qunit-helpers";
9 | import { i18n } from "discourse-i18n";
10 | import docsFixtures from "../fixtures/docs";
11 |
12 | let DOCS_URL_PATH = "docs";
13 |
14 | acceptance("Docs - Sidebar with docs disabled", function (needs) {
15 | needs.user();
16 | needs.site({ docs_path: DOCS_URL_PATH });
17 | needs.settings({
18 | docs_enabled: false,
19 | navigation_menu: "sidebar",
20 | });
21 |
22 | test("docs sidebar link is hidden", async function (assert) {
23 | await visit("/");
24 |
25 | await click(
26 | ".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
27 | );
28 |
29 | assert.notOk(
30 | exists(".sidebar-section-link[data-link-name='docs']"),
31 | "it does not display the docs link in sidebar"
32 | );
33 | });
34 | });
35 |
36 | acceptance("Docs - Sidebar with docs enabled", function (needs) {
37 | needs.user();
38 | needs.site({ docs_path: DOCS_URL_PATH });
39 | needs.settings({
40 | docs_enabled: true,
41 | navigation_menu: "sidebar",
42 | });
43 |
44 | needs.pretender((server, helper) => {
45 | server.get("/" + DOCS_URL_PATH + ".json", () =>
46 | helper.response(cloneJSON(docsFixtures))
47 | );
48 | });
49 |
50 | test("clicking on docs link", async function (assert) {
51 | await visit("/");
52 |
53 | await click(
54 | ".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
55 | );
56 |
57 | assert.strictEqual(
58 | query(".sidebar-section-link[data-link-name='docs']").textContent.trim(),
59 | i18n("sidebar.docs_link_text"),
60 | "displays the right text for the link"
61 | );
62 |
63 | assert.strictEqual(
64 | query(".sidebar-section-link[data-link-name='docs']").title,
65 | i18n("sidebar.docs_link_title"),
66 | "displays the right title for the link"
67 | );
68 |
69 | await click(".sidebar-section-link[data-link-name='docs']");
70 |
71 | assert.strictEqual(
72 | currentURL(),
73 | "/" + DOCS_URL_PATH,
74 | "it navigates to the right page"
75 | );
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/spec/system/docs_index_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | describe "Discourse Docs | Index", type: :system do
4 | fab!(:category)
5 | fab!(:topic_1) { Fabricate(:topic, category: category) }
6 | fab!(:topic_2) { Fabricate(:topic, category: category) }
7 | fab!(:post_1) { Fabricate(:post, topic: topic_1) }
8 | fab!(:post_2) { Fabricate(:post, topic: topic_2) }
9 |
10 | before do
11 | SiteSetting.docs_enabled = true
12 | SiteSetting.docs_categories = category.id.to_s
13 |
14 | if SiteSetting.respond_to?(:tooltips_enabled)
15 | # Unfortunately this plugin is enabled by default, and it messes with the docs specs
16 | SiteSetting.tooltips_enabled = false
17 | end
18 | end
19 |
20 | it "does not error when showing the index" do
21 | visit("/docs")
22 | expect(page).to have_css(".raw-topic-link", text: topic_1.title)
23 | expect(page).to have_css(".raw-topic-link", text: topic_2.title)
24 | end
25 |
26 | describe "topic excerpts" do
27 | before do
28 | topic_1.update_excerpt(post_1.excerpt_for_topic)
29 | topic_2.update_excerpt(post_2.excerpt_for_topic)
30 | end
31 |
32 | context "when docs_show_topic_excerpts is false" do
33 | before { SiteSetting.always_include_topic_excerpts = false }
34 |
35 | it "does not show the topic excerpts by default" do
36 | visit("/docs")
37 | expect(page).to have_css(".topic-list-item", count: 2)
38 | expect(page).to have_no_css(".topic-excerpt")
39 | end
40 | end
41 |
42 | context "when docs_show_topic_excerpts is true" do
43 | before { SiteSetting.always_include_topic_excerpts = true }
44 |
45 | it "shows the excerpts" do
46 | visit("/docs")
47 | expect(page).to have_css(".topic-excerpt", text: topic_1.excerpt)
48 | expect(page).to have_css(".topic-excerpt", text: topic_2.excerpt)
49 | end
50 | end
51 |
52 | context "when the theme modifier serialize_topic_excerpts is true" do
53 | before do
54 | ThemeModifierSet.find_by(theme_id: SiteSetting.default_theme_id).update!(
55 | serialize_topic_excerpts: true,
56 | )
57 | Theme.clear_cache!
58 | end
59 |
60 | after { Theme.clear_cache! }
61 |
62 | it "shows the excerpts" do
63 | visit("/docs")
64 | expect(page).to have_css(".topic-excerpt", text: topic_1.excerpt)
65 | expect(page).to have_css(".topic-excerpt", text: topic_2.excerpt)
66 | end
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/models/docs.js:
--------------------------------------------------------------------------------
1 | import EmberObject from "@ember/object";
2 | import { ajax } from "discourse/lib/ajax";
3 | import Site from "discourse/models/site";
4 | import Topic from "discourse/models/topic";
5 | import User from "discourse/models/user";
6 | import { getDocs } from "../../lib/get-docs";
7 |
8 | class Docs extends EmberObject {}
9 | const docsPath = getDocs();
10 |
11 | Docs.reopenClass({
12 | list(params) {
13 | let filters = [];
14 |
15 | if (params.filterCategories) {
16 | filters.push(`category=${params.filterCategories}`);
17 | }
18 | if (params.filterTags) {
19 | filters.push(`tags=${params.filterTags}`);
20 | }
21 | if (params.filterSolved) {
22 | filters.push(`solved=${params.filterSolved}`);
23 | }
24 | if (params.searchTerm) {
25 | filters.push(`search=${params.searchTerm}`);
26 | }
27 | if (params.ascending) {
28 | filters.push("ascending=true");
29 | }
30 | if (params.orderColumn) {
31 | filters.push(`order=${params.orderColumn}`);
32 | }
33 | if (params.page) {
34 | filters.push(`page=${params.page}`);
35 | }
36 | if (params.selectedTopic) {
37 | filters.push(`topic=${params.selectedTopic}`);
38 | filters.push("track_visit=true");
39 | }
40 |
41 | return ajax(`/${docsPath}.json?${filters.join("&")}`).then((data) => {
42 | const site = Site.current();
43 | data.categories?.forEach((category) => site.updateCategory(category));
44 | data.topics.topic_list.categories?.forEach((category) =>
45 | site.updateCategory(category)
46 | );
47 | data.topics.topic_list.topics = data.topics.topic_list.topics.map(
48 | (topic) => Topic.create(topic)
49 | );
50 | data.topic = Topic.create(data.topic);
51 | data.topic.post_stream?.posts.forEach((post) =>
52 | this._initUserModels(post)
53 | );
54 | return data;
55 | });
56 | },
57 |
58 | loadMore(loadMoreUrl) {
59 | return ajax(loadMoreUrl).then((data) => {
60 | const site = Site.current();
61 | data.topics.topic_list.categories?.forEach((category) =>
62 | site.updateCategory(category)
63 | );
64 | data.topics.topic_list.topics = data.topics.topic_list.topics.map(
65 | (topic) => Topic.create(topic)
66 | );
67 | return data;
68 | });
69 | },
70 |
71 | _initUserModels(post) {
72 | if (post.mentioned_users) {
73 | post.mentioned_users = post.mentioned_users.map((u) => User.create(u));
74 | }
75 | },
76 | });
77 |
78 | export default Docs;
79 |
--------------------------------------------------------------------------------
/test/javascripts/fixtures/docs.js:
--------------------------------------------------------------------------------
1 | export default {
2 | tags: [
3 | {
4 | id: "something",
5 | count: 74,
6 | active: false,
7 | },
8 | ],
9 | categories: [
10 | {
11 | id: 1,
12 | count: 119,
13 | active: false,
14 | },
15 | ],
16 | topics: {
17 | users: [
18 | {
19 | id: 2,
20 | username: "cvx",
21 | name: "Jarek",
22 | avatar_template: "/letter_avatar/cvx/{size}/2.png",
23 | },
24 | ],
25 | primary_groups: [],
26 | topic_list: {
27 | can_create_topic: true,
28 | draft: null,
29 | draft_key: "new_topic",
30 | draft_sequence: 94,
31 | per_page: 30,
32 | top_tags: ["something"],
33 | topics: [
34 | {
35 | id: 54881,
36 | title: "Importing from Software X",
37 | fancy_title: "Importing from Software X",
38 | slug: "importing-from-software-x",
39 | posts_count: 112,
40 | reply_count: 72,
41 | highest_post_number: 122,
42 | image_url: null,
43 | created_at: "2016-12-28T14:59:29.396Z",
44 | last_posted_at: "2020-11-14T16:21:35.720Z",
45 | bumped: true,
46 | bumped_at: "2020-11-14T16:21:35.720Z",
47 | archetype: "regular",
48 | unseen: false,
49 | pinned: false,
50 | unpinned: null,
51 | visible: true,
52 | closed: false,
53 | archived: false,
54 | bookmarked: null,
55 | liked: null,
56 | tags: ["something"],
57 | views: 15222,
58 | like_count: 167,
59 | has_summary: true,
60 | last_poster_username: "cvx",
61 | category_id: 1,
62 | pinned_globally: false,
63 | featured_link: null,
64 | has_accepted_answer: false,
65 | posters: [
66 | {
67 | extras: null,
68 | description: "Original Poster",
69 | user_id: 2,
70 | primary_group_id: null,
71 | },
72 | {
73 | extras: null,
74 | description: "Frequent Poster",
75 | user_id: 2,
76 | primary_group_id: null,
77 | },
78 | {
79 | extras: "latest",
80 | description: "Most Recent Poster",
81 | user_id: 2,
82 | primary_group_id: null,
83 | },
84 | ],
85 | },
86 | ],
87 | },
88 | load_more_url: "/docs.json?page=1",
89 | },
90 | search_count: null,
91 | };
92 |
--------------------------------------------------------------------------------
/app/controllers/docs/docs_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Docs
4 | class DocsController < ApplicationController
5 | requires_plugin PLUGIN_NAME
6 |
7 | skip_before_action :check_xhr, only: [:index]
8 |
9 | def index
10 | if params[:tags].is_a?(Array) || params[:tags].is_a?(ActionController::Parameters)
11 | raise Discourse::InvalidParameters.new("Only strings are accepted for tag lists")
12 | end
13 |
14 | filters = {
15 | topic: params[:topic],
16 | tags: params[:tags],
17 | category: params[:category],
18 | solved: params[:solved],
19 | search_term: params[:search],
20 | ascending: params[:ascending],
21 | order: params[:order],
22 | page: params[:page],
23 | }
24 |
25 | query = Docs::Query.new(guardian, filters).list
26 |
27 | if filters[:topic].present?
28 | begin
29 | @topic = Topic.find(filters[:topic])
30 | rescue StandardError
31 | raise Discourse::NotFound
32 | end
33 |
34 | @excerpt =
35 | @topic.posts[0].excerpt(500, { strip_links: true, text_entities: true }) if @topic.posts[
36 | 0
37 | ].present?
38 | @excerpt = (@excerpt || "").gsub(/\n/, " ").strip
39 |
40 | query["topic"] = get_topic(@topic, current_user)
41 | end
42 |
43 | respond_to do |format|
44 | format.html do
45 | @title = set_title
46 | render :get_topic
47 | end
48 |
49 | format.json { render json: query }
50 | end
51 | end
52 |
53 | def get_topic(topic, current_user)
54 | return nil unless Docs.topic_in_docs(topic.category_id, topic.tags)
55 |
56 | topic_view = TopicView.new(topic.id, current_user)
57 | guardian = Guardian.new(current_user)
58 |
59 | ip = request.remote_ip
60 | user_id = (current_user.id if current_user)
61 |
62 | TopicsController.defer_track_visit(topic.id, user_id) if should_track_visit_to_topic?
63 | TopicsController.defer_topic_view(topic.id, ip, user_id)
64 |
65 | TopicViewSerializer.new(topic_view, scope: guardian, root: false)
66 | end
67 |
68 | def should_track_visit_to_topic?
69 | !!((!request.format.json? || params[:track_visit]) && current_user)
70 | end
71 |
72 | def set_title
73 | title = "#{I18n.t("js.docs.title")} - #{SiteSetting.title}"
74 | if @topic
75 | topic_title = @topic["unicode_title"] || @topic["title"]
76 | title = "#{topic_title} - #{title}"
77 | end
78 | title
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/components/docs-topic.gjs:
--------------------------------------------------------------------------------
1 | import Component from "@ember/component";
2 | import { reads } from "@ember/object/computed";
3 | import { service } from "@ember/service";
4 | import { htmlSafe } from "@ember/template";
5 | import { classNames } from "@ember-decorators/component";
6 | import DButton from "discourse/components/d-button";
7 | import PluginOutlet from "discourse/components/plugin-outlet";
8 | import Post from "discourse/components/post";
9 | import icon from "discourse/helpers/d-icon";
10 | import discourseDebounce from "discourse/lib/debounce";
11 | import computed, { bind } from "discourse/lib/decorators";
12 | import transformPost from "discourse/lib/transform-post";
13 | import { i18n } from "discourse-i18n";
14 |
15 | @classNames("docs-topic")
16 | export default class DocsTopic extends Component {
17 | @service currentUser;
18 | @service site;
19 |
20 | @reads("post.cooked") originalPostContent;
21 |
22 | @computed("currentUser", "model")
23 | post() {
24 | return transformPost(this.currentUser, this.site, this.model);
25 | }
26 |
27 | @computed("topic", "topic.post_stream")
28 | model() {
29 | const post = this.store.createRecord(
30 | "post",
31 | this.topic.post_stream?.posts[0]
32 | );
33 |
34 | if (!post.topic) {
35 | post.set("topic", this.topic);
36 | }
37 |
38 | return post;
39 | }
40 |
41 | @bind
42 | _emitScrollEvent() {
43 | this.appEvents.trigger("docs-topic:current-post-scrolled");
44 | }
45 |
46 | @bind
47 | debounceScrollEvent() {
48 | discourseDebounce(this, this._emitScrollEvent, 200);
49 | }
50 |
51 | didInsertElement() {
52 | super.didInsertElement(...arguments);
53 |
54 | document.body.classList.add("archetype-docs-topic");
55 | document.addEventListener("scroll", this.debounceScrollEvent);
56 | }
57 |
58 | willDestroyElement() {
59 | super.willDestroyElement(...arguments);
60 |
61 | document.body.classList.remove("archetype-docs-topic");
62 | document.removeEventListener("scroll", this.debounceScrollEvent);
63 | }
64 |
65 |
66 |
71 |
72 |
73 |
{{htmlSafe this.topic.fancyTitle}}
74 |
75 |
76 |
77 |
78 | {{icon "far-comment"}}
79 | {{i18n "docs.topic.navigate_to_topic"}}
80 |
81 |
82 |
83 |
84 |
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/spec/plugin_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe Docs do
6 | fab!(:category)
7 | fab!(:topic) { Fabricate(:topic, category: category) }
8 | fab!(:post) { Fabricate(:post, topic: topic) }
9 | fab!(:non_docs_category, :category)
10 | fab!(:non_docs_topic) { Fabricate(:topic, category: non_docs_category) }
11 | fab!(:non_docs_post) { Fabricate(:post, topic: non_docs_topic) }
12 |
13 | before do
14 | SiteSetting.docs_enabled = true
15 | SiteSetting.docs_categories = category.id.to_s
16 | GlobalSetting.stubs(:docs_path).returns("docs")
17 | end
18 |
19 | describe "docs oneboxes" do
20 | let(:docs_list_url) { "#{Discourse.base_url}/#{GlobalSetting.docs_path}" }
21 | let(:docs_topic_url) { "#{Discourse.base_url}/#{GlobalSetting.docs_path}?topic=#{topic.id}" }
22 | let(:non_docs_topic_url) do
23 | "#{Discourse.base_url}/#{GlobalSetting.docs_path}?topic=#{non_docs_topic.id}"
24 | end
25 |
26 | context "when inline" do
27 | it "renders docs list" do
28 | results = InlineOneboxer.new([docs_list_url], skip_cache: true).process
29 | expect(results).to be_present
30 | expect(results[0][:url]).to eq(docs_list_url)
31 | expect(results[0][:title]).to eq(I18n.t("js.docs.title"))
32 | end
33 |
34 | it "renders docs topic" do
35 | results = InlineOneboxer.new([docs_topic_url], skip_cache: true).process
36 | expect(results).to be_present
37 | expect(results[0][:url]).to eq(docs_topic_url)
38 | expect(results[0][:title]).to eq(topic.title)
39 | end
40 |
41 | it "does not render topic if not in docs" do
42 | results = InlineOneboxer.new([non_docs_topic_url], skip_cache: true).process
43 | expect(results).to be_empty
44 | end
45 | end
46 |
47 | context "when regular" do
48 | it "renders docs list" do
49 | onebox = Oneboxer.preview(docs_list_url)
50 | expect(onebox).to match_html <<~HTML
51 |
58 | HTML
59 | end
60 |
61 | it "renders docs topic" do
62 | onebox = Oneboxer.preview(docs_topic_url)
63 | expect(onebox).to include(%{data-topic="#{topic.id}">})
64 | expect(onebox).to include(PrettyText.avatar_img(post.user.avatar_template_url, "tiny"))
65 | expect(onebox).to include(%{#{topic.title}})
66 | expect(onebox).to include(post.excerpt)
67 | end
68 |
69 | it "does not onebox topic if not in docs" do
70 | onebox = Oneboxer.preview(non_docs_topic_url)
71 | expect(onebox).to eq(%{#{non_docs_topic_url}})
72 | end
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | activesupport (8.0.3)
5 | base64
6 | benchmark (>= 0.3)
7 | bigdecimal
8 | concurrent-ruby (~> 1.0, >= 1.3.1)
9 | connection_pool (>= 2.2.5)
10 | drb
11 | i18n (>= 1.6, < 2)
12 | logger (>= 1.4.2)
13 | minitest (>= 5.1)
14 | securerandom (>= 0.3)
15 | tzinfo (~> 2.0, >= 2.0.5)
16 | uri (>= 0.13.1)
17 | ast (2.4.3)
18 | base64 (0.3.0)
19 | benchmark (0.4.1)
20 | bigdecimal (3.3.0)
21 | concurrent-ruby (1.3.5)
22 | connection_pool (2.5.4)
23 | drb (2.2.3)
24 | i18n (1.14.7)
25 | concurrent-ruby (~> 1.0)
26 | json (2.15.1)
27 | language_server-protocol (3.17.0.5)
28 | lint_roller (1.1.0)
29 | logger (1.7.0)
30 | minitest (5.26.0)
31 | parallel (1.27.0)
32 | parser (3.3.9.0)
33 | ast (~> 2.4.1)
34 | racc
35 | prettier_print (1.2.1)
36 | prism (1.5.1)
37 | racc (1.8.1)
38 | rack (3.2.3)
39 | rainbow (3.1.1)
40 | regexp_parser (2.11.3)
41 | rubocop (1.81.1)
42 | json (~> 2.3)
43 | language_server-protocol (~> 3.17.0.2)
44 | lint_roller (~> 1.1.0)
45 | parallel (~> 1.10)
46 | parser (>= 3.3.0.2)
47 | rainbow (>= 2.2.2, < 4.0)
48 | regexp_parser (>= 2.9.3, < 3.0)
49 | rubocop-ast (>= 1.47.1, < 2.0)
50 | ruby-progressbar (~> 1.7)
51 | unicode-display_width (>= 2.4.0, < 4.0)
52 | rubocop-ast (1.47.1)
53 | parser (>= 3.3.7.2)
54 | prism (~> 1.4)
55 | rubocop-capybara (2.22.1)
56 | lint_roller (~> 1.1)
57 | rubocop (~> 1.72, >= 1.72.1)
58 | rubocop-discourse (3.13.3)
59 | activesupport (>= 6.1)
60 | lint_roller (>= 1.1.0)
61 | rubocop (>= 1.73.2)
62 | rubocop-capybara (>= 2.22.0)
63 | rubocop-factory_bot (>= 2.27.0)
64 | rubocop-rails (>= 2.30.3)
65 | rubocop-rspec (>= 3.0.1)
66 | rubocop-rspec_rails (>= 2.31.0)
67 | rubocop-factory_bot (2.27.1)
68 | lint_roller (~> 1.1)
69 | rubocop (~> 1.72, >= 1.72.1)
70 | rubocop-rails (2.33.4)
71 | activesupport (>= 4.2.0)
72 | lint_roller (~> 1.1)
73 | rack (>= 1.1)
74 | rubocop (>= 1.75.0, < 2.0)
75 | rubocop-ast (>= 1.44.0, < 2.0)
76 | rubocop-rspec (3.7.0)
77 | lint_roller (~> 1.1)
78 | rubocop (~> 1.72, >= 1.72.1)
79 | rubocop-rspec_rails (2.31.0)
80 | lint_roller (~> 1.1)
81 | rubocop (~> 1.72, >= 1.72.1)
82 | rubocop-rspec (~> 3.5)
83 | ruby-progressbar (1.13.0)
84 | securerandom (0.4.1)
85 | syntax_tree (6.3.0)
86 | prettier_print (>= 1.2.0)
87 | tzinfo (2.0.6)
88 | concurrent-ruby (~> 1.0)
89 | unicode-display_width (3.2.0)
90 | unicode-emoji (~> 4.1)
91 | unicode-emoji (4.1.0)
92 | uri (1.0.4)
93 |
94 | PLATFORMS
95 | ruby
96 |
97 | DEPENDENCIES
98 | rubocop-discourse
99 | syntax_tree
100 |
101 | BUNDLED WITH
102 | 2.7.2
103 |
--------------------------------------------------------------------------------
/test/javascripts/fixtures/docs-show-tag-groups.js:
--------------------------------------------------------------------------------
1 | export default {
2 | tag_groups: [
3 | {
4 | id: 1,
5 | name: "my-tag-group-1",
6 | tags: [
7 | {
8 | id: "something 1",
9 | count: 50,
10 | active: true,
11 | },
12 | {
13 | id: "something 2",
14 | count: 10,
15 | active: true,
16 | },
17 | ],
18 | },
19 | {
20 | id: 2,
21 | name: "my-tag-group-2",
22 | tags: [
23 | {
24 | id: "something 2",
25 | count: 10,
26 | active: true,
27 | },
28 | ],
29 | },
30 | {
31 | id: 3,
32 | name: "my-tag-group-3",
33 | tags: [
34 | {
35 | id: "something 3",
36 | count: 1,
37 | active: false,
38 | },
39 | ],
40 | },
41 | {
42 | id: 4,
43 | name: "my-tag-group-4",
44 | tags: [
45 | {
46 | id: "something 4",
47 | count: 1,
48 | active: false,
49 | },
50 | ],
51 | },
52 | ],
53 | categories: [
54 | {
55 | id: 1,
56 | count: 119,
57 | active: false,
58 | },
59 | ],
60 | topics: {
61 | users: [
62 | {
63 | id: 2,
64 | username: "cvx",
65 | name: "Jarek",
66 | avatar_template: "/letter_avatar/cvx/{size}/2.png",
67 | },
68 | ],
69 | primary_groups: [],
70 | topic_list: {
71 | can_create_topic: true,
72 | draft: null,
73 | draft_key: "new_topic",
74 | draft_sequence: 94,
75 | per_page: 30,
76 | top_tags: ["something"],
77 | topics: [
78 | {
79 | id: 54881,
80 | title: "Importing from Software X",
81 | fancy_title: "Importing from Software X",
82 | slug: "importing-from-software-x",
83 | posts_count: 112,
84 | reply_count: 72,
85 | highest_post_number: 122,
86 | image_url: null,
87 | created_at: "2016-12-28T14:59:29.396Z",
88 | last_posted_at: "2020-11-14T16:21:35.720Z",
89 | bumped: true,
90 | bumped_at: "2020-11-14T16:21:35.720Z",
91 | archetype: "regular",
92 | unseen: false,
93 | pinned: false,
94 | unpinned: null,
95 | visible: true,
96 | closed: false,
97 | archived: false,
98 | bookmarked: null,
99 | liked: null,
100 | tags: ["something"],
101 | views: 15222,
102 | like_count: 167,
103 | has_summary: true,
104 | last_poster_username: "cvx",
105 | category_id: 1,
106 | pinned_globally: false,
107 | featured_link: null,
108 | has_accepted_answer: false,
109 | posters: [
110 | {
111 | extras: null,
112 | description: "Original Poster",
113 | user_id: 2,
114 | primary_group_id: null,
115 | },
116 | {
117 | extras: null,
118 | description: "Frequent Poster",
119 | user_id: 2,
120 | primary_group_id: null,
121 | },
122 | {
123 | extras: "latest",
124 | description: "Most Recent Poster",
125 | user_id: 2,
126 | primary_group_id: null,
127 | },
128 | ],
129 | },
130 | ],
131 | },
132 | load_more_url: "/docs.json?page=1",
133 | },
134 | search_count: null,
135 | };
136 |
--------------------------------------------------------------------------------
/plugin.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # name: discourse-docs
4 | # about: Provides the ability to find and filter knowledge base topics.
5 | # meta_topic_id: 130172
6 | # version: 0.1
7 | # author: Justin DiRose
8 | # url: https://github.com/discourse/discourse-docs
9 |
10 | enabled_site_setting :docs_enabled
11 |
12 | register_asset "stylesheets/common/docs.scss"
13 | register_asset "stylesheets/mobile/docs.scss"
14 |
15 | register_svg_icon "arrow-down-a-z"
16 | register_svg_icon "arrow-up-a-z"
17 | register_svg_icon "arrow-up-1-9"
18 | register_svg_icon "arrow-down-1-9"
19 | register_svg_icon "far-circle"
20 |
21 | require_relative "lib/docs/engine"
22 | require_relative "lib/docs/query"
23 |
24 | GlobalSetting.add_default :docs_path, "docs"
25 |
26 | module ::Docs
27 | PLUGIN_NAME = "discourse-docs"
28 | end
29 |
30 | after_initialize do
31 | require_dependency "search"
32 |
33 | if SiteSetting.docs_enabled
34 | if Search.respond_to? :advanced_filter
35 | Search.advanced_filter(/in:(kb|docs)/) do |posts|
36 | selected_categories = SiteSetting.docs_categories.split("|")
37 | if selected_categories
38 | categories = Category.where("id IN (?)", selected_categories).pluck(:id)
39 | end
40 |
41 | selected_tags = SiteSetting.docs_tags.split("|")
42 | tags = Tag.where("name IN (?)", selected_tags).pluck(:id) if selected_tags
43 |
44 | posts.where(
45 | "category_id IN (?) OR topics.id IN (SELECT DISTINCT(tt.topic_id) FROM topic_tags tt WHERE tt.tag_id IN (?))",
46 | categories,
47 | tags,
48 | )
49 | end
50 | end
51 | end
52 |
53 | if Oneboxer.respond_to?(:register_local_handler)
54 | Oneboxer.register_local_handler("docs/docs") do |url, route|
55 | uri = URI(url)
56 | query = URI.decode_www_form(uri.query).to_h if uri.query
57 |
58 | if query && query["topic"]
59 | topic = Topic.includes(:tags).find_by(id: query["topic"])
60 | if Docs.topic_in_docs(topic.category_id, topic.tags) && Guardian.new.can_see_topic?(topic)
61 | first_post = topic.ordered_posts.first
62 | args = {
63 | topic_id: topic.id,
64 | post_number: first_post.post_number,
65 | avatar: PrettyText.avatar_img(first_post.user.avatar_template_url, "tiny"),
66 | original_url: url,
67 | title: PrettyText.unescape_emoji(CGI.escapeHTML(topic.title)),
68 | category_html: CategoryBadge.html_for(topic.category),
69 | quote:
70 | PrettyText.unescape_emoji(
71 | first_post.excerpt(SiteSetting.post_onebox_maxlength, keep_svg: true),
72 | ),
73 | }
74 |
75 | template = Oneboxer.template("discourse_topic_onebox")
76 | Mustache.render(template, args)
77 | end
78 | else
79 | args = { url: url, name: I18n.t("js.docs.title") }
80 | Mustache.render(Docs.onebox_template, args)
81 | end
82 | end
83 | end
84 |
85 | if InlineOneboxer.respond_to?(:register_local_handler)
86 | InlineOneboxer.register_local_handler("docs/docs") do |url, route|
87 | uri = URI(url)
88 | query = URI.decode_www_form(uri.query).to_h if uri.query
89 |
90 | if query && query["topic"]
91 | topic = Topic.includes(:tags).find_by(id: query["topic"])
92 | if Docs.topic_in_docs(topic.category_id, topic.tags) && Guardian.new.can_see_topic?(topic)
93 | { url: url, title: topic.title }
94 | end
95 | else
96 | { url: url, title: I18n.t("js.docs.title") }
97 | end
98 | end
99 | end
100 |
101 | add_to_class(:topic_query, :list_docs_topics) { default_results(@options) }
102 |
103 | on(:robots_info) do |robots_info|
104 | robots_info[:agents] ||= []
105 |
106 | any_user_agent = robots_info[:agents].find { |info| info[:name] == "*" }
107 | if !any_user_agent
108 | any_user_agent = { name: "*" }
109 | robots_info[:agents] << any_user_agent
110 | end
111 |
112 | any_user_agent[:disallow] ||= []
113 | any_user_agent[:disallow] << "/#{GlobalSetting.docs_path}/"
114 | end
115 |
116 | add_to_serializer(:site, :docs_path) { GlobalSetting.docs_path }
117 | end
118 |
--------------------------------------------------------------------------------
/test/javascripts/acceptance/docs-test.js:
--------------------------------------------------------------------------------
1 | import { click, visit } from "@ember/test-helpers";
2 | import { test } from "qunit";
3 | import {
4 | acceptance,
5 | count,
6 | exists,
7 | query,
8 | } from "discourse/tests/helpers/qunit-helpers";
9 | import docsFixtures from "../fixtures/docs";
10 | import docsShowTagGroupsFixtures from "../fixtures/docs-show-tag-groups";
11 |
12 | let DOCS_URL_PATH = "docs";
13 |
14 | acceptance("Docs", function (needs) {
15 | needs.user();
16 | needs.site({ docs_path: DOCS_URL_PATH });
17 | needs.settings({
18 | docs_enabled: true,
19 | });
20 |
21 | needs.pretender((server, helper) => {
22 | server.get("/" + DOCS_URL_PATH + ".json", (request) => {
23 | if (request.queryParams.category === "1") {
24 | const fixture = JSON.parse(JSON.stringify(docsFixtures));
25 |
26 | return helper.response(
27 | Object.assign(fixture, {
28 | categories: [
29 | {
30 | id: 1,
31 | count: 119,
32 | active: true,
33 | },
34 | ],
35 | })
36 | );
37 | } else {
38 | return helper.response(docsFixtures);
39 | }
40 | });
41 | });
42 |
43 | test("index page", async function (assert) {
44 | this.siteSettings.tagging_enabled = true;
45 |
46 | await visit("/");
47 |
48 | await click(
49 | ".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
50 | );
51 |
52 | await click(
53 | ".sidebar-section[data-section-name='community'] .sidebar-section-link[data-link-name='docs']"
54 | );
55 |
56 | assert.equal(query(".docs-category .docs-item-id").innerText, "bug");
57 | assert.equal(query(".docs-category .docs-item-count").innerText, "119");
58 | assert.equal(query(".docs-tag .docs-item-id").innerText, "something");
59 | assert.equal(query(".docs-tag .docs-item-count").innerText, "74");
60 | assert.dom(".raw-topic-link").hasText("Importing from Software X");
61 | });
62 |
63 | test("selecting a category", async function (assert) {
64 | await visit("/" + DOCS_URL_PATH);
65 | assert.equal(count(".docs-category.selected"), 0);
66 |
67 | await click(".docs-item.docs-category");
68 | assert.equal(count(".docs-category.selected"), 1);
69 |
70 | await click(".docs-item.docs-category");
71 | assert.equal(
72 | count(".docs-category.selected"),
73 | 0,
74 | "clicking again deselects"
75 | );
76 | });
77 | });
78 |
79 | acceptance("Docs - with tag groups enabled", function (needs) {
80 | needs.user();
81 | needs.site({ docs_path: DOCS_URL_PATH });
82 | needs.settings({
83 | docs_enabled: true,
84 | });
85 |
86 | function getRootElementText(selector) {
87 | return Array.from(query(selector).childNodes)
88 | .filter((node) => node.nodeType === Node.TEXT_NODE)
89 | .map((node) => node.textContent.trim())
90 | .join("");
91 | }
92 |
93 | function assertTagGroup(assert, tagGroup) {
94 | let groupTagSelector = `.docs-filter-tag-group-${tagGroup.id}`;
95 | assert.equal(
96 | getRootElementText(groupTagSelector),
97 | tagGroup.expectedTagGroupName
98 | );
99 | assert.equal(
100 | query(`${groupTagSelector} .docs-tag .docs-item-id`).innerText,
101 | tagGroup.expectedTagName
102 | );
103 | assert.equal(
104 | query(`${groupTagSelector} .docs-tag .docs-item-count`).innerText,
105 | tagGroup.expectedCount
106 | );
107 | }
108 |
109 | needs.pretender((server, helper) => {
110 | server.get("/" + DOCS_URL_PATH + ".json", () => {
111 | return helper.response(docsShowTagGroupsFixtures);
112 | });
113 | });
114 |
115 | test("Show tag groups", async function (assert) {
116 | this.siteSettings.tagging_enabled = true;
117 | this.siteSettings.show_tags_by_group = true;
118 | this.siteSettings.docs_tag_groups =
119 | "my-tag-group-1|my-tag-group-2|my-tag-group-3";
120 |
121 | await visit("/");
122 |
123 | await click(
124 | ".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
125 | );
126 |
127 | await click(
128 | ".sidebar-section[data-section-name='community'] .sidebar-section-link[data-link-name='docs']"
129 | );
130 |
131 | assert.equal(query(".docs-category .docs-item-id").innerText, "bug");
132 | assert.equal(query(".docs-category .docs-item-count").innerText, "119");
133 |
134 | const expectedTagGroups = [
135 | {
136 | id: "1",
137 | expectedTagGroupName: "my-tag-group-1",
138 | expectedTagName: "something 1",
139 | expectedCount: "50",
140 | },
141 | {
142 | id: "2",
143 | expectedTagGroupName: "my-tag-group-2",
144 | expectedTagName: "something 2",
145 | expectedCount: "10",
146 | },
147 | {
148 | id: "3",
149 | expectedTagGroupName: "my-tag-group-3",
150 | expectedTagName: "something 3",
151 | expectedCount: "1",
152 | },
153 | ];
154 |
155 | for (let tagGroup of expectedTagGroups) {
156 | assertTagGroup(assert, tagGroup);
157 | }
158 | });
159 | });
160 | acceptance("Docs - empty state", function (needs) {
161 | needs.user();
162 | needs.site({ docs_path: DOCS_URL_PATH });
163 | needs.settings({
164 | docs_enabled: true,
165 | });
166 |
167 | needs.pretender((server, helper) => {
168 | server.get("/" + DOCS_URL_PATH + ".json", () => {
169 | const response = {
170 | tags: [],
171 | categories: [],
172 | topics: {
173 | topic_list: {
174 | can_create_topic: true,
175 | per_page: 30,
176 | top_tags: [],
177 | topics: [],
178 | },
179 | load_more_url: null,
180 | },
181 | topic_count: 0,
182 | };
183 |
184 | return helper.response(response);
185 | });
186 | });
187 |
188 | test("shows the empty state panel when there are no docs", async function (assert) {
189 | await visit("/" + DOCS_URL_PATH);
190 | assert.ok(exists("div.empty-state"));
191 | });
192 | });
193 |
--------------------------------------------------------------------------------
/assets/stylesheets/common/docs.scss:
--------------------------------------------------------------------------------
1 | @use "lib/viewport";
2 |
3 | .docs-search-wrapper {
4 | position: relative;
5 | width: 500px;
6 |
7 | .d-icon {
8 | position: absolute;
9 | right: 0.75em;
10 | top: 25%;
11 | font-size: 1.5em;
12 | color: var(--primary-low-mid);
13 | pointer-events: none;
14 |
15 | @media screen and (width <= 400px) {
16 | // Just decoration, remove on small screens
17 | display: none;
18 | }
19 | }
20 |
21 | .btn.clear-search {
22 | background-color: var(--secondary);
23 | color: var(--tertiary);
24 | font-size: 0.75em;
25 | position: absolute;
26 | right: 0.8em;
27 | text-transform: lowercase;
28 | top: 20%;
29 | }
30 | }
31 |
32 | .docs-search {
33 | align-items: center;
34 | background-color: var(--primary-very-low);
35 | display: flex;
36 | justify-content: center;
37 | padding: 1.5em 1em;
38 |
39 | @include viewport.from(sm) {
40 | // More breathing room on larger screens
41 | margin-bottom: 2em;
42 | }
43 |
44 | .docs-search-bar {
45 | height: 50px;
46 | margin-bottom: 0;
47 | width: 100%;
48 | }
49 | }
50 |
51 | .docs-browse {
52 | display: flex;
53 |
54 | // TODO: Remove once legacy topic-list is removed
55 | .topic-list-data.replies,
56 | .topic-list-data.posts,
57 | .topic-list-data.views {
58 | display: none;
59 | }
60 |
61 | .loading-container {
62 | display: flex;
63 | flex-basis: 100%;
64 | padding: 0.625em 0;
65 | }
66 |
67 | .docs-results {
68 | display: flex;
69 | flex-direction: column;
70 | flex-basis: 100%;
71 |
72 | .result-count {
73 | padding-top: 15px;
74 | padding-left: 0.625em;
75 | }
76 | }
77 |
78 | .docs-filters {
79 | flex: 0 1 20%;
80 |
81 | // min-width on flex allows container to
82 | // be more narrow than content if needed
83 | min-width: 200px;
84 |
85 | @include viewport.from(md) {
86 | padding-right: 2em;
87 | }
88 | }
89 |
90 | .docs-items {
91 | padding: 0.57em 0 1.5em 0;
92 |
93 | a {
94 | color: var(--primary);
95 | white-space: nowrap;
96 | }
97 |
98 | h3 {
99 | font-size: $font-up-1;
100 | }
101 |
102 | .docs-item-count {
103 | margin-left: auto;
104 | color: var(--primary-high);
105 | font-size: $font-down-1;
106 | }
107 |
108 | .docs-item {
109 | display: flex;
110 | align-items: center;
111 | cursor: pointer;
112 | padding: 0.25em 0.5em;
113 |
114 | .d-icon {
115 | height: 1em;
116 | margin-right: 0.25em;
117 | color: var(--primary-high);
118 |
119 | &.d-icon-plus {
120 | height: 0.75em;
121 | margin-right: 0.25em;
122 | }
123 | }
124 |
125 | &.selected .d-icon {
126 | color: var(--primary);
127 | }
128 |
129 | &:hover {
130 | background: var(--highlight-medium);
131 | }
132 |
133 | &.selected:hover {
134 | background: var(--danger-low);
135 |
136 | .d-icon {
137 | color: var(--danger);
138 | }
139 | }
140 |
141 | .tag-id,
142 | .category-id {
143 | margin-right: 3px;
144 | overflow: hidden;
145 | text-overflow: ellipsis;
146 | }
147 | }
148 |
149 | .selected {
150 | font-weight: bold;
151 | }
152 | }
153 |
154 | .docs-topic-list {
155 | flex-basis: 100%;
156 |
157 | .topic-list-header .topic-list-data {
158 | min-width: 5em;
159 |
160 | &[role="button"] {
161 | cursor: pointer;
162 | }
163 |
164 | &:hover {
165 | background-color: var(--primary-low);
166 | }
167 |
168 | .d-icon {
169 | vertical-align: middle;
170 | }
171 | }
172 |
173 | .topic-list-data:last-of-type {
174 | text-align: center;
175 | }
176 |
177 | .badge-wrapper .badge-category .category-name {
178 | // extra protection for ultra-long category names
179 | max-width: 30vw;
180 | }
181 |
182 | .discourse-tags {
183 | font-weight: normal;
184 | font-size: $font-down-1;
185 | }
186 |
187 | .raw-topic-link {
188 | color: var(--tertiary);
189 | cursor: pointer;
190 | display: inline-block;
191 | word-break: break-word;
192 |
193 | & > * {
194 | pointer-events: none;
195 | }
196 | }
197 | }
198 |
199 | .docs-topic {
200 | display: flex;
201 | flex-direction: column;
202 |
203 | .docs-nav-link {
204 | font-weight: 700;
205 |
206 | &.return {
207 | align-items: center;
208 | background: none;
209 | color: var(--tertiary);
210 | display: inline-flex;
211 | font-size: $font-0;
212 | justify-content: normal;
213 | padding: 0;
214 |
215 | &::before {
216 | content: "«";
217 | margin-right: 5px;
218 | }
219 | }
220 |
221 | &.more {
222 | font-size: $font-up-1;
223 | padding: 10px 0;
224 | }
225 | }
226 |
227 | .topic-content {
228 | padding-top: 10px;
229 |
230 | h1 {
231 | line-height: $line-height-medium;
232 | }
233 |
234 | .lightbox-wrapper img {
235 | max-width: 100%;
236 | }
237 |
238 | code,
239 | pre {
240 | // Prevent pre from being wider than viewport
241 | white-space: pre-wrap;
242 | word-break: break-word;
243 | }
244 | }
245 |
246 | #share-link .reply-as-new-topic {
247 | display: none;
248 | }
249 |
250 | .post-info.edits {
251 | display: none;
252 | }
253 | }
254 | }
255 |
256 | .docs-items {
257 | .item-controls {
258 | display: flex;
259 | justify-content: space-between;
260 |
261 | .btn {
262 | background-color: transparent;
263 | padding: 0.25em;
264 |
265 | svg {
266 | color: var(--primary-high);
267 | }
268 |
269 | &:hover,
270 | &.active {
271 | background-color: var(--secondary-very-high);
272 |
273 | svg {
274 | color: var(--primary-high);
275 | }
276 | }
277 | height: 28px;
278 | }
279 | }
280 |
281 | input {
282 | width: 100%;
283 | }
284 |
285 | ul {
286 | margin: 0;
287 | list-style: none;
288 | }
289 | }
290 |
291 | @media print {
292 | .archetype-docs-topic {
293 | #main > div {
294 | grid-template-columns: 0 1fr;
295 | }
296 |
297 | .has-sidebar,
298 | .docs-search,
299 | .alert,
300 | .docs-filters,
301 | #skip-link {
302 | display: none;
303 | }
304 |
305 | .docs-topic {
306 | .docs-nav-link.return,
307 | .docs-nav-link.more {
308 | display: none;
309 | }
310 | }
311 | }
312 | }
313 |
314 | .docs-solved {
315 | padding: 0;
316 |
317 | input {
318 | width: auto;
319 | }
320 |
321 | .docs-item {
322 | width: 100%;
323 | }
324 | }
325 |
--------------------------------------------------------------------------------
/lib/docs/query.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Docs
4 | class Query
5 | def initialize(guardian, filters = {})
6 | @guardian = guardian
7 | @filters = filters
8 | @limit = 30
9 | end
10 |
11 | def self.categories
12 | SiteSetting.docs_categories.split("|")
13 | end
14 |
15 | def self.tags
16 | SiteSetting.docs_tags.split("|")
17 | end
18 |
19 | def list
20 | # query for topics matching selected categories & tags
21 | opts = { no_definitions: true, limit: false }
22 | tq = TopicQuery.new(@guardian.user, opts)
23 | results = tq.list_docs_topics
24 | results =
25 | results.left_outer_joins(SiteSetting.show_tags_by_group ? { tags: :tag_groups } : :tags)
26 | results = results.references(:categories)
27 | results =
28 | results.where("topics.category_id IN (?)", Query.categories).or(
29 | results.where("tags.name IN (?)", Query.tags),
30 | )
31 |
32 | # filter results by selected tags
33 | if @filters[:tags].present?
34 | tag_filters = @filters[:tags].split("|")
35 | tags_count = tag_filters.length
36 | tag_filters = Tag.where_name(tag_filters).pluck(:id) unless Integer === tag_filters[0]
37 |
38 | if tags_count == tag_filters.length
39 | tag_filters.each_with_index do |tag, index|
40 | # to_i to make it clear this is not an injection
41 | sql_alias = "tt#{index.to_i}"
42 | results =
43 | results.joins(
44 | "INNER JOIN topic_tags #{sql_alias} ON #{sql_alias}.topic_id = topics.id AND #{sql_alias}.tag_id = #{tag}",
45 | )
46 | end
47 | else
48 | results = results.none # don't return any results unless all tags exist in the database
49 | end
50 | end
51 |
52 | if @filters[:solved].present?
53 | results =
54 | results.where(
55 | "topics.id IN (
56 | SELECT tc.topic_id
57 | FROM topic_custom_fields tc
58 | WHERE tc.name = 'accepted_answer_post_id' AND
59 | tc.value IS NOT NULL
60 | )",
61 | )
62 | end
63 |
64 | # filter results by search term
65 | if @filters[:search_term].present?
66 | term = Search.prepare_data(@filters[:search_term])
67 | escaped_ts_query = Search.ts_query(term: term)
68 |
69 | results = results.where(<<~SQL)
70 | topics.id IN (
71 | SELECT pp.topic_id FROM post_search_data pd
72 | JOIN posts pp ON pp.id = pd.post_id AND pp.post_number = 1
73 | JOIN topics tt ON pp.topic_id = tt.id
74 | WHERE
75 | tt.id = topics.id AND
76 | pp.deleted_at IS NULL AND
77 | tt.deleted_at IS NULL AND
78 | pp.post_type <> #{Post.types[:whisper].to_i} AND
79 | pd.search_data @@ #{escaped_ts_query}
80 | )
81 | SQL
82 | end
83 |
84 | if @filters[:order] == "title"
85 | if @filters[:ascending].present?
86 | results = results.reorder("topics.title")
87 | else
88 | results = results.reorder("topics.title DESC")
89 | end
90 | elsif @filters[:order] == "activity"
91 | if @filters[:ascending].present?
92 | results = results.reorder("topics.last_posted_at")
93 | else
94 | results = results.reorder("topics.last_posted_at DESC")
95 | end
96 | end
97 |
98 | # conduct a second set of joins so we don't mess up the count
99 | count_query = results.joins <<~SQL
100 | INNER JOIN topic_tags ttx ON ttx.topic_id = topics.id
101 | INNER JOIN tags t2 ON t2.id = ttx.tag_id
102 | SQL
103 |
104 | if SiteSetting.show_tags_by_group
105 | enabled_tag_groups = SiteSetting.docs_tag_groups.split("|")
106 | subquery = TagGroup.where(name: enabled_tag_groups).select(:id)
107 | results = results.joins(tags: :tag_groups).where(tag_groups: { id: subquery })
108 |
109 | tags =
110 | count_query
111 | .joins(tags: :tag_groups)
112 | .where(tag_groups: { id: subquery })
113 | .group("tag_groups.id", "tag_groups.name", "tags.name")
114 | .reorder("")
115 | .count
116 |
117 | tags = create_group_tags_object(tags)
118 | else
119 | tags = count_query.group("t2.name").reorder("").count
120 | tags = create_tags_object(tags)
121 | end
122 |
123 | categories =
124 | results
125 | .where("topics.category_id IS NOT NULL")
126 | .group("topics.category_id")
127 | .reorder("")
128 | .count
129 | categories = create_categories_object(categories)
130 |
131 | # filter results by selected category
132 | # needs to be after building categories filter list
133 | if @filters[:category].present?
134 | category_ids = @filters[:category].split("|")
135 | results =
136 | results.where("topics.category_id IN (?)", category_ids) if category_ids.all? { |id|
137 | id =~ /\A\d+\z/
138 | }
139 | end
140 |
141 | results_length = results.size
142 |
143 | if @filters[:page]
144 | offset = @filters[:page].to_i * @limit
145 | page_range = offset + @limit
146 | end_of_list = true if page_range > results_length
147 | else
148 | offset = 0
149 | page_range = @limit
150 | end_of_list = true if results_length < @limit
151 | end
152 |
153 | results = results.offset(offset).limit(@limit) #results[offset...page_range]
154 |
155 | # assemble the object
156 | topic_query = tq.create_list(:docs, { unordered: true }, results)
157 |
158 | topic_list = TopicListSerializer.new(topic_query, scope: @guardian).as_json
159 |
160 | if end_of_list.nil?
161 | topic_list["load_more_url"] = load_more_url
162 | else
163 | topic_list["load_more_url"] = nil
164 | end
165 |
166 | tags_key = SiteSetting.show_tags_by_group ? :tag_groups : :tags
167 | {
168 | tags_key => tags,
169 | :categories => categories,
170 | :topics => topic_list,
171 | :topic_count => results_length,
172 | :meta => {
173 | show_topic_excerpts: show_topic_excerpts,
174 | },
175 | }
176 | end
177 |
178 | def create_group_tags_object(tags)
179 | tags_hash = ActiveSupport::OrderedHash.new
180 | allowed_tags = DiscourseTagging.filter_allowed_tags(Guardian.new(@user)).map(&:name)
181 |
182 | tags.each do |group_tags_data, count|
183 | group_tag_id, group_tag_name, tag_name = group_tags_data
184 | active = @filters[:tags]&.include?(tag_name)
185 |
186 | tags_hash[group_tag_id] ||= { id: group_tag_id, name: group_tag_name, tags: [] }
187 | tags_hash[group_tag_id][:tags] << { id: tag_name, count: count, active: active }
188 | end
189 |
190 | tags_hash
191 | .transform_values do |group|
192 | group[:tags] = group[:tags].filter { |tag| allowed_tags.include?(tag[:id]) }
193 | group
194 | end
195 | .values
196 | end
197 |
198 | def create_tags_object(tags)
199 | tags_object = []
200 |
201 | tags.each do |tag|
202 | active = @filters[:tags].include?(tag[0]) if @filters[:tags]
203 | tags_object << { id: tag[0], count: tag[1], active: active || false }
204 | end
205 |
206 | allowed_tags = DiscourseTagging.filter_allowed_tags(@guardian).map(&:name)
207 |
208 | tags_object = tags_object.select { |tag| allowed_tags.include?(tag[:id]) }
209 |
210 | tags_object.sort_by { |tag| [tag[:active] ? 0 : 1, -tag[:count]] }
211 | end
212 |
213 | def create_categories_object(category_counts)
214 | categories =
215 | Category
216 | .where(id: category_counts.keys)
217 | .includes(
218 | :uploaded_logo,
219 | :uploaded_logo_dark,
220 | :uploaded_background,
221 | :uploaded_background_dark,
222 | )
223 | .joins("LEFT JOIN topics t on t.id = categories.topic_id")
224 | .select("categories.*, t.slug topic_slug")
225 |
226 | Category.preload_user_fields!(@guardian, categories)
227 |
228 | categories
229 | .map do |category|
230 | count = category_counts[category.id]
231 | active = @filters[:category] && @filters[:category].include?(category.id.to_s)
232 |
233 | BasicCategorySerializer
234 | .new(category, scope: @guardian, root: false)
235 | .as_json
236 | .merge(count:, active:)
237 | end
238 | .sort_by { |category| [category[:active] ? 0 : 1, -category[:count]] }
239 | end
240 |
241 | def load_more_url
242 | filters = []
243 |
244 | filters.push("tags=#{@filters[:tags]}") if @filters[:tags].present?
245 | filters.push("category=#{@filters[:category]}") if @filters[:category].present?
246 | filters.push("solved=#{@filters[:solved]}") if @filters[:solved].present?
247 | filters.push("search=#{@filters[:search_term]}") if @filters[:search_term].present?
248 | filters.push("sort=#{@filters[:sort]}") if @filters[:sort].present?
249 |
250 | if @filters[:page].present?
251 | filters.push("page=#{@filters[:page].to_i + 1}")
252 | else
253 | filters.push("page=1")
254 | end
255 |
256 | "/#{GlobalSetting.docs_path}.json?#{filters.join("&")}"
257 | end
258 |
259 | def show_topic_excerpts
260 | SiteSetting.always_include_topic_excerpts ||
261 | ThemeModifierHelper.new(request: @guardian.request).serialize_topic_excerpts
262 | end
263 | end
264 | end
265 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/controllers/docs/index.js:
--------------------------------------------------------------------------------
1 | import Controller, { inject as controller } from "@ember/controller";
2 | import { action } from "@ember/object";
3 | import { alias, equal, gt, readOnly } from "@ember/object/computed";
4 | import { getOwner } from "@ember/owner";
5 | import { htmlSafe } from "@ember/template";
6 | import { on } from "@ember-decorators/object";
7 | import discourseComputed from "discourse/lib/decorators";
8 | import getURL from "discourse/lib/get-url";
9 | import { i18n } from "discourse-i18n";
10 | import Docs from "discourse/plugins/discourse-docs/discourse/models/docs";
11 |
12 | const SHOW_FILTER_AT = 10;
13 |
14 | export default class DocsIndexController extends Controller {
15 | @controller application;
16 |
17 | queryParams = [
18 | {
19 | ascending: "ascending",
20 | filterCategories: "category",
21 | filterTags: "tags",
22 | filterSolved: "solved",
23 | orderColumn: "order",
24 | searchTerm: "search",
25 | selectedTopic: "topic",
26 | },
27 | ];
28 |
29 | isLoading = false;
30 | isLoadingMore = false;
31 | isTopicLoading = false;
32 | filterTags = null;
33 | filterCategories = null;
34 | filterSolved = false;
35 | searchTerm = null;
36 | selectedTopic = null;
37 | topic = null;
38 | expandedFilters = false;
39 | ascending = null;
40 | orderColumn = null;
41 |
42 | @gt("categories.length", SHOW_FILTER_AT) showCategoryFilter;
43 | categoryFilter = "";
44 | categorySort = {};
45 |
46 | @gt("tags.length", SHOW_FILTER_AT) showTagFilter;
47 | tagFilter = "";
48 | tagSort = {};
49 |
50 | @alias("model.topics.load_more_url") loadMoreUrl;
51 | @readOnly("model.categories") categories;
52 | @alias("model.topics.topic_list.topics") topics;
53 | @readOnly("model.tags") tags;
54 | @readOnly("model.meta.show_topic_excerpts") showExcerpts;
55 | @readOnly("model.tag_groups") tagGroups;
56 | @alias("model.topic_count") topicCount;
57 | @equal("topicCount", 0) emptyResults;
58 |
59 | @on("init")
60 | _setupFilters() {
61 | if (this.site.desktopView) {
62 | this.set("expandedFilters", true);
63 | }
64 | this.setProperties({
65 | categorySort: {
66 | type: "numeric", // or alpha
67 | direction: "desc", // or asc
68 | },
69 | tagSort: {
70 | type: "numeric", // or alpha
71 | direction: "desc", // or asc
72 | },
73 | });
74 | }
75 |
76 | @discourseComputed("categories", "categorySort", "categoryFilter")
77 | sortedCategories(categories, categorySort, filter) {
78 | let { type, direction } = categorySort;
79 | if (type === "numeric") {
80 | categories = categories.sort((a, b) => a.count - b.count);
81 | } else {
82 | categories = categories.sort((a, b) => {
83 | const first = this.site.categories
84 | .find((item) => item.id === a.id)
85 | .name.toLowerCase(),
86 | second = this.site.categories
87 | .find((item) => item.id === b.id)
88 | ?.name.toLowerCase();
89 | return first.localeCompare(second);
90 | });
91 | }
92 |
93 | if (direction === "desc") {
94 | categories = categories.reverse();
95 | }
96 |
97 | if (this.showCategoryFilter) {
98 | return categories.filter((category) => {
99 | let categoryData = this.site.categories.find(
100 | (item) => item.id === category.id
101 | );
102 | return (
103 | categoryData.name.toLowerCase().indexOf(filter.toLowerCase()) > -1 ||
104 | (categoryData.description_excerpt &&
105 | categoryData.description_excerpt
106 | .toLowerCase()
107 | .indexOf(filter.toLowerCase()) > -1)
108 | );
109 | });
110 | }
111 |
112 | return categories;
113 | }
114 |
115 | @discourseComputed("categorySort")
116 | categorySortNumericIcon(catSort) {
117 | if (catSort.type === "numeric" && catSort.direction === "asc") {
118 | return "arrow-down-1-9";
119 | }
120 | return "arrow-up-1-9";
121 | }
122 |
123 | @discourseComputed("categorySort")
124 | categorySortAlphaIcon(catSort) {
125 | if (catSort.type === "alpha" && catSort.direction === "asc") {
126 | return "arrow-down-a-z";
127 | }
128 | return "arrow-up-a-z";
129 | }
130 |
131 | @discourseComputed("tags", "tagSort", "tagFilter")
132 | sortedTags(tags, tagSort, filter) {
133 | let { type, direction } = tagSort;
134 | if (type === "numeric") {
135 | tags = tags.sort((a, b) => a.count - b.count);
136 | } else {
137 | tags = tags.sort((a, b) => {
138 | return a.id.toLowerCase().localeCompare(b.id.toLowerCase());
139 | });
140 | }
141 |
142 | if (direction === "desc") {
143 | tags = tags.reverse();
144 | }
145 |
146 | if (this.showTagFilter) {
147 | return tags.filter((tag) => {
148 | return tag.id.toLowerCase().indexOf(filter.toLowerCase()) > -1;
149 | });
150 | }
151 |
152 | return tags;
153 | }
154 |
155 | @discourseComputed("tagGroups", "tagSort", "tagFilter")
156 | sortedTagGroups(tagGroups, tagSort, filter) {
157 | let { type, direction } = tagSort;
158 | let sortedTagGroups = [...tagGroups];
159 |
160 | if (type === "numeric") {
161 | sortedTagGroups.forEach((group) => {
162 | group.totalCount = group.tags.reduce(
163 | (acc, curr) => acc + curr.count,
164 | 0
165 | );
166 | });
167 |
168 | sortedTagGroups.sort((a, b) => b.totalCount - a.totalCount);
169 | } else {
170 | sortedTagGroups.sort((a, b) =>
171 | a.name.toLowerCase().localeCompare(b.name.toLowerCase())
172 | );
173 | }
174 |
175 | if (direction === "desc") {
176 | sortedTagGroups.reverse();
177 | }
178 |
179 | if (this.showTagFilter) {
180 | return sortedTagGroups.filter((tag) =>
181 | tag.id.toLowerCase().includes(filter.toLowerCase())
182 | );
183 | }
184 |
185 | return sortedTagGroups;
186 | }
187 |
188 | @discourseComputed("tagSort")
189 | tagSortNumericIcon(tagSort) {
190 | if (tagSort.type === "numeric" && tagSort.direction === "asc") {
191 | return "arrow-down-1-9";
192 | }
193 | return "arrow-up-1-9";
194 | }
195 |
196 | @discourseComputed("tagSort")
197 | tagSortAlphaIcon(tagSort) {
198 | if (tagSort.type === "alpha" && tagSort.direction === "asc") {
199 | return "arrow-down-a-z";
200 | }
201 | return "arrow-up-a-z";
202 | }
203 |
204 | @discourseComputed("topics", "isSearching", "filterSolved")
205 | noContent(topics, isSearching, filterSolved) {
206 | const filtered = isSearching || filterSolved;
207 | return this.topicCount === 0 && !filtered;
208 | }
209 |
210 | @discourseComputed("loadMoreUrl")
211 | canLoadMore(loadMoreUrl) {
212 | return loadMoreUrl === null ? false : true;
213 | }
214 |
215 | @discourseComputed("searchTerm")
216 | isSearching(searchTerm) {
217 | return !!searchTerm;
218 | }
219 |
220 | @discourseComputed("isSearching", "filterSolved")
221 | isSearchingOrFiltered(isSearching, filterSolved) {
222 | return isSearching || filterSolved;
223 | }
224 |
225 | @discourseComputed
226 | canFilterSolved() {
227 | return (
228 | this.siteSettings.solved_enabled &&
229 | this.siteSettings.docs_add_solved_filter
230 | );
231 | }
232 |
233 | @discourseComputed("filterTags")
234 | filtered(filterTags) {
235 | return !!filterTags;
236 | }
237 |
238 | @discourseComputed("siteSettings.tagging_enabled", "shouldShowTagsByGroup")
239 | shouldShowTags(tagging_enabled, shouldShowTagsByGroup) {
240 | return tagging_enabled && !shouldShowTagsByGroup;
241 | }
242 |
243 | @discourseComputed(
244 | "siteSettings.show_tags_by_group",
245 | "siteSettings.docs_tag_groups"
246 | )
247 | shouldShowTagsByGroup(show_tags_by_group, docs_tag_groups) {
248 | return show_tags_by_group && Boolean(docs_tag_groups);
249 | }
250 |
251 | @discourseComputed()
252 | emptyState() {
253 | let body = i18n("docs.no_docs.body");
254 | if (this.docsCategoriesAndTags.length) {
255 | body += i18n("docs.no_docs.to_include_topic_in_docs");
256 | body += ` (${this.docsCategoriesAndTags.join(", ")}).`;
257 | } else if (this.currentUser.staff) {
258 | const setUpPluginMessage = i18n("docs.no_docs.setup_the_plugin", {
259 | settingsUrl: getURL(
260 | "/admin/site_settings/category/plugins?filter=plugin:discourse-docs"
261 | ),
262 | });
263 | body += ` ${setUpPluginMessage}`;
264 | }
265 |
266 | return {
267 | title: i18n("docs.no_docs.title"),
268 | body: htmlSafe(body),
269 | };
270 | }
271 |
272 | @discourseComputed("docsCategories", "docsTags")
273 | docsCategoriesAndTags(docsCategories, docsTags) {
274 | return docsCategories.concat(docsTags);
275 | }
276 |
277 | @discourseComputed()
278 | docsCategories() {
279 | if (!this.siteSettings.docs_categories) {
280 | return [];
281 | }
282 |
283 | return this.siteSettings.docs_categories
284 | .split("|")
285 | .map(
286 | (c) =>
287 | this.site.categories.find((item) => item.id === parseInt(c, 10))?.name
288 | )
289 | .filter(Boolean);
290 | }
291 |
292 | @discourseComputed()
293 | docsTags() {
294 | if (!this.siteSettings.docs_tags) {
295 | return [];
296 | }
297 |
298 | return this.siteSettings.docs_tags.split("|").map((t) => `#${t}`);
299 | }
300 |
301 | @action
302 | toggleCategorySort(newType) {
303 | let { type, direction } = this.categorySort;
304 | this.set("categorySort", {
305 | type: newType,
306 | direction:
307 | type === newType ? (direction === "asc" ? "desc" : "asc") : "asc",
308 | });
309 | }
310 |
311 | @action
312 | toggleTagSort(newType) {
313 | let { type, direction } = this.tagSort;
314 | this.set("tagSort", {
315 | type: newType,
316 | direction:
317 | type === newType ? (direction === "asc" ? "desc" : "asc") : "asc",
318 | });
319 | }
320 |
321 | @action
322 | onChangeFilterSolved(event) {
323 | this.set("filterSolved", event.target.checked);
324 | }
325 |
326 | @action
327 | updateSelectedTags(tag, event) {
328 | event?.preventDefault();
329 |
330 | let filter = this.filterTags;
331 | if (filter && filter.includes(tag.id)) {
332 | filter = filter
333 | .split("|")
334 | .filter((f) => f !== tag.id)
335 | .join("|");
336 | } else if (filter) {
337 | filter = `${filter}|${tag.id}`;
338 | } else {
339 | filter = tag.id;
340 | }
341 |
342 | this.setProperties({
343 | filterTags: filter,
344 | selectedTopic: null,
345 | });
346 | }
347 |
348 | @action
349 | updateSelectedCategories(category, event) {
350 | event?.preventDefault();
351 |
352 | const filterCategories =
353 | category.id === parseInt(this.filterCategories, 10) ? null : category.id;
354 | this.setProperties({
355 | filterCategories,
356 | selectedTopic: null,
357 | });
358 | }
359 |
360 | @action
361 | performSearch(term) {
362 | if (term === "") {
363 | this.set("searchTerm", null);
364 | return false;
365 | }
366 |
367 | if (term.length < this.siteSettings.min_search_term_length) {
368 | return false;
369 | }
370 |
371 | this.setProperties({
372 | searchTerm: term,
373 | selectedTopic: null,
374 | });
375 | }
376 |
377 | @action
378 | sortBy(column) {
379 | const order = this.orderColumn;
380 | const ascending = this.ascending;
381 | if (column === "title") {
382 | this.set("orderColumn", "title");
383 | } else if (column === "activity") {
384 | this.set("orderColumn", "activity");
385 | }
386 |
387 | if (!ascending && order) {
388 | this.set("ascending", true);
389 | } else {
390 | this.set("ascending", "");
391 | }
392 | }
393 |
394 | @action
395 | loadMore() {
396 | if (this.canLoadMore && !this.isLoadingMore) {
397 | this.set("isLoadingMore", true);
398 |
399 | Docs.loadMore(this.loadMoreUrl).then((result) => {
400 | const topics = this.topics.concat(result.topics.topic_list.topics);
401 |
402 | this.setProperties({
403 | topics,
404 | loadMoreUrl: result.topics.load_more_url || null,
405 | isLoadingMore: false,
406 | });
407 | });
408 | }
409 | }
410 |
411 | @action
412 | toggleFilters() {
413 | if (!this.expandedFilters) {
414 | this.set("expandedFilters", true);
415 | } else {
416 | this.set("expandedFilters", false);
417 | }
418 | }
419 |
420 | @action
421 | returnToList() {
422 | this.set("selectedTopic", null);
423 | getOwner(this).lookup("service:router").transitionTo("docs");
424 | }
425 | }
426 |
--------------------------------------------------------------------------------
/spec/requests/docs_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe Docs::DocsController do
6 | fab!(:category)
7 | fab!(:topic) { Fabricate(:topic, title: "I love carrot today", category: category) }
8 | fab!(:topic2) { Fabricate(:topic, title: "I love pineapple today", category: category) }
9 | fab!(:tag) { Fabricate(:tag, topics: [topic], name: "test") }
10 |
11 | def get_tag_attributes(tag)
12 | { "id" => tag.name, "count" => 1 }
13 | end
14 |
15 | def get_tags_from_response(response_tags)
16 | response_tags.map { |tag| tag.except("active") }
17 | end
18 |
19 | before do
20 | SiteSetting.tagging_enabled = true
21 | SiteSetting.docs_enabled = true
22 | SiteSetting.docs_categories = category.id.to_s
23 | SiteSetting.docs_tags = "test"
24 | GlobalSetting.stubs(:docs_path).returns("docs")
25 | end
26 |
27 | describe "docs data" do
28 | context "when any user" do
29 | it "should return the right response" do
30 | get "/#{GlobalSetting.docs_path}.json"
31 |
32 | expect(response.status).to eq(200)
33 |
34 | json = JSON.parse(response.body)
35 | tags = json["tags"]
36 | topics = json["topics"]["topic_list"]["topics"]
37 |
38 | expect(tags.size).to eq(1)
39 | expect(topics.size).to eq(2)
40 | end
41 |
42 | it "should return a topic count" do
43 | get "/#{GlobalSetting.docs_path}.json"
44 |
45 | json = response.parsed_body
46 | topic_count = json["topic_count"]
47 |
48 | expect(topic_count).to eq(2)
49 | end
50 | end
51 |
52 | context "when some docs topics are private" do
53 | let!(:group) { Fabricate(:group) }
54 | let!(:private_category) { Fabricate(:private_category, group: group) }
55 | let!(:private_topic) { Fabricate(:topic, category: private_category) }
56 |
57 | before { SiteSetting.docs_categories = "#{category.id}|#{private_category.id}" }
58 |
59 | it "should not show topics in private categories without permissions" do
60 | get "/#{GlobalSetting.docs_path}.json"
61 |
62 | json = JSON.parse(response.body)
63 | topics = json["topics"]["topic_list"]["topics"]
64 |
65 | expect(topics.size).to eq(2)
66 | end
67 |
68 | it "should show topics when users have permissions" do
69 | admin = Fabricate(:admin)
70 | sign_in(admin)
71 |
72 | get "/#{GlobalSetting.docs_path}.json"
73 |
74 | json = JSON.parse(response.body)
75 | topics = json["topics"]["topic_list"]["topics"]
76 |
77 | expect(topics.size).to eq(3)
78 | end
79 | end
80 |
81 | context "when filtering by tag" do
82 | fab!(:tag2) { Fabricate(:tag, topics: [topic], name: "test2") }
83 | fab!(:tag3) { Fabricate(:tag, topics: [topic], name: "test3") }
84 |
85 | it "should return a list filtered by tag" do
86 | get "/#{GlobalSetting.docs_path}.json?tags=test"
87 |
88 | expect(response.status).to eq(200)
89 |
90 | json = JSON.parse(response.body)
91 | topics = json["topics"]["topic_list"]["topics"]
92 |
93 | expect(topics.size).to eq(1)
94 | end
95 |
96 | it "should properly filter with more than two tags" do
97 | get "/#{GlobalSetting.docs_path}.json?tags=test%7ctest2%7ctest3"
98 |
99 | expect(response.status).to eq(200)
100 |
101 | json = response.parsed_body
102 | tags = json["tags"]
103 | topics = json["topics"]["topic_list"]["topics"]
104 |
105 | expect(tags.size).to eq(3)
106 | expect(topics.size).to eq(1)
107 | end
108 |
109 | it "should not error out when tags is an array" do
110 | get "/#{GlobalSetting.docs_path}.json?tags[]=test"
111 |
112 | expect(response.status).to eq(400)
113 | end
114 |
115 | it "should not error out when tags is a nested parameter" do
116 | get "/#{GlobalSetting.docs_path}.json?tags[foo]=test"
117 |
118 | expect(response.status).to eq(400)
119 | end
120 |
121 | context "when show_tags_by_group is enabled" do
122 | fab!(:tag4) { Fabricate(:tag, topics: [topic], name: "test4") }
123 |
124 | fab!(:tag_group_1) { Fabricate(:tag_group, name: "test-test2", tag_names: %w[test test2]) }
125 | fab!(:tag_group_2) do
126 | Fabricate(:tag_group, name: "test3-test4", tag_names: %w[test3 test4])
127 | end
128 | fab!(:non_docs_tag_group) do
129 | Fabricate(:tag_group, name: "non-docs-group", tag_names: %w[test3])
130 | end
131 | fab!(:empty_tag_group) { Fabricate(:tag_group, name: "empty-group") }
132 |
133 | let(:docs_json_path) { "/#{GlobalSetting.docs_path}.json" }
134 | let(:parsed_body) { response.parsed_body }
135 | let(:tag_groups) { parsed_body["tag_groups"] }
136 | let(:tag_ids) { tag_groups.map { |group| group["id"] } }
137 |
138 | before do
139 | SiteSetting.show_tags_by_group = true
140 | SiteSetting.docs_tag_groups = "test-test2|test3-test4"
141 | get docs_json_path
142 | end
143 |
144 | it "should add groups to the tags attribute" do
145 | get docs_json_path
146 | expect(get_tags_from_response(tag_groups[0]["tags"])).to contain_exactly(
147 | *[tag, tag2].map { |t| get_tag_attributes(t) },
148 | )
149 | expect(get_tags_from_response(tag_groups[1]["tags"])).to contain_exactly(
150 | *[tag3, tag4].map { |t| get_tag_attributes(t) },
151 | )
152 | end
153 |
154 | it "only displays tag groups that are enabled" do
155 | SiteSetting.docs_tag_groups = "test3-test4"
156 | get docs_json_path
157 | expect(tag_groups.size).to eq(1)
158 | expect(get_tags_from_response(tag_groups[0]["tags"])).to contain_exactly(
159 | *[tag3, tag4].map { |t| get_tag_attributes(t) },
160 | )
161 | end
162 |
163 | it "does not return tag groups without tags" do
164 | expect(tag_ids).not_to include(empty_tag_group.id)
165 | end
166 |
167 | it "does not return non-docs tag groups" do
168 | expect(tag_ids).not_to include(non_docs_tag_group.id)
169 | end
170 | end
171 | end
172 |
173 | context "when filtering by category" do
174 | let!(:category2) { Fabricate(:category) }
175 | let!(:topic3) { Fabricate(:topic, category: category2) }
176 |
177 | before { SiteSetting.docs_categories = "#{category.id}|#{category2.id}" }
178 |
179 | it "should return a list filtered by category" do
180 | get "/#{GlobalSetting.docs_path}.json?category=#{category2.id}"
181 |
182 | expect(response.status).to eq(200)
183 |
184 | json = JSON.parse(response.body)
185 | categories = json["categories"]
186 | topics = json["topics"]["topic_list"]["topics"]
187 |
188 | expect(categories.size).to eq(2)
189 | expect(categories[0]).to include({ "active" => true, "count" => 1, "id" => category2.id })
190 | expect(categories[1]).to include({ "active" => false, "count" => 2, "id" => category.id })
191 | expect(topics.size).to eq(1)
192 | end
193 |
194 | it "ignores category filter when incorrect argument" do
195 | get "/#{GlobalSetting.docs_path}.json?category=hack"
196 |
197 | expect(response.status).to eq(200)
198 |
199 | json = JSON.parse(response.body)
200 | categories = json["categories"]
201 | topics = json["topics"]["topic_list"]["topics"]
202 |
203 | expect(categories.size).to eq(2)
204 | expect(topics.size).to eq(3)
205 | end
206 | end
207 |
208 | context "when ordering results" do
209 | describe "by title" do
210 | it "should return the list ordered descending" do
211 | get "/#{GlobalSetting.docs_path}.json?order=title"
212 |
213 | expect(response.status).to eq(200)
214 |
215 | json = response.parsed_body
216 | topics = json["topics"]["topic_list"]["topics"]
217 |
218 | expect(topics[0]["id"]).to eq(topic2.id)
219 | expect(topics[1]["id"]).to eq(topic.id)
220 | end
221 |
222 | it "should return the list ordered ascending with an additional parameter" do
223 | get "/#{GlobalSetting.docs_path}.json?order=title&ascending=true"
224 |
225 | expect(response.status).to eq(200)
226 |
227 | json = response.parsed_body
228 | topics = json["topics"]["topic_list"]["topics"]
229 |
230 | expect(topics[0]["id"]).to eq(topic.id)
231 | expect(topics[1]["id"]).to eq(topic2.id)
232 | end
233 | end
234 |
235 | describe "by date" do
236 | before { topic2.update(last_posted_at: Time.zone.now + 100) }
237 |
238 | it "should return the list ordered descending" do
239 | get "/#{GlobalSetting.docs_path}.json?order=activity"
240 |
241 | expect(response.status).to eq(200)
242 |
243 | json = response.parsed_body
244 | topics = json["topics"]["topic_list"]["topics"]
245 |
246 | expect(topics[0]["id"]).to eq(topic.id)
247 | expect(topics[1]["id"]).to eq(topic2.id)
248 | end
249 |
250 | it "should return the list ordered ascending with an additional parameter" do
251 | get "/#{GlobalSetting.docs_path}.json?order=activity&ascending=true"
252 |
253 | expect(response.status).to eq(200)
254 |
255 | json = response.parsed_body
256 | topics = json["topics"]["topic_list"]["topics"]
257 |
258 | expect(topics[0]["id"]).to eq(topic2.id)
259 | expect(topics[1]["id"]).to eq(topic.id)
260 | end
261 | end
262 | end
263 |
264 | context "when searching" do
265 | before { SearchIndexer.enable }
266 |
267 | # no fab here otherwise will be missing from search
268 | let!(:post) do
269 | topic = Fabricate(:topic, title: "I love banana today", category: category)
270 | Fabricate(:post, topic: topic, raw: "walking and running is fun")
271 | end
272 |
273 | let!(:post2) do
274 | topic = Fabricate(:topic, title: "I love the amazing tomorrow", category: category)
275 | Fabricate(:post, topic: topic, raw: "I also eat bananas")
276 | end
277 |
278 | it "should correctly filter topics" do
279 | get "/#{GlobalSetting.docs_path}.json?search=banana"
280 |
281 | expect(response.status).to eq(200)
282 |
283 | json = JSON.parse(response.body)
284 | topics = json["topics"]["topic_list"]["topics"]
285 |
286 | # ordered by latest for now
287 |
288 | expect(topics[0]["id"]).to eq(post2.topic_id)
289 | expect(topics[1]["id"]).to eq(post.topic_id)
290 |
291 | expect(topics.size).to eq(2)
292 |
293 | get "/#{GlobalSetting.docs_path}.json?search=walk"
294 |
295 | json = JSON.parse(response.body)
296 | topics = json["topics"]["topic_list"]["topics"]
297 |
298 | expect(topics.size).to eq(1)
299 | end
300 | end
301 |
302 | context "when getting topic first post contents" do
303 | let!(:non_ke_topic) { Fabricate(:topic) }
304 |
305 | it "should correctly grab the topic" do
306 | get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}"
307 |
308 | expect(response.parsed_body["topic"]["id"]).to eq(topic.id)
309 | end
310 |
311 | it "should get topics matching a selected docs tag or category" do
312 | get "/#{GlobalSetting.docs_path}.json?topic=#{non_ke_topic.id}"
313 |
314 | expect(response.parsed_body["topic"]).to be_blank
315 | end
316 |
317 | it "should return a docs topic when only tags are added to settings" do
318 | SiteSetting.docs_categories = nil
319 |
320 | get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}"
321 |
322 | expect(response.parsed_body["topic"]["id"]).to eq(topic.id)
323 | end
324 |
325 | it "should return a docs topic when only categories are added to settings" do
326 | SiteSetting.docs_tags = nil
327 |
328 | get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}"
329 |
330 | expect(response.parsed_body["topic"]["id"]).to eq(topic.id)
331 | end
332 |
333 | it "should create TopicViewItem" do
334 | admin = Fabricate(:admin)
335 | sign_in(admin)
336 | expect do get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}" end.to change {
337 | TopicViewItem.count
338 | }.by(1)
339 | end
340 |
341 | it "should create TopicUser if authenticated" do
342 | expect do
343 | get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}&track_visit=true"
344 | end.not_to change { TopicUser.count }
345 |
346 | admin = Fabricate(:admin)
347 | sign_in(admin)
348 | expect do
349 | get "/#{GlobalSetting.docs_path}.json?topic=#{topic.id}&track_visit=true"
350 | end.to change { TopicUser.count }.by(1)
351 | end
352 | end
353 | end
354 | end
355 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/templates/docs/index.gjs:
--------------------------------------------------------------------------------
1 | import { Input } from "@ember/component";
2 | import { fn } from "@ember/helper";
3 | import { on } from "@ember/modifier";
4 | import RouteTemplate from "ember-route-template";
5 | import { and, eq } from "truth-helpers";
6 | import BasicTopicList from "discourse/components/basic-topic-list";
7 | import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
8 | import DButton from "discourse/components/d-button";
9 | import EmptyState from "discourse/components/empty-state";
10 | import LoadMore from "discourse/components/load-more";
11 | import PluginOutlet from "discourse/components/plugin-outlet";
12 | import lazyHash from "discourse/helpers/lazy-hash";
13 | import { i18n } from "discourse-i18n";
14 | import DocsCategory from "../../components/docs-category";
15 | import DocsTag from "../../components/docs-tag";
16 | import DocsTopic from "../../components/docs-topic";
17 |
18 | export default RouteTemplate(
19 |
20 |
21 | {{#if @controller.noContent}}
22 |
26 | {{else}}
27 |
28 | {{#if @controller.site.mobileView}}
29 | {{#unless @controller.selectedTopic}}
30 |
36 | {{/unless}}
37 | {{/if}}
38 |
39 |
40 | {{#if @controller.expandedFilters}}
41 | {{#if @controller.canFilterSolved}}
42 |
53 | {{/if}}
54 |
55 | {{#if @controller.categories}}
56 |
57 |
58 |
59 | {{i18n "docs.categories"}}
60 |
61 |
86 |
87 | {{#if @controller.showCategoryFilter}}
88 |
93 | {{/if}}
94 |
95 | {{#each @controller.sortedCategories as |category|}}
96 | -
97 |
104 |
105 | {{/each}}
106 |
107 |
108 | {{/if}}
109 |
110 | {{#if (and @controller.tags @controller.shouldShowTags)}}
111 |
166 | {{/if}}
167 | {{#if
168 | (and @controller.tagGroups @controller.shouldShowTagsByGroup)
169 | }}
170 |
235 | {{/if}}
236 | {{/if}}
237 |
238 |
239 | {{#if @controller.selectedTopic}}
240 |
243 |
247 |
252 |
253 | {{else}}
254 |
255 | {{#if @controller.isSearchingOrFiltered}}
256 | {{#if @controller.emptyResults}}
257 |
258 | {{i18n "search.no_results"}}
259 |
260 |
261 |
265 |
266 | {{else}}
267 |
268 | {{i18n "docs.search.results" count=@controller.topicCount}}
269 |
270 | {{/if}}
271 | {{/if}}
272 |
273 | {{#unless @controller.emptyResults}}
274 |
278 |
284 |
285 |
288 | {{/unless}}
289 |
290 | {{/if}}
291 |
292 | {{/if}}
293 |
294 |
295 | );
296 |
--------------------------------------------------------------------------------