├── .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 | 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 | 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 | 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 | 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.

\n

I 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 | 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 | 295 | ); 296 | --------------------------------------------------------------------------------