├── .prettierrc ├── .eslintrc ├── .streerc ├── .rubocop.yml ├── .template-lintrc.js ├── assets ├── javascripts │ ├── components │ │ ├── canned-replies-form.js │ │ └── canned-reply.js │ ├── discourse │ │ └── templates │ │ │ ├── modal │ │ │ ├── new-reply.hbs │ │ │ ├── edit-reply.hbs │ │ │ └── canned-replies.hbs │ │ │ ├── components │ │ │ ├── canned-replies-form.hbs │ │ │ └── canned-reply.hbs │ │ │ └── connectors │ │ │ └── editor-preview │ │ │ └── canned-replies.hbs │ ├── initializers │ │ └── add-canned-replies-ui-builder.js │ ├── controllers │ │ ├── new-reply.js │ │ ├── edit-reply.js │ │ └── canned-replies.js │ ├── lib │ │ └── apply-reply.js │ └── connectors │ │ └── editor-preview │ │ └── canned-replies.js └── stylesheets │ └── canned-replies.scss ├── .gitignore ├── package.json ├── Gemfile ├── .editorconfig ├── config ├── locales │ ├── server.be.yml │ ├── server.bg.yml │ ├── server.cs.yml │ ├── server.da.yml │ ├── server.el.yml │ ├── server.et.yml │ ├── server.gl.yml │ ├── server.id.yml │ ├── server.lt.yml │ ├── server.lv.yml │ ├── server.sq.yml │ ├── server.sr.yml │ ├── server.te.yml │ ├── server.th.yml │ ├── server.vi.yml │ ├── client.en_GB.yml │ ├── server.bs_BA.yml │ ├── server.en_GB.yml │ ├── server.fa_IR.yml │ ├── server.nb_NO.yml │ ├── server.zh_TW.yml │ ├── client.id.yml │ ├── client.sk.yml │ ├── client.el.yml │ ├── client.gl.yml │ ├── client.sr.yml │ ├── client.th.yml │ ├── client.be.yml │ ├── client.bg.yml │ ├── client.lt.yml │ ├── client.lv.yml │ ├── client.te.yml │ ├── server.hu.yml │ ├── server.sk.yml │ ├── client.zh_TW.yml │ ├── client.pt.yml │ ├── client.sq.yml │ ├── client.nb_NO.yml │ ├── client.cs.yml │ ├── client.vi.yml │ ├── server.ko.yml │ ├── client.et.yml │ ├── server.zh_CN.yml │ ├── server.sw.yml │ ├── server.ja.yml │ ├── server.en.yml │ ├── client.da.yml │ ├── server.ur.yml │ ├── client.en.yml │ ├── server.uk.yml │ ├── server.hr.yml │ ├── client.zh_CN.yml │ ├── client.he.yml │ ├── client.ja.yml │ ├── client.fa_IR.yml │ ├── server.ar.yml │ ├── server.ca.yml │ ├── client.ko.yml │ ├── server.he.yml │ ├── server.hy.yml │ ├── client.ur.yml │ ├── client.sl.yml │ ├── client.hu.yml │ ├── client.ro.yml │ ├── client.uk.yml │ ├── client.pl_PL.yml │ ├── client.hr.yml │ ├── client.bs_BA.yml │ ├── client.ca.yml │ ├── client.sw.yml │ ├── server.pl_PL.yml │ ├── server.sv.yml │ ├── server.tr_TR.yml │ ├── client.hy.yml │ ├── client.tr_TR.yml │ ├── server.ru.yml │ ├── client.ar.yml │ ├── server.sl.yml │ ├── client.sv.yml │ ├── server.nl.yml │ ├── server.pt.yml │ ├── client.nl.yml │ ├── client.de.yml │ ├── client.fi.yml │ ├── client.ru.yml │ ├── server.de.yml │ ├── server.es.yml │ ├── server.fi.yml │ ├── server.ro.yml │ ├── client.it.yml │ ├── client.es.yml │ ├── server.pt_BR.yml │ ├── client.fr.yml │ ├── client.pt_BR.yml │ ├── server.it.yml │ └── server.fr.yml ├── settings.yml └── routes.rb ├── lib └── discourse_canned_replies │ └── engine.rb ├── translator.yml ├── .github └── workflows │ └── discourse-plugin.yml ├── app ├── jobs │ └── onceoff │ │ └── rename_canned_replies.rb ├── controllers │ └── discourse_canned_replies │ │ └── canned_replies_controller.rb └── models │ └── discourse_canned_replies │ └── reply.rb ├── README.md ├── spec ├── plugin_spec.rb └── integration │ └── canned_reply │ └── canned_replies_spec.rb ├── Gemfile.lock ├── plugin.rb ├── test └── javascripts │ └── acceptance │ └── canned-replies-test.js └── LICENSE.txt /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-discourse" 3 | } 4 | -------------------------------------------------------------------------------- /.streerc: -------------------------------------------------------------------------------- 1 | --print-width=100 2 | --plugins=plugin/trailing_comma 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | rubocop-discourse: stree-compat.yml 3 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["ember-template-lint-plugin-discourse"], 3 | extends: "discourse:recommended", 4 | }; 5 | -------------------------------------------------------------------------------- /assets/javascripts/components/canned-replies-form.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | 3 | export default Component.extend({}); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .DS_Store 4 | node_modules/ 5 | yarn-error.log 6 | node_modules 7 | .rubocop-https---raw-githubusercontent-com-discourse-* 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Discourse", 3 | "license": "GPL-2.0-only", 4 | "devDependencies": { 5 | "eslint-config-discourse": "^3.4.0" 6 | } 7 | } 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 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*.*] 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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.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.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.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.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.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 | -------------------------------------------------------------------------------- /lib/discourse_canned_replies/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ::DiscourseCannedReplies 4 | class Engine < ::Rails::Engine 5 | engine_name PLUGIN_NAME 6 | isolate_namespace DiscourseCannedReplies 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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/settings.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | canned_replies_enabled: 3 | default: true 4 | client: true 5 | canned_replies_groups: 6 | default: "" 7 | type: list 8 | canned_replies_everyone_enabled: 9 | default: false 10 | client: true 11 | canned_replies_everyone_can_edit: 12 | default: false 13 | client: true 14 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | DiscourseCannedReplies::Engine.routes.draw do 4 | resources :canned_replies, path: "/", only: %i[index create destroy update] do 5 | member do 6 | get "reply" 7 | patch "use" 8 | end 9 | end 10 | end 11 | 12 | Discourse::Application.routes.draw { mount ::DiscourseCannedReplies::Engine, at: "/canned_replies" } 13 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Judul" 12 | insert: 13 | new_button: "Baru" 14 | edit_button: "Ubah" 15 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Názov" 12 | insert: 13 | new_button: "Nový" 14 | edit_button: "Upraviť" 15 | back: "Späť" 16 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Τίτλος" 12 | insert: 13 | new_button: "Νέο" 14 | edit_button: "Επεξεργασία" 15 | back: "Πίσω" 16 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Título" 12 | insert: 13 | new_button: "Novo" 14 | edit_button: "Editar" 15 | back: "Volver" 16 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Naslov" 12 | insert: 13 | new_button: "Novo" 14 | edit_button: "Izmeni" 15 | back: "Nazad" 16 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "ชื่อเรื่อง" 12 | insert: 13 | new_button: "ใหม่" 14 | edit_button: "แก้ไข" 15 | back: "กลับ" 16 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "тытульны" 12 | insert: 13 | new_button: "Новая" 14 | edit_button: "Рэдагаваць" 15 | back: "Назад" 16 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Заглавие" 12 | insert: 13 | new_button: "Нов" 14 | edit_button: "Редактирай" 15 | back: "Назад" 16 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Antraštė" 12 | insert: 13 | new_button: "Naujas" 14 | edit_button: "Redaguoti" 15 | back: "Atgal" 16 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Virsraksts" 12 | insert: 13 | new_button: "Jauns" 14 | edit_button: "Labot" 15 | back: "Atpakaļ" 16 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "శీర్షిక" 12 | insert: 13 | new_button: "కొత్త" 14 | edit_button: "సవరించు" 15 | back: "వెనుకకు" 16 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Megjelenítse az eltett válaszok beillesztése gombot a szerkesztő ablakon?' 10 | replies: 11 | default_reply: 12 | title: "Első eltett válaszom" 13 | -------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: 'Má sa v editore zobraziť tlačítko na vloženie pripravenej odpovede?' 10 | replies: 11 | default_reply: 12 | title: "Moja prvá pripravená odpoveď" 13 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/templates/modal/new-reply.hbs: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "標題" 12 | content: 13 | name: "內容" 14 | insert: 15 | new_button: "新使用者" 16 | edit_button: "編輯" 17 | back: "上一步" 18 | -------------------------------------------------------------------------------- /app/jobs/onceoff/rename_canned_replies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jobs 4 | class RenameCannedReplies < ::Jobs::Onceoff 5 | OLD_PLUGIN_NAME = "canned_replies" 6 | NEW_PLUGIN_NAME = "discourse-canned-replies" 7 | 8 | def execute_onceoff(args) 9 | PluginStoreRow.where(plugin_name: NEW_PLUGIN_NAME).delete_all 10 | PluginStoreRow.where(plugin_name: OLD_PLUGIN_NAME).update_all(plugin_name: NEW_PLUGIN_NAME) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Título" 12 | content: 13 | name: "Conteúdo" 14 | insert: 15 | new_button: "Novo" 16 | edit_button: "Editar" 17 | back: "Retroceder" 18 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Titulli" 12 | content: 13 | name: "Content" 14 | insert: 15 | new_button: "I Ri" 16 | edit_button: "Redakto" 17 | back: "Kthehu mbrapa" 18 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Tittel" 12 | content: 13 | name: "Innhold" 14 | insert: 15 | choose: "(velg et svar)" 16 | new_button: "Ny" 17 | edit_button: "Rediger" 18 | back: "Forrige" 19 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/templates/components/canned-replies-form.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 |
-------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Nadpis" 12 | invalid: Nadpis nemůže být prázdný. 13 | content: 14 | name: "Obsah" 15 | invalid: Obsah nemůže být prázdný. 16 | insert: 17 | insert_button: "Vložit odpověď" 18 | new_button: "Noví" 19 | edit_button: "Upravit" 20 | back: "Zpět" 21 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Tiêu đề" 12 | invalid: Tiêu đề không được trống 13 | content: 14 | name: "Nội dung" 15 | invalid: Nội dung không được trống 16 | insert: 17 | choose: "(chọn một câu trả lời)" 18 | new_button: "Mới" 19 | edit_button: "Sửa" 20 | back: "Quay lại" 21 | -------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: '작성기 창에 미리 준비된 답장 삽입 버튼을 표시 하시겠습니까?' 10 | canned_replies_groups: '비 직원 그룹의 미리 준비된 답변에 그룹 이름별로 액세스 권한 추가' 11 | canned_replies_everyone_enabled: '모든 사람이 미리 준비된 답변을 사용할 수 있도록 허용' 12 | canned_replies_everyone_can_edit: '모든 사람이 미리 준비된 답장을 작성, 편집 및 삭제하도록 허용' 13 | replies: 14 | default_reply: 15 | title: "나의 첫 통조림 답변" 16 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | title: 11 | name: "Pealkiri" 12 | invalid: Pealkiri ei saa olla tühi. 13 | content: 14 | name: "Sisu" 15 | invalid: Sisu ei saa olla tühi. 16 | insert: 17 | choose: "(vali vastus)" 18 | insert_button: "Sisesta vastus" 19 | new_button: "Uus" 20 | edit_button: "uuda" 21 | sort: 22 | alphabetically: "Sorteeri tähestiku järjekorras" 23 | back: "Tagasi" 24 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: '在编辑器窗口上显示“插入预设回复”按钮?' 10 | canned_replies_groups: '按群组名称为非管理群组添加预设回复的访问权限' 11 | canned_replies_everyone_enabled: '允许所有人使用预设回复' 12 | canned_replies_everyone_can_edit: '允许所有人创建、编辑和删除预设回复' 13 | replies: 14 | default_reply: 15 | title: "我的第一个预设回复" 16 | body: | 17 | 这是一个示例预设回复。 18 | 您可以使用 **markdown ** 设置回复样式。点击**新建**按钮可以创建新回复,点击**编辑**按钮可以编辑或移除现有的预设回复。 19 | 20 | * 当回复名单为空时,将添加此预设回复。* 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This plugin is no longer supported. Please migrate to the [new discourse-templates plugin](https://meta.discourse.org/t/discourse-templates/229250). 4 | 5 | # Canned replies plugin 6 | This plugin adds support for inserting a canned reply into the composer window via a UI. 7 | 8 | Official Plugin Topic & Documentation: https://meta.discourse.org/t/discourse-canned-replies/48296/ 9 | 10 | ## Installation 11 | 12 | Follow the directions at [Install a Plugin](https://meta.discourse.org/t/install-a-plugin/19157) using https://github.com/discourse/discourse-canned-replies.git as the repository URL. 13 | 14 | ## Authors 15 | 16 | Jay Pfaffman 17 | 18 | André Pereira 19 | 20 | ## License 21 | 22 | GNU GPL v2 23 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/templates/components/canned-reply.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
{{reply.title}}
4 | 5 |
6 | 11 | 12 | {{#if canEdit}} 13 | 18 | {{/if}} 19 |
20 |
21 | 22 |
23 | {{cook-text reply.content}} 24 |
25 |
-------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: 'Onyesha Kitufe cha kuweka majibu yaliyohifadhiwa kwenye sehemu ya kuandika? ' 10 | replies: 11 | default_reply: 12 | title: "Jibu langu la kwanza lililohifadhiwa" 13 | body: | 14 | Huu ni mfano wa majibu yaliyohifadhiwa. 15 | Tumia **markdown** kuweka mtindo kwenye majibu yako. Bonyeza kitufe cha **mpya** kutengeneza majibu mapya au kitufe cha **hariri** kuhariri ay ondoa jibu ulilo hifadhi. 16 | 17 | *Jibu jipya litahifadhiwa kama orodha ya majibu iko tupu.* 18 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'コンポーザーウィンドウに[返信定型文を挿入]ボタンを表示しますか?' 10 | canned_replies_groups: 'グループ名でスタッフ以外のグループに返信定型文へのアクセスを追加する' 11 | canned_replies_everyone_enabled: '返信定型文の使用を全員に許可する' 12 | canned_replies_everyone_can_edit: '返信定型文の作成、編集、および削除を全員に許可する' 13 | replies: 14 | default_reply: 15 | title: "私の最初の返信定型文" 16 | body: | 17 | これは返信定型文の例です。 18 | **マークダウン**を使用して、返信にスタイルを設定できます。**新規**ボタンをクリックして新しい返信を作成するか、**編集**ボタンをクリックして既存の返信定型文を編集または削除します。 19 | 20 | *この返信定型文は、返信リストが空である場合に追加されます。* 21 | -------------------------------------------------------------------------------- /config/locales/server.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | site_settings: 3 | canned_replies_enabled: 'Show insert canned reply button on composer window?' 4 | canned_replies_groups: 'Add access to canned replies for non-staff groups by group name' 5 | canned_replies_everyone_enabled: 'Allow everyone to use canned replies' 6 | canned_replies_everyone_can_edit: 'Allow everyone to create, edit, and delete canned replies' 7 | replies: 8 | default_reply: 9 | title: "My first canned reply" 10 | body: | 11 | This is an example canned reply. 12 | You can use **markdown** to style your replies. Click the **new** button to create new replies or the **edit** button to edit or remove an existing canned reply. 13 | 14 | *This canned reply will be added when the replies list is empty.* 15 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/templates/modal/edit-reply.hbs: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Filtrer efter titel ..." 11 | title: 12 | name: "Titel" 13 | invalid: Titel kan ikke være tom. 14 | content: 15 | name: "Indhold" 16 | invalid: Indholdet kan ikke være tomt. 17 | insert: 18 | choose: "(vælg et svar)" 19 | insert_button: "Indsæt Svar" 20 | new_button: "Ny" 21 | edit_button: "Rediger" 22 | sort: 23 | alphabetically: "Sortér alfabetisk" 24 | usage: "Sorter efter anvendelsesfrekvens" 25 | back: "Tilbage" 26 | -------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: 'کومپوزر کی ونڈو پر طہ شدہ جواب کا بٹن دکھائیں؟' 10 | canned_replies_groups: 'گروپ نام کے حساب سے غیر سٹاف گروپس کیلئے طہ شدہ جوابات تک رسائی شامل کریں' 11 | replies: 12 | default_reply: 13 | title: "میرا پہلا طہ شدہ جواب" 14 | body: | 15 | یہ ایک مثال طہ شدہ جواب ہے۔ 16 | آپ اپنے جوابات کو سٹائل کرنے کیلئے **مارکڈائون** کا استعمال کرسکتے ہیں۔ نیا جواب بنابے کیلئے **نیا** بٹن پر کلک کریں یا کسی موجودہ طہ شدہ جواب میں ترمیم یا حذف کرنے کیلئے **ترمیم** بٹن پر کلک کریں۔ 17 | 18 | *یہ طہ شدہ جواب شامل کیا جائے گا جب جوابات کی فہرست خالی ہو گی۔* 19 | -------------------------------------------------------------------------------- /config/locales/client.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | js: 3 | canned_replies: 4 | filter_hint: "Filter by title..." 5 | composer_button_text: "Canned replies" 6 | title: 7 | name: "Title" 8 | invalid: Title can't be empty. 9 | content: 10 | name: "Content" 11 | invalid: Content can't be empty. 12 | insert: 13 | choose: "(choose a reply)" 14 | insert_button: "Insert Reply" 15 | new_button: "New" 16 | edit_button: "Edit" 17 | modal_title: "Insert Canned Reply" 18 | sort: 19 | alphabetically: "Sort alphabetically" 20 | usage: "Sort by frequency of use" 21 | add: 22 | modal_title: "Add Canned Reply" 23 | edit: 24 | modal_title: "Edit Canned Reply" 25 | remove_confirm: "Are you sure you want to delete this canned reply?" 26 | back: "Back" 27 | -------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: 'Показати кнопку вставляння готової відповіді у вікно композитора?' 10 | canned_replies_groups: 'Додайте доступ до готових відповідей для неспівробітників за назвою групи' 11 | replies: 12 | default_reply: 13 | title: "Моя перша готова відповідь" 14 | body: | 15 | Це приклад готової відповіді. 16 | Ви можете використовувати **markdown** для стилювання відповідей. Натисніть кнопку **new**, щоб створити нові відповіді, або кнопку **edit**, щоб відредагувати або видалити наявну відповідь. 17 | *Ця готова відповідь буде додана, коли список відповідей порожній.* 18 | -------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: 'Prikaži gumb za ubacivanje pripremljenog odgovora u prozoru composer-a' 10 | canned_replies_groups: 'Dodaj pristup pripremljenim odgovorima grupama po nazivu' 11 | replies: 12 | default_reply: 13 | title: "Moj prvi pripremljeni odgovor" 14 | body: | 15 | Ovo je primjer pripremljenog odgovora. 16 | Možeš koristiti **markdown** za stiliziranje svojih odgovora. Pritisni **novi** gumb za kreiranje ili **uredi** gumb za uređivanje ili brisanje već postojećeg pripremljenog odgovora. 17 | 18 | *Ovaj pripremljeni odgovor će biti dodan kada će lista odgovora biti prazna* 19 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "按标题筛选…" 11 | composer_button_text: "预设回复" 12 | title: 13 | name: "标题" 14 | invalid: 标题不能为空。 15 | content: 16 | name: "内容" 17 | invalid: 内容不能为空。 18 | insert: 19 | choose: "(选择回复)" 20 | insert_button: "插入回复" 21 | new_button: "新建" 22 | edit_button: "编辑" 23 | modal_title: "插入预设回复" 24 | sort: 25 | alphabetically: "按字母顺序排序" 26 | usage: "按使用频率排序" 27 | add: 28 | modal_title: "添加预设回复" 29 | edit: 30 | modal_title: "编辑预设回复" 31 | remove_confirm: "确定要删除此预设回复吗?" 32 | back: "返回" 33 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "תגובות מוכנות" 11 | title: 12 | name: "כותרת" 13 | invalid: הכותרת לא יכולה להיות ריקה. 14 | content: 15 | name: "תוכן" 16 | invalid: התוכן לא יכול להיות ריק. 17 | insert: 18 | choose: "(נא לבחור תגובה)" 19 | insert_button: "הוספת תגובה" 20 | new_button: "חדש" 21 | edit_button: "עריכה" 22 | modal_title: "הוספת תגובה מוכנה" 23 | sort: 24 | alphabetically: "מיון לפי האלפבית" 25 | usage: "מיון לפי תדירות השימוש" 26 | add: 27 | modal_title: "הוספת תגובה מוכנה" 28 | edit: 29 | modal_title: "עריכת תגובה מוכנה" 30 | back: "חזרה" 31 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "タイトルでフィルター..." 11 | composer_button_text: "返信定型文" 12 | title: 13 | name: "タイトル" 14 | invalid: タイトルを空白にできません。 15 | content: 16 | name: "コンテンツ" 17 | invalid: コンテンツを空白にできません。 18 | insert: 19 | choose: "(返信を選択)" 20 | insert_button: "返信を挿入" 21 | new_button: "新規" 22 | edit_button: "編集" 23 | modal_title: "返信定型文を挿入" 24 | sort: 25 | alphabetically: "アルファベット順に並べ替え" 26 | usage: "使用頻度で並べ替え" 27 | add: 28 | modal_title: "返信定型文を追加" 29 | edit: 30 | modal_title: "返信定型文を編集" 31 | remove_confirm: "この返信定型文を削除してもよろしいですか?" 32 | back: "戻る" 33 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "پاسخ های آماده" 11 | title: 12 | name: "عنوان" 13 | invalid: عنوان نمیتواند خالی باشد 14 | content: 15 | name: "محتوا" 16 | invalid: محتوا نمیتوانتد خالی باشد. 17 | insert: 18 | choose: "(پاسخی انتخاب کن)" 19 | insert_button: "اضافه کن" 20 | new_button: "جدید" 21 | edit_button: "ویرایش" 22 | modal_title: "افزودن پاسخ آماده" 23 | sort: 24 | alphabetically: "به ترتیب حروف الفبا" 25 | usage: "به ترتیب استفاده" 26 | add: 27 | modal_title: "پاسخ آماده را اضافه کن" 28 | edit: 29 | modal_title: "پاسخ آماده را ویرایش کن" 30 | back: "بازگشت" 31 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'هل تريد إظهار زر إدراج الرد المحدَّد مسبقًا في نافذة أداة الإنشاء؟' 10 | canned_replies_groups: 'إضافة الوصول إلى الردود المحدَّدة مسبقًا لمجموعات المستخدمين من غير الأعضاء في طاقم العمل باستخدام اسم المجموعة' 11 | canned_replies_everyone_enabled: 'السماح للجميع باستخدام الردود المحدَّدة مسبقًا' 12 | canned_replies_everyone_can_edit: 'السماح للجميع بإنشاء الردود المحدَّدة مسبقًا وتعديلها وحذفها' 13 | replies: 14 | default_reply: 15 | title: "أول رد محدَّد مسبقًا لي" 16 | body: | 17 | هذا مثال للرد المحدَّد مسبقًا. 18 | يمكنك استخدام **Markdown** لتغيير نمط ردودك. انقر على زر **جديد** لإنشاء ردود جديدة أو زر **تعديل** لتعديل أو إزالة رد محدَّد مسبقًا حالي. 19 | -------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: 'Mostra el botó d''inserció de resposta enllaunada en la finestra de redacció?' 10 | canned_replies_groups: 'Afegeix accés a les respostes enllaunades per a grups que no siguin de l''equip responsable, per nom de grup' 11 | replies: 12 | default_reply: 13 | title: "La meva primera resposta enllaunada" 14 | body: | 15 | Aquest és un exemple de resposta enllaunada. 16 | Podeu fer servir **markdown*** per a formatar les respostes. Feu clic en el botó **Nova** per a crear noves respostes o el botó **Edita** per a editar o eliminar una resposta enllaunada existent. 17 | 18 | * Aquesta resposta enllaunada s'afegirà quan la llista de respostes estigui buida.* 19 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "제목으로 필터링..." 11 | composer_button_text: "미리 준비된 답변" 12 | title: 13 | name: "제목" 14 | invalid: 제목은 비워 둘 수 없습니다. 15 | content: 16 | name: "사이트 컨텐츠" 17 | invalid: 내용은 비워 둘 수 없습니다. 18 | insert: 19 | choose: "(답장 선택)" 20 | insert_button: "답장 삽입" 21 | new_button: "새값" 22 | edit_button: "수정" 23 | modal_title: "미리 준비된 답변 삽입" 24 | sort: 25 | alphabetically: "알파벳순으로 정렬" 26 | usage: "사용 빈도별로 정렬" 27 | add: 28 | modal_title: "미리 준비된 답변 추가" 29 | edit: 30 | modal_title: "미리 준비된 답변 수정" 31 | remove_confirm: "정말로 현재의 미리 준비된 답장을 삭제 하시겠습니까?" 32 | back: "뒤로" 33 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'להציג כפתור הוספת תגובה מוכנה בחלון כתיבת ההודעה?' 10 | canned_replies_groups: 'הוספת גישה לתגובות מוכנות לקבוצות שאינן לחברי סגל לפי שם קבוצה' 11 | canned_replies_everyone_enabled: 'לאפשר לכולם להשתמש בתגובות מוכנות' 12 | canned_replies_everyone_can_edit: 'לכולם יש את האפשרות ליצור, לערוך ולמחוק תגובות מוכנות מראש' 13 | replies: 14 | default_reply: 15 | title: "התגובה המוכנה הראשונה שלי" 16 | body: | 17 | זו הודעה מוכנה לדוגמה. 18 | ניתן להשתמש ב**סימון** כדי לסגנן את התגובות שלך. יש ללחוץ על הכפתור **חדש** כדי ליצור תגובות חדשות או על הכפתור **עריכה** כדי לערוך או להסיר תגובה מוכנה קיימת. 19 | 20 | *תגובה מוכנה זו תתווסף כאשר רשימת התגובות ריקה.* 21 | -------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: 'Ցուցադրե՞լ Մուտքագրել պահեստավորված պատասխան կոճակը կոմպոզերի պատուհանում:' 10 | canned_replies_groups: 'Ավելացնել պահեստավորված պատասխանների հասանելիություն անձնակազմի անդամ չհանդիսացող խմբերի համար՝ ըստ խմբի անվանման' 11 | replies: 12 | default_reply: 13 | title: "Իմ առաջին պահեստավորված պատասխանը" 14 | body: | 15 | Սա պահեստավորված պատասխանի օրինակ է: 16 | Դուք կարող եք օգտագործել **markdown** Ձեր պատասխանների ոճը կառավարելու համար: Սեղմեք **նոր** կոճակընոր պատասխաններ ստեղծելու կամ **խմբագրել** կոճակը՝ գոյություն ունեցող պահեստավորված պատասխանը խմբագրելու կամ հեռացնելու համար: 17 | 18 | *Այս պահեստավորված պատասխանը կավելացվի, որբ պատասխանների ցանկը լինի դատարկ:* 19 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "طہ شدہ جوابات" 11 | title: 12 | name: "عنوان" 13 | invalid: عنوان خالی نہیں ھو سکتا 14 | content: 15 | name: "متن" 16 | invalid: متن خالی نہیں ھو سکتا 17 | insert: 18 | choose: "(ایک جواب کا انتخاب کیجیے)" 19 | insert_button: "جواب شامل کریں" 20 | new_button: "نیا" 21 | edit_button: "ترمیم کریں" 22 | modal_title: "طہ شدہ جواب شامل کریں" 23 | sort: 24 | alphabetically: "حروف تہجی ترتیب دیں" 25 | usage: "استعمال کی تعدد کے حساب سے ترتیب دیں" 26 | add: 27 | modal_title: "طہ شدہ جواب شامل کریں" 28 | edit: 29 | modal_title: "طہ شدہ جواب میں ترمیم کریں" 30 | back: "واپس" 31 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "Pripravljeni odgovori" 11 | title: 12 | name: "Naslov" 13 | invalid: Naslov ne sme biti prazen. 14 | content: 15 | name: "Vsebina" 16 | invalid: Vsebina ne sme biti prazna. 17 | insert: 18 | choose: "(izberi odgovor)" 19 | insert_button: "Vstavi odgovor" 20 | new_button: "Nov" 21 | edit_button: "Uredi" 22 | modal_title: "Vstavi pripravljen odgovor" 23 | sort: 24 | alphabetically: "Uredi po abecednem redu" 25 | usage: "Uredi po pogostnosti uporabe" 26 | add: 27 | modal_title: "Dodaj pripravljen odgovor" 28 | edit: 29 | modal_title: "Uredi pripravljen odgovor" 30 | back: "Nazaj" 31 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "Eltett válaszok" 11 | title: 12 | name: "Cím" 13 | invalid: Cím nem lehet üres. 14 | content: 15 | name: "Tartalom" 16 | invalid: Tartalom nem lehet üres. 17 | insert: 18 | choose: "(válasz választása)" 19 | insert_button: "Válasz Beillesztése" 20 | new_button: "Új" 21 | edit_button: "Szerkesztés" 22 | modal_title: "Eltett Válasz Beillesztése" 23 | sort: 24 | alphabetically: "Rendezés betűrendbe" 25 | usage: "Rendezés használat gyakorisága szerint" 26 | add: 27 | modal_title: "Eltett Válasz Hozzáadása" 28 | edit: 29 | modal_title: "Eltett Válasz Szerkesztése" 30 | back: "Vissza" 31 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "Răspunsuri predefinite" 11 | title: 12 | name: "Titlu" 13 | invalid: Titlul nu poate fi gol. 14 | content: 15 | name: "Conținut" 16 | invalid: Conținutul nu poate fi gol. 17 | insert: 18 | choose: "(alege un răspuns)" 19 | insert_button: "Inserează răspuns" 20 | new_button: "Nou" 21 | edit_button: "Editează" 22 | modal_title: "Inserează răspuns predefinit" 23 | sort: 24 | alphabetically: "Sortează alfabetic" 25 | usage: "Sortează după frecvența de utilizare" 26 | add: 27 | modal_title: "Adaugă răspuns predefinit" 28 | edit: 29 | modal_title: "Editează răspunsul predefinit" 30 | back: "Înapoi" 31 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "Готові відповіді" 11 | title: 12 | name: "Назва" 13 | invalid: Назва не може бути порожньою. 14 | content: 15 | name: "Вміст" 16 | invalid: Вміст не може бути порожнім. 17 | insert: 18 | choose: "(виберіть відповідь)" 19 | insert_button: "Вставити відповідь" 20 | new_button: "Новий" 21 | edit_button: "Редагувати" 22 | modal_title: "Вставити готову відповідь" 23 | sort: 24 | alphabetically: "Сортування за алфавітом" 25 | usage: "Сортувати за частотою використання" 26 | add: 27 | modal_title: "Додати готову відповідь" 28 | edit: 29 | modal_title: "Редагувати готову відповідь" 30 | back: "Назад" 31 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "Sugerowane odpowiedzi" 11 | title: 12 | name: "Tytuł" 13 | invalid: Tytuł nie może być pusty. 14 | content: 15 | name: "Teksty" 16 | invalid: Zawartość nie może być pusta. 17 | insert: 18 | choose: "(wybierz odpowiedź)" 19 | insert_button: "Wstaw odpowiedź" 20 | new_button: "Nowa" 21 | edit_button: "Edytuj" 22 | modal_title: "Wstaw sugerowaną odpowiedź" 23 | sort: 24 | alphabetically: "Sortuj alfabetycznie" 25 | usage: "Sortuj wg częstotliwości używania" 26 | add: 27 | modal_title: "Dodaj sugerowaną odpowiedź" 28 | edit: 29 | modal_title: "Edytuj sugerowaną odpowiedź" 30 | back: "Wstecz" 31 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "Preddefinirani odgovori" 11 | title: 12 | name: "Naslov" 13 | invalid: Naslov ne može biti prazan. 14 | content: 15 | name: "Sadržaj" 16 | invalid: Sadržaj ne može biti prazan. 17 | insert: 18 | choose: "(izaberite odgovor)" 19 | insert_button: "Unesite odgovor" 20 | new_button: "Novo" 21 | edit_button: "Izmijeni" 22 | modal_title: "Umetnite predefinirani odgovor" 23 | sort: 24 | alphabetically: "Poredaj po abecedi" 25 | usage: "Poredaj prema učestalosti upotrebe" 26 | add: 27 | modal_title: "Dodaj predefinirani odgovor" 28 | edit: 29 | modal_title: "Uredi predefinirani odgovor" 30 | back: "Natrag" 31 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "Konzervirani odgovori" 11 | title: 12 | name: "Naslov" 13 | invalid: Naslov ne može biti prazan. 14 | content: 15 | name: "Sadržaj" 16 | invalid: Sadržaj ne može biti prazan. 17 | insert: 18 | choose: "(odaberite odgovor)" 19 | insert_button: "Umetni odgovor" 20 | new_button: "New" 21 | edit_button: "Izmijeni" 22 | modal_title: "Umetni Kozervirani Odgovor" 23 | sort: 24 | alphabetically: "Sortiraj po abecednom redu" 25 | usage: "Sortiraj po frekvenciji upotrebe" 26 | add: 27 | modal_title: "Dodaj Konzervirani Odgovor" 28 | edit: 29 | modal_title: "Uredi Konzervirani Odgovor" 30 | back: "Prethodno" 31 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "Respostes enllaunades" 11 | title: 12 | name: "Títol" 13 | invalid: El títol no pot estar buit. 14 | content: 15 | name: "Contingut" 16 | invalid: El contingut no pot estar buit. 17 | insert: 18 | choose: "(trieu una resposta)" 19 | insert_button: "Insereix una resposta" 20 | new_button: "Nou" 21 | edit_button: "Edita" 22 | modal_title: "Introduïu una resposta enllaunada" 23 | sort: 24 | alphabetically: "Ordena alfabèticament" 25 | usage: "Ordena per freqüència d'ús" 26 | add: 27 | modal_title: "Afegeix una resposta enllaunada" 28 | edit: 29 | modal_title: "Edita la resposta enllaunada" 30 | back: "Enrere" 31 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/templates/connectors/editor-preview/canned-replies.hbs: -------------------------------------------------------------------------------- 1 | {{#if cannedVisible}} 2 |
3 | 4 | {{#if canEdit}} 5 |
6 | 12 | 17 | 18 | 23 |
24 | {{/if}} 25 | 26 | {{#each filteredReplies as |r|}} 27 | 28 | {{/each}} 29 |
30 |
31 | {{/if}} -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "Majibu ambayo yameifadhiwa" 11 | title: 12 | name: "Kichwa cha Habari" 13 | invalid: Kichwa cha habari lazima kiwe na taarifa. 14 | content: 15 | name: "Maudhui" 16 | invalid: Sehemu ya maudhui lazima iwe na taarifa. 17 | insert: 18 | choose: "(chagua jibu)" 19 | insert_button: "Andika Jibu Lako" 20 | new_button: "Mpya" 21 | edit_button: "Hariri" 22 | modal_title: "Chomeka Jibu Lililohifadhiwa" 23 | sort: 24 | alphabetically: "Panga kwa alfabeti" 25 | usage: "Chuja kwa mara ya utumiaji" 26 | add: 27 | modal_title: "Ongeza jibu lililohifadhiwa" 28 | edit: 29 | modal_title: "Hariri Taarifa zilizohifadhiwa" 30 | back: "Iliyopita" 31 | -------------------------------------------------------------------------------- /spec/plugin_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe DiscourseCannedReplies do 6 | let(:user) { Fabricate(:user) } 7 | let(:admin) { Fabricate(:admin) } 8 | let(:group) { Fabricate(:group) } 9 | 10 | it "works for staff and users in group" do 11 | SiteSetting.canned_replies_groups = group.name 12 | expect(admin.can_use_canned_replies?).to eq(true) 13 | expect(user.can_use_canned_replies?).to eq(false) 14 | 15 | group.add(user) 16 | expect(user.reload.can_use_canned_replies?).to eq(true) 17 | end 18 | 19 | it "works for everyone when enabled" do 20 | expect(user.can_use_canned_replies?).to eq(false) 21 | 22 | SiteSetting.canned_replies_everyone_enabled = true 23 | expect(user.reload.can_use_canned_replies?).to eq(true) 24 | end 25 | 26 | it "allows everyone to edit when enabled" do 27 | expect(user.can_edit_canned_replies?).to eq(false) 28 | 29 | SiteSetting.canned_replies_everyone_can_edit = true 30 | expect(user.reload.can_edit_canned_replies?).to eq(true) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Pokazuj przycisk wstawiania sugerowanej odpowiedzi w oknie edytora?' 10 | canned_replies_groups: 'Dodaj dostęp do sugerowanych odpowiedzi dla grup o nazwie' 11 | canned_replies_everyone_enabled: 'Pozwól wszystkim na używanie gotowych odpowiedzi' 12 | canned_replies_everyone_can_edit: 'Pozwól wszystkim tworzyć, edytować i usuwać gotowe odpowiedzi' 13 | replies: 14 | default_reply: 15 | title: "Moja pierwsza sugerowana odpowiedź" 16 | body: | 17 | To jest przykład sugerowanej odpowiedzi. 18 | Możesz użyć **markdown**, by stylizować swoje odpowiedzi. Naciśnij przycisk **nowa**, by stworzyć nową odpowiedź lub **edytuj*, by edytować lub usunąć sugerowaną odpowiedź. 19 | 20 | *Ta odpowiedź sugerowana będzie dodana, gdy lista odpowiedzi jest pusta.* 21 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Vill du visa knappen Infoga förberett svar i redigeringsfönstret?' 10 | canned_replies_groups: 'Lägg till åtkomst till förberedda svar för icke-personalgrupper efter gruppnamn' 11 | canned_replies_everyone_enabled: 'Tillåt att alla använder förberedda svar' 12 | canned_replies_everyone_can_edit: 'Tillåt att alla skapar, ändrar eller raderar förberedda svar' 13 | replies: 14 | default_reply: 15 | title: "Mitt första förberedda svar" 16 | body: | 17 | Detta är ett exempel på ett förberett svar. 18 | Du kan använda **markdown** för att utforma dina svar. Klicka på knappen **new** för att skapa nya svar eller på knappen **edit** för att redigera eller ta bort ett befintligt förberett svar. 19 | 20 | *Detta förberedda svar läggs till när svarslistan är tom.* 21 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Yazar penceresinde eklenen hazır yanıt düğmesini göster?' 10 | canned_replies_groups: 'Personel olmayan gruplar için hazır cevaplara grup adına göre erişim ekleyin' 11 | canned_replies_everyone_enabled: 'Herkesin hazır cevapları kullanmasına izin ver' 12 | canned_replies_everyone_can_edit: 'Herkesin hazır cevaplar oluşturmasına, düzenlemesine ve silmesine izin ver' 13 | replies: 14 | default_reply: 15 | title: "İlk hazır yanıtım" 16 | body: | 17 | Bu örnek bir hazır yanıttır. 18 | Cevaplarınızı şekillendirmek için ** markdown ** kullanabilirsiniz. Yeni yanıtlar yaratmak için ** yeni ** düğmesini veya mevcut bir hazır yanıtı düzenlemek veya kaldırmak için ** düzenle ** düğmesini tıklayın. 19 | 20 | * Bu hazır yanıt, yanıtlar listesi boşken eklenecektir. * 21 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | composer_button_text: "Պահեստավորված պատասխաններ" 11 | title: 12 | name: "Վերնագիր" 13 | invalid: 'Վերնագիրը չի կարող դատարկ լինել:' 14 | content: 15 | name: "Բովանդակություն" 16 | invalid: 'Բովանդակությունը չի կարող դատարկ լինել:' 17 | insert: 18 | choose: "(ընտրեք պատասխան)" 19 | insert_button: "Մուտքագրել Պատասխան" 20 | new_button: "Նոր" 21 | edit_button: "Խմբագրել" 22 | modal_title: "Մուտքագրեք Պահեստավորված Պատասխանը" 23 | sort: 24 | alphabetically: "Դասավորել այբբենական կարգով" 25 | usage: "Դասավորել ըստ օգտագործման հաճախականության" 26 | add: 27 | modal_title: "Ավելացնել Պահեստավորված Պատասխան" 28 | edit: 29 | modal_title: "Խմբագրել Պահեստավորված Պատասխանը" 30 | back: "Ետ" 31 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Başlığa göre filtrele" 11 | composer_button_text: "Hazır cevaplar" 12 | title: 13 | name: "Başlık" 14 | invalid: Başlık boş olamaz. 15 | content: 16 | name: "İçerik" 17 | invalid: İçerik boş olamaz. 18 | insert: 19 | choose: "(bir yanıt seçin)" 20 | insert_button: "Yanıt Ekle" 21 | new_button: "Yeni" 22 | edit_button: "Düzenle" 23 | modal_title: "Hazır Yanıt Ekle" 24 | sort: 25 | alphabetically: "Alfabetik olarak sırala" 26 | usage: "Kullanım sıklığına göre sırala" 27 | add: 28 | modal_title: "Hazır Yanıt Ekle" 29 | edit: 30 | modal_title: "Hazır Yanıtı Düzenle" 31 | remove_confirm: "Bu hazır yanıtı silmek istediğinizden emin misiniz?" 32 | back: "Geri" 33 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Отображать кнопку вставки готовых ответов в редакторе' 10 | canned_replies_groups: 'Добавить доступ к готовым ответам для групп, не связанных с персоналом (по имени группы)' 11 | canned_replies_everyone_enabled: 'Разрешать всем использовать готовые ответы' 12 | canned_replies_everyone_can_edit: 'Разрешать всем создавать, редактировать и удалять готовые ответы' 13 | replies: 14 | default_reply: 15 | title: "Мой первый готовый ответ" 16 | body: | 17 | Это пример готового ответа. 18 | Можно использовать разметку **markdown** в тексте готовых ответов. Нажмите на кнопку **Новый**, чтобы создать новый ответ, или на кнопку **Редактировать**, чтобы отредактировать или удалить существующий. 19 | 20 | *Этот готовый ответ добавится автоматически, пока список готовых ответов еще пуст.* 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | json (2.6.2) 6 | parallel (1.22.1) 7 | parser (3.1.2.1) 8 | ast (~> 2.4.1) 9 | prettier_print (1.2.0) 10 | rainbow (3.1.1) 11 | regexp_parser (2.6.0) 12 | rexml (3.2.5) 13 | rubocop (1.36.0) 14 | json (~> 2.3) 15 | parallel (~> 1.10) 16 | parser (>= 3.1.2.1) 17 | rainbow (>= 2.2.2, < 4.0) 18 | regexp_parser (>= 1.8, < 3.0) 19 | rexml (>= 3.2.5, < 4.0) 20 | rubocop-ast (>= 1.20.1, < 2.0) 21 | ruby-progressbar (~> 1.7) 22 | unicode-display_width (>= 1.4.0, < 3.0) 23 | rubocop-ast (1.21.0) 24 | parser (>= 3.1.1.0) 25 | rubocop-discourse (3.0) 26 | rubocop (>= 1.1.0) 27 | rubocop-rspec (>= 2.0.0) 28 | rubocop-rspec (2.13.2) 29 | rubocop (~> 1.33) 30 | ruby-progressbar (1.11.0) 31 | syntax_tree (5.1.0) 32 | prettier_print (>= 1.2.0) 33 | unicode-display_width (2.3.0) 34 | 35 | PLATFORMS 36 | ruby 37 | 38 | DEPENDENCIES 39 | rubocop-discourse 40 | syntax_tree 41 | 42 | BUNDLED WITH 43 | 2.1.4 44 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "التصفية حسب العنوان..." 11 | composer_button_text: "الردود الآلية" 12 | title: 13 | name: "العنوان" 14 | invalid: لا يمكن للعنوان أن يكون فارغًا. 15 | content: 16 | name: "المحتوى" 17 | invalid: لا يمكن للمحتوى أن يكون فارغًا. 18 | insert: 19 | choose: "(اختر ردًا)" 20 | insert_button: "أضف ردًا" 21 | new_button: "جديد" 22 | edit_button: "تعديل" 23 | modal_title: "إضافة رد محدَّد مسبقًا" 24 | sort: 25 | alphabetically: "الترتيب حسب الاسم" 26 | usage: "الترتيب حسب تكرار الاستخدام" 27 | add: 28 | modal_title: "إضافة رد محدَّد مسبقًا" 29 | edit: 30 | modal_title: "تعديل الرد المحدَّد مسبقًا" 31 | remove_confirm: "هل تريد بالتأكيد حذف هذا الرد المحدَّد مسبقًا؟" 32 | back: "الرجوع" 33 | -------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: 'Prikaži gumb za vstavljanje pripravljenega odgovora v oknu urejevalnika?' 10 | canned_replies_groups: 'Omogoči uporabo pripravljenih odgovorov tudi drugim poimenskim skupinam uporabnikov, ne samo osebju' 11 | canned_replies_everyone_enabled: 'Dovoli vsem, da uporabljajo pripravljene odgovore' 12 | canned_replies_everyone_can_edit: 'Dovoli vsem ustvarjanje, urejanje in brisanje pripravljenih odgovorov' 13 | replies: 14 | default_reply: 15 | title: "Moj prvi pripravljen odgovor" 16 | body: | 17 | To je primer pripravljenega odgovora. 18 | Za oblikovanje odgovorov lahko uporabite **markdown**. Kliknite na gumb **nov**, da ustvarite nove odgovore ali na gumb **uredi**, da uredite ali odstranite obstoječe odgovore. 19 | 20 | *Ta pripravljen odgovor bo dodan, če je seznam pripravljenih odgovorov prazen.* 21 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Filtrera efter titel..." 11 | composer_button_text: "Förberedda svar" 12 | title: 13 | name: "Rubrik" 14 | invalid: Rubriken kan inte vara tom. 15 | content: 16 | name: "Innehåll" 17 | invalid: Innehållet kan inte vara tomt. 18 | insert: 19 | choose: "(välj ett svar)" 20 | insert_button: "Infoga svar" 21 | new_button: "Ny" 22 | edit_button: "Redigera" 23 | modal_title: "Infoga förberett svar" 24 | sort: 25 | alphabetically: "Sortera alfabetiskt" 26 | usage: "Sortera efter användningsfrekvens" 27 | add: 28 | modal_title: "Lägg till förberett svar" 29 | edit: 30 | modal_title: "Redigera förberett svar" 31 | remove_confirm: "Är du säker på att du vill radera detta förberedda svar?" 32 | back: "Tillbaka" 33 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Knop Sjabloonantwoord invoegen weergeven in editorvenster?' 10 | canned_replies_groups: 'Toegang tot sjabloonantwoorden voor niet-medewerkergroepen toevoegen op groepsnaam' 11 | canned_replies_everyone_enabled: 'Iedereen toestaan sjabloonantwoorden te gebruiken' 12 | canned_replies_everyone_can_edit: 'Iedereen toestaan sjabloonantwoorden te maken, bewerken en verwijderen' 13 | replies: 14 | default_reply: 15 | title: "Mijn eerste sjabloonantwoord" 16 | body: | 17 | Dit is een voorbeeld van een sjabloonantwoord. 18 | Je kunt **markdown** gebruiken om stijl aan je antwoorden te geven. Klik op de knop **Nieuw** om een nieuw antwoord te maken of op de knop **Bewerken** om een bestaand sjabloonantwoord te bewerken of te verwijderen. 19 | 20 | *Dit sjabloonantwoord wordt toegevoegd wanneer de antwoordenlijst leeg is.* 21 | -------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: 'Mostrar o botão de inserção de resposta gravada na janela do compor?' 10 | canned_replies_groups: 'Adicionar acesso para as respostas gravadas para os grupos não pertencentes à equipa por nome do grupo' 11 | canned_replies_everyone_enabled: 'Permitir que todos utilizem as respostas gravadas' 12 | canned_replies_everyone_can_edit: 'Permitir que todos criem, editem e eliminem as respostas gravadas' 13 | replies: 14 | default_reply: 15 | title: "A minha primeira resposta gravada" 16 | body: | 17 | Este é um exemplo de resposta gravada. 18 | Pode utilizar a **marcação** para estilizar as suas respostas. Clique no botão **nova** para criar novas respostas ou o botão **editar** para editar ou remover uma resposta existente gravada. 19 | 20 | *Esta resposta gravada será adicionada quando a lista de respostas estiver vazia.* 21 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Filteren op titel..." 11 | composer_button_text: "Sjabloonantwoorden" 12 | title: 13 | name: "Titel" 14 | invalid: Titel mag niet leeg zijn. 15 | content: 16 | name: "Inhoud" 17 | invalid: Inhoud mag niet leeg zijn. 18 | insert: 19 | choose: "(kies een antwoord)" 20 | insert_button: "Antwoord invoegen" 21 | new_button: "Nieuw" 22 | edit_button: "Bewerken" 23 | modal_title: "Sjabloonantwoord invoegen" 24 | sort: 25 | alphabetically: "Alfabetisch sorteren" 26 | usage: "Sorteren op gebruiksfrequentie" 27 | add: 28 | modal_title: "Sjabloonantwoord toevoegen" 29 | edit: 30 | modal_title: "Sjabloonantwoord bewerken" 31 | remove_confirm: "Weet je zeker dat je dit sjabloonantwoord wilt verwijderen?" 32 | back: "Terug" 33 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Nach Titel filtern …" 11 | composer_button_text: "Antwortvorlagen" 12 | title: 13 | name: "Titel" 14 | invalid: Der Titel darf nicht leer sein. 15 | content: 16 | name: "Inhalt" 17 | invalid: Der Inhalt darf nicht leer sein. 18 | insert: 19 | choose: "(wähle eine Antwort)" 20 | insert_button: "Antwort einfügen" 21 | new_button: "Neu" 22 | edit_button: "Bearbeiten" 23 | modal_title: "Antwortvorlage einfügen" 24 | sort: 25 | alphabetically: "Alphabetisch sortieren" 26 | usage: "Sortieren nach Nutzungshäufigkeit" 27 | add: 28 | modal_title: "Antwortvorlage hinzufügen" 29 | edit: 30 | modal_title: "Antwortvorlage bearbeiten" 31 | remove_confirm: "Möchtest du wirklich diese Antwortvorlage löschen?" 32 | back: "Zurück" 33 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Suodata otsikon mukaan..." 11 | composer_button_text: "Tallennetut vastaukset" 12 | title: 13 | name: "Otsikko" 14 | invalid: Otsikko ei voi olla tyhjä. 15 | content: 16 | name: "Sisältö" 17 | invalid: Sisältö ei voi olla tyhjä. 18 | insert: 19 | choose: "(valitse vastaus)" 20 | insert_button: "Lisää vastaus" 21 | new_button: "Uusi" 22 | edit_button: "Muokkaa" 23 | modal_title: "Lisää tallennettu vastaus" 24 | sort: 25 | alphabetically: "Järjestä aakkosjärjestykseen" 26 | usage: "Järjestä käytön mukaan" 27 | add: 28 | modal_title: "Lisää tallennettu vastaus" 29 | edit: 30 | modal_title: "Muokkaa tallennettua vastausta" 31 | remove_confirm: "Haluatko varmasti poistaa tämän tallennetun vastauksen?" 32 | back: "Takaisin" 33 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Фильтр по названию..." 11 | composer_button_text: "Готовые ответы" 12 | title: 13 | name: "Название" 14 | invalid: Название не может быть пустым. 15 | content: 16 | name: "Содержание" 17 | invalid: Содержание не может быть пустым. 18 | insert: 19 | choose: "(выберите ответ)" 20 | insert_button: "Вставить ответ" 21 | new_button: "Новый" 22 | edit_button: "Редактировать" 23 | modal_title: "Вставить готовый ответ" 24 | sort: 25 | alphabetically: "Сортировать в алфавитном порядке" 26 | usage: "Сортировать по частоте использования" 27 | add: 28 | modal_title: "Добавить готовый ответ" 29 | edit: 30 | modal_title: "Редактировать готовый ответ" 31 | remove_confirm: "Вы действительно хотите удалить этот готовый ответ?" 32 | back: "Назад" 33 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Schaltfläche zum Einfügen von Antwortvorlagen im Editor-Fenster anzeigen?' 10 | canned_replies_groups: 'Zugriff auf Antwortvorlagen für Nicht-Team-Gruppen auf Basis des Gruppennamens gewähren' 11 | canned_replies_everyone_enabled: 'Jedem erlauben, Antwortvorlagen zu verwenden' 12 | canned_replies_everyone_can_edit: 'Jedem erlauben, Antwortvorlagen zu erstellen, zu bearbeiten und zu löschen' 13 | replies: 14 | default_reply: 15 | title: "Meine erste Antwortvorlage" 16 | body: | 17 | Das ist ein Beispiel für eine Antwortvorlage. 18 | Du kannst **Markdown** verwenden, um deine Antworten zu formatieren. Klicke die **Neu**-Schaltfläche an, um neue Antworten zu erstellen, bzw. die **Bearbeiten**-Schaltfläche, um bestehende Vorlagen zu bearbeiten oder zu entfernen. 19 | 20 | *Diese Antwortvorlage wird hinzugefügt, wenn die Antwortliste leer ist.* 21 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: '¿Mostrar el botón de insertar respuestas predefinidas en el editor?' 10 | canned_replies_groups: 'Añadir acceso a las respuestas predefinidas para grupos que no sean parte del staff por nombre de grupo' 11 | canned_replies_everyone_enabled: 'Permitir a todos usar respuestas predefinidas' 12 | canned_replies_everyone_can_edit: 'Permitir a todos crear, editar, y eliminar respuestas predefinidas' 13 | replies: 14 | default_reply: 15 | title: "Mi primera respuesta predefinida" 16 | body: | 17 | Este es un ejemplo de una respuesta predefinida. 18 | Puedes usar **marcado** para dar formato a tus respuestas. Haz clic en el botón **nueva** para crear nuevas respuestas o en el botón de **editar** para editar o eliminar alguna respuesta predefinida ya existente. 19 | 20 | *Esta respuesta predefinida se añadirá cuando la lista de respuestas esté vacía.* 21 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Näytä lisää tallennettu vastaus -painike viesti-ikkunassa?' 10 | canned_replies_groups: 'Lisää pääsy tallennettuihin vastauksiin muille kuin henkilökunnan ryhmille ryhmän nimen perusteella' 11 | canned_replies_everyone_enabled: 'Salli kaikkien käyttää tallennettuja vastauksia' 12 | canned_replies_everyone_can_edit: 'Salli kaikkien luoda, muokata ja poistaa tallennettuja vastauksia' 13 | replies: 14 | default_reply: 15 | title: "Ensimmäinen tallennettu vastaukseni" 16 | body: | 17 | Tämä on esimerkki tallennetusta vastauksesta. 18 | Vastausten muotoilussa voi käyttää **markdownia**. Voit aloittaa uuden vastauksen työstämisen klikkaamalla **Uusi**-painiketta. Voit muokata olemassa olevaa tallennettua vastausta tai poistaa sellaisen **Muokkaa**-painikkeen avulla. 19 | 20 | *Tämä tallennettu vastaus lisätään, kun muita tallennettuja vastauksia ei ole.* 21 | -------------------------------------------------------------------------------- /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 | site_settings: 9 | canned_replies_enabled: 'Afișezi butonul de inserare răspuns predefinit în fereastra editorului?' 10 | canned_replies_groups: 'Adaugă acces la răspunsurile predefinite pentru grupurile care nu fac parte din personal după numele grupului' 11 | canned_replies_everyone_enabled: 'Permite oricui să folosească răspunsuri predefinite' 12 | canned_replies_everyone_can_edit: 'Permite oricui să creeze, să editeze și să șteargă răspunsuri predefinite' 13 | replies: 14 | default_reply: 15 | title: "Primul meu răspuns predefinit" 16 | body: | 17 | Acesta este un exemplu de răspuns predefinit. 18 | Poţi utiliza **markdown** pentru a stiliza răspunsurile. Apasă pe butonul **nou** pentru a crea răspunsuri noi sau pe butonul **editează** pentru a edita sau a șterge un răspuns predefinit deja existent. 19 | 20 | *Acest răspuns predefinit va fi adăugat când lista de răspunsuri este goală.* 21 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Filtra per titolo..." 11 | composer_button_text: "Risposte predefinite" 12 | title: 13 | name: "Titolo" 14 | invalid: Il titolo non può essere vuoto. 15 | content: 16 | name: "Contenuto" 17 | invalid: Il contenuto non può essere vuoto. 18 | insert: 19 | choose: "(scegli una risposta)" 20 | insert_button: "Inserisci Risposta" 21 | new_button: "Nuovo" 22 | edit_button: "Modifica" 23 | modal_title: "Inserisci Risposta Predefinita" 24 | sort: 25 | alphabetically: "Ordina alfabeticamente" 26 | usage: "Ordina per frequenza di utilizzo" 27 | add: 28 | modal_title: "Aggiungi Risposta Predefinita" 29 | edit: 30 | modal_title: "Modifica Risposta Predefinita" 31 | remove_confirm: "Vuoi cancellare questa risposta predefinita?" 32 | back: "Indietro" 33 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Filtrar por título ..." 11 | composer_button_text: "Respuestas predefinidas" 12 | title: 13 | name: "Título" 14 | invalid: El título no puede estar vacío. 15 | content: 16 | name: "Contenido" 17 | invalid: El contenido no puede estar vacío. 18 | insert: 19 | choose: "(elige una respuesta)" 20 | insert_button: "Insertar respuesta" 21 | new_button: "Nuevo" 22 | edit_button: "Editar" 23 | modal_title: "Insertar respuesta predefinida" 24 | sort: 25 | alphabetically: "Ordenar alfabéticamente" 26 | usage: "Ordenar por frecuencia de uso" 27 | add: 28 | modal_title: "Añadir respuesta predefinida" 29 | edit: 30 | modal_title: "Editar respuesta predefinida" 31 | remove_confirm: "¿Seguro que quieres eliminar esta respuesta predefinida?" 32 | back: "Atrás" 33 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Mostrar botão de inserir resposta predefinida na janela do compositor?' 10 | canned_replies_groups: 'Adicionar acesso às respostas predefinidas para grupos que são sejam de membros da equipe por nome do grupo' 11 | canned_replies_everyone_enabled: 'Permitir que todas as pessoas usem respostas predefinidas' 12 | canned_replies_everyone_can_edit: 'Permitir que todas as pessoas criem, editem e excluam respostas predefinidas' 13 | replies: 14 | default_reply: 15 | title: "Minha primeira resposta predefinida" 16 | body: | 17 | Este é um exemplo de resposta predefinida. 18 | Você pode usar **markdown** para estilizar suas respostas. Clique no botão **novo** para criar novas respostas ou no botão **editar** para editar ou remover uma resposta predefinida existente. 19 | 20 | *Esta resposta predefinida será adicionada quando a lista de respostas estiver vazia.* 21 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Filtrer par titre…" 11 | composer_button_text: "Réponses prédéfinies" 12 | title: 13 | name: "Titre" 14 | invalid: Le titre ne peut pas être vide. 15 | content: 16 | name: "Contenu" 17 | invalid: Le contenu ne peut pas être vide. 18 | insert: 19 | choose: "(choisir une réponse)" 20 | insert_button: "Insérer la réponse" 21 | new_button: "Nouveau" 22 | edit_button: "Modifier" 23 | modal_title: "Insérer la réponse prédéfinie" 24 | sort: 25 | alphabetically: "Trier par ordre alphabétique" 26 | usage: "Trier par fréquence d'utilisation" 27 | add: 28 | modal_title: "Ajouter une réponse prédéfinie" 29 | edit: 30 | modal_title: "Modifier une réponse prédéfinie" 31 | remove_confirm: "Voulez-vous vraiment supprimer cette réponse prédéfinie ?" 32 | back: "Retour" 33 | -------------------------------------------------------------------------------- /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 | canned_replies: 10 | filter_hint: "Filtrar por título..." 11 | composer_button_text: "Respostas predefinidas" 12 | title: 13 | name: "Título" 14 | invalid: O título não pode ficar vazio. 15 | content: 16 | name: "Conteúdo" 17 | invalid: O conteúdo não pode ficar vazio. 18 | insert: 19 | choose: "(escolha uma resposta)" 20 | insert_button: "Inserir resposta" 21 | new_button: "Novo" 22 | edit_button: "Editar" 23 | modal_title: "Inserir resposta predefinida" 24 | sort: 25 | alphabetically: "Ordenar alfabeticamente" 26 | usage: "Ordenar por frequência de uso" 27 | add: 28 | modal_title: "Adicionar resposta predefinida" 29 | edit: 30 | modal_title: "Editar resposta predefinida" 31 | remove_confirm: "Você tem certeza de que deseja excluir esta resposta predefinida?" 32 | back: "Voltar" 33 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Mostrare il pulsante per inserire le risposte predefinite nella finestra di composizione?' 10 | canned_replies_groups: 'Aggiungi l''accesso alle risposte predefinite per i gruppi non appartenenti allo staff in base al nome del gruppo' 11 | canned_replies_everyone_enabled: 'Consenti a tutti di utilizzare le risposte predefinite' 12 | canned_replies_everyone_can_edit: 'Consenti a tutti di creare, modificare ed eliminare le risposte predefinite' 13 | replies: 14 | default_reply: 15 | title: "La mia prima risposta predefinita" 16 | body: | 17 | Questo è un esempio di risposta predefinita. 18 | Puoi usare **markdown** per formattare le tue risposte. Clicca il pulsante **nuovo** per creare nuove risposte o il pulsante **modifica** per modificare o rimuovere una risposta predefinita esistente. 19 | 20 | *Questa risposta predefinita verrà aggiunta quando la lista delle risposte è vuota.* 21 | -------------------------------------------------------------------------------- /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 | canned_replies_enabled: 'Afficher le bouton pour insérer une réponse prédéfinie dans la fenêtre de rédaction ?' 10 | canned_replies_groups: 'Ajouter un accès aux réponses prédéfinies pour les groupes ne faisant pas partie des responsables par nom de groupe' 11 | canned_replies_everyone_enabled: 'Autoriser tout le monde à utiliser les réponses prédéfinies' 12 | canned_replies_everyone_can_edit: 'Autoriser tout le monde à créer, modifier et supprimer des réponses prédéfinies' 13 | replies: 14 | default_reply: 15 | title: "Ma première réponse prédéfinie" 16 | body: | 17 | Ceci est un exemple de réponse prédéfinie. 18 | Vous pouvez utiliser le **marquage** dans vos réponses. Cliquez sur le bouton **nouveau** pour créer de nouvelles réponses ou sur le bouton **modifier** pour modifier ou supprimer une réponse prédéfinie existante. 19 | 20 | *Cette réponse prédéfinie sera ajoutée quand la liste de réponses sera vide.* 21 | -------------------------------------------------------------------------------- /assets/javascripts/initializers/add-canned-replies-ui-builder.js: -------------------------------------------------------------------------------- 1 | import { withPluginApi } from "discourse/lib/plugin-api"; 2 | import showModal from "discourse/lib/show-modal"; 3 | 4 | function initializeCannedRepliesUIBuilder(api) { 5 | api.modifyClass("controller:composer", { 6 | pluginId: "discourse-canned-replies", 7 | actions: { 8 | showCannedRepliesButton() { 9 | if (this.site.mobileView) { 10 | showModal("canned-replies").set("composerModel", this.model); 11 | } else { 12 | this.appEvents.trigger("composer:show-preview"); 13 | this.appEvents.trigger("canned-replies:show"); 14 | } 15 | }, 16 | }, 17 | }); 18 | 19 | api.addToolbarPopupMenuOptionsCallback(() => { 20 | return { 21 | id: "canned_replies_button", 22 | icon: "far-clipboard", 23 | action: "showCannedRepliesButton", 24 | label: "canned_replies.composer_button_text", 25 | }; 26 | }); 27 | } 28 | 29 | export default { 30 | name: "add-canned-replies-ui-builder", 31 | 32 | initialize(container) { 33 | const siteSettings = container.lookup("site-settings:main"); 34 | const currentUser = container.lookup("current-user:main"); 35 | if ( 36 | siteSettings.canned_replies_enabled && 37 | currentUser && 38 | currentUser.can_use_canned_replies 39 | ) { 40 | withPluginApi("0.5", initializeCannedRepliesUIBuilder); 41 | } 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /assets/javascripts/controllers/new-reply.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import ModalFunctionality from "discourse/mixins/modal-functionality"; 3 | import showModal from "discourse/lib/show-modal"; 4 | import { ajax } from "discourse/lib/ajax"; 5 | import discourseComputed from "discourse-common/utils/decorators"; 6 | import { popupAjaxError } from "discourse/lib/ajax-error"; 7 | 8 | export default Controller.extend(ModalFunctionality, { 9 | newTitle: "", 10 | newContent: "", 11 | 12 | onShow() { 13 | this.setProperties({ 14 | newTitle: "", 15 | newContent: "", 16 | }); 17 | }, 18 | 19 | @discourseComputed("newTitle", "newContent") 20 | disableSaveButton(newTitle, newContent) { 21 | return newTitle === "" || newContent === ""; 22 | }, 23 | 24 | actions: { 25 | save() { 26 | ajax("/canned_replies", { 27 | type: "POST", 28 | data: { title: this.newTitle, content: this.newContent }, 29 | }) 30 | .then(() => { 31 | this.send("closeModal"); 32 | if (this.site.mobileView) { 33 | showModal("canned-replies"); 34 | } else { 35 | this.appEvents.trigger("canned-replies:show"); 36 | } 37 | }) 38 | .catch(popupAjaxError); 39 | }, 40 | 41 | cancel() { 42 | this.send("closeModal"); 43 | if (this.site.mobileView) { 44 | showModal("canned-replies"); 45 | } else { 46 | this.appEvents.trigger("canned-replies:show"); 47 | } 48 | }, 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/templates/modal/canned-replies.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 |
10 | 18 |
19 |
20 | 21 | {{#if selectedReply}} 22 |
23 |
{{cook-text selectedReply.content}}
24 |
25 | {{/if}} 26 |
27 |
28 |
29 | 30 | -------------------------------------------------------------------------------- /assets/javascripts/components/canned-reply.js: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import showModal from "discourse/lib/show-modal"; 3 | import applyReply from "discourse/plugins/discourse-canned-replies/lib/apply-reply"; 4 | import { getOwner } from "discourse-common/lib/get-owner"; 5 | 6 | export default Component.extend({ 7 | canEdit: false, 8 | 9 | init() { 10 | this._super(...arguments); 11 | const currentUser = this.get("currentUser"); 12 | const everyoneCanEdit = 13 | this.get("siteSettings.canned_replies_everyone_enabled") && 14 | this.get("siteSettings.canned_replies_everyone_can_edit"); 15 | const currentUserCanEdit = 16 | this.get("siteSettings.canned_replies_enabled") && 17 | currentUser && 18 | currentUser.can_edit_canned_replies; 19 | const canEdit = currentUserCanEdit ? currentUserCanEdit : everyoneCanEdit; 20 | this.set("canEdit", canEdit); 21 | }, 22 | 23 | actions: { 24 | apply() { 25 | const composer = getOwner(this).lookup("controller:composer"); 26 | 27 | applyReply( 28 | this.get("reply.id"), 29 | this.get("reply.title"), 30 | this.get("reply.content"), 31 | composer.model 32 | ); 33 | 34 | this.appEvents.trigger("canned-replies:hide"); 35 | }, 36 | 37 | editReply() { 38 | const composer = getOwner(this).lookup("controller:composer"); 39 | 40 | getOwner(this).lookup("service:modal").close(); 41 | showModal("edit-reply").setProperties({ 42 | composerModel: composer.composerModel, 43 | replyId: this.get("reply.id"), 44 | replyTitle: this.get("reply.title"), 45 | replyContent: this.get("reply.content"), 46 | }); 47 | }, 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /assets/javascripts/lib/apply-reply.js: -------------------------------------------------------------------------------- 1 | import { ajax } from "discourse/lib/ajax"; 2 | import { popupAjaxError } from "discourse/lib/ajax-error"; 3 | 4 | export default function (replyId, replyTitle, replyContent, model) { 5 | // Replace variables with values. 6 | if (model) { 7 | const vars = { 8 | my_username: model.get("user.username"), 9 | my_name: model.get("user.name"), 10 | original_poster_username: model.get("topic.details.created_by.username"), 11 | original_poster_name: model.get("topic.details.created_by.name"), 12 | reply_to_username: model.get("post.username"), 13 | reply_to_name: model.get("post.name"), 14 | last_poster_username: model.get("topic.last_poster_username"), 15 | reply_to_or_last_poster_username: 16 | model.get("post.username") || model.get("topic.last_poster_username"), 17 | }; 18 | 19 | for (let key in vars) { 20 | if (vars[key]) { 21 | replyTitle = replyTitle.replace( 22 | new RegExp(`%{${key}(,fallback:.[^}]*)?}`, "g"), 23 | vars[key] 24 | ); 25 | replyContent = replyContent.replace( 26 | new RegExp(`%{${key}(,fallback:.[^}]*)?}`, "g"), 27 | vars[key] 28 | ); 29 | } else { 30 | replyTitle = replyTitle.replace( 31 | new RegExp(`%{${key},fallback:(.[^}]*)}`, "g"), 32 | "$1" 33 | ); 34 | replyTitle = replyTitle.replace(new RegExp(`%{${key}}`, "g"), ""); 35 | replyContent = replyContent.replace( 36 | new RegExp(`%{${key},fallback:(.[^}]*)}`, "g"), 37 | "$1" 38 | ); 39 | replyContent = replyContent.replace(new RegExp(`%{${key}}`, "g"), ""); 40 | } 41 | } 42 | } 43 | 44 | // Finally insert canned reply. 45 | model.appEvents.trigger("composer:insert-block", replyContent); 46 | if (model && !model.title) { 47 | model.set("title", replyTitle); 48 | } 49 | 50 | ajax(`/canned_replies/${replyId}/use`, { 51 | type: "PATCH", 52 | }).catch(popupAjaxError); 53 | } 54 | -------------------------------------------------------------------------------- /plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # name: discourse-canned-replies 4 | # about: Add canned replies through the composer 5 | # version: 1.2 6 | # authors: Jay Pfaffman and André Pereira 7 | # url: https://github.com/discourse/discourse-canned-replies 8 | # transpile_js: true 9 | 10 | enabled_site_setting :canned_replies_enabled 11 | 12 | register_asset "stylesheets/canned-replies.scss" 13 | 14 | register_svg_icon "far-clipboard" if respond_to?(:register_svg_icon) 15 | 16 | module ::DiscourseCannedReplies 17 | PLUGIN_NAME = "discourse-canned-replies" 18 | STORE_NAME = "replies" 19 | end 20 | 21 | require_relative "lib/discourse_canned_replies/engine" 22 | 23 | after_initialize do 24 | AdminDashboardData.add_problem_check do 25 | "The discourse-canned-replies plugin is no longer supported. Please migrate to the new discourse-templates plugin and uninstall discourse-canned-replies." 26 | end 27 | 28 | require_relative "app/jobs/onceoff/rename_canned_replies.rb" 29 | 30 | add_to_class(:user, :can_edit_canned_replies?) do 31 | return true if staff? 32 | return true if SiteSetting.canned_replies_everyone_can_edit 33 | group_list = SiteSetting.canned_replies_groups.split("|").map(&:downcase) 34 | groups.any? { |group| group_list.include?(group.name.downcase) } 35 | end 36 | 37 | add_to_class(:user, :can_use_canned_replies?) do 38 | return true if staff? 39 | return true if SiteSetting.canned_replies_everyone_enabled 40 | group_list = SiteSetting.canned_replies_groups.split("|").map(&:downcase) 41 | groups.any? { |group| group_list.include?(group.name.downcase) } 42 | end 43 | 44 | add_to_class(:guardian, :can_edit_canned_replies?) { user && user.can_edit_canned_replies? } 45 | 46 | add_to_class(:guardian, :can_use_canned_replies?) { user && user.can_use_canned_replies? } 47 | 48 | add_to_serializer(:current_user, :can_use_canned_replies) { object.can_use_canned_replies? } 49 | 50 | add_to_serializer(:current_user, :can_edit_canned_replies) { object.can_edit_canned_replies? } 51 | end 52 | -------------------------------------------------------------------------------- /app/controllers/discourse_canned_replies/canned_replies_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ::DiscourseCannedReplies 4 | class CannedRepliesController < ::ApplicationController 5 | requires_plugin DiscourseCannedReplies::PLUGIN_NAME 6 | before_action :ensure_logged_in 7 | before_action :ensure_canned_replies_enabled 8 | skip_before_action :check_xhr 9 | 10 | def index 11 | user_id = current_user.id 12 | replies = DiscourseCannedReplies::Reply.all(user_id) 13 | 14 | render json: { replies: replies } 15 | end 16 | 17 | def create 18 | guardian.ensure_can_edit_canned_replies! 19 | 20 | title = params.require(:title) 21 | content = params.require(:content) 22 | user_id = current_user.id 23 | 24 | record = DiscourseCannedReplies::Reply.add(user_id, title, content) 25 | render json: record 26 | end 27 | 28 | def update 29 | guardian.ensure_can_edit_canned_replies! 30 | 31 | reply_id = params.require(:id) 32 | title = params.require(:title) 33 | content = params.require(:content) 34 | user_id = current_user.id 35 | 36 | record = DiscourseCannedReplies::Reply.edit(user_id, reply_id, title, content) 37 | render json: record 38 | end 39 | 40 | def destroy 41 | guardian.ensure_can_edit_canned_replies! 42 | 43 | reply_id = params.require(:id) 44 | user_id = current_user.id 45 | record = DiscourseCannedReplies::Reply.remove(user_id, reply_id) 46 | 47 | render json: record 48 | end 49 | 50 | def reply 51 | reply_id = params.require(:id) 52 | user_id = current_user.id 53 | 54 | record = DiscourseCannedReplies::Reply.get_reply(user_id, reply_id) 55 | render json: record 56 | end 57 | 58 | def use 59 | reply_id = params.require(:id) 60 | user_id = current_user.id 61 | record = DiscourseCannedReplies::Reply.use(user_id, reply_id) 62 | 63 | render json: record 64 | end 65 | 66 | private 67 | 68 | def ensure_canned_replies_enabled 69 | guardian.ensure_can_use_canned_replies! 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /assets/javascripts/controllers/edit-reply.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import ModalFunctionality from "discourse/mixins/modal-functionality"; 3 | import showModal from "discourse/lib/show-modal"; 4 | import { ajax } from "discourse/lib/ajax"; 5 | import discourseComputed from "discourse-common/utils/decorators"; 6 | import { popupAjaxError } from "discourse/lib/ajax-error"; 7 | import I18n from "I18n"; 8 | import { inject as service } from "@ember/service"; 9 | 10 | export default Controller.extend(ModalFunctionality, { 11 | dialog: service(), 12 | replyTitle: "", 13 | replyContent: "", 14 | replyId: "", 15 | saving: null, 16 | 17 | onShow() { 18 | this.set("saving", null); 19 | }, 20 | 21 | @discourseComputed("saving") 22 | savingLabel(saving) { 23 | return saving === null ? "save" : saving ? "saving" : "saved"; 24 | }, 25 | 26 | @discourseComputed("replyTitle", "replyContent", "saving") 27 | disableSaveButton(replyTitle, replyContent, saving) { 28 | return saving || replyTitle === "" || replyContent === ""; 29 | }, 30 | 31 | actions: { 32 | save() { 33 | this.set("saving", true); 34 | 35 | ajax(`/canned_replies/${this.replyId}`, { 36 | type: "PATCH", 37 | data: { 38 | title: this.replyTitle, 39 | content: this.replyContent, 40 | }, 41 | }) 42 | .catch(popupAjaxError) 43 | .finally(() => { 44 | this.set("saving", false); 45 | this.appEvents.trigger("canned-replies:show"); 46 | }); 47 | }, 48 | 49 | remove() { 50 | this.dialog.deleteConfirm({ 51 | message: I18n.t("canned_replies.edit.remove_confirm"), 52 | didConfirm: () => { 53 | return ajax(`/canned_replies/${this.replyId}`, { 54 | type: "DELETE", 55 | }) 56 | .then(() => { 57 | this.send("closeModal"); 58 | if (this.site.mobileView) { 59 | showModal("canned-replies"); 60 | } else { 61 | this.appEvents.trigger("canned-replies:show"); 62 | } 63 | }) 64 | .catch(popupAjaxError); 65 | }, 66 | }); 67 | }, 68 | 69 | cancel() { 70 | this.send("closeModal"); 71 | if (this.site.mobileView) { 72 | showModal("canned-replies"); 73 | } else { 74 | this.appEvents.trigger("canned-replies:show"); 75 | } 76 | }, 77 | }, 78 | }); 79 | -------------------------------------------------------------------------------- /assets/stylesheets/canned-replies.scss: -------------------------------------------------------------------------------- 1 | .canned-replies-modal { 2 | max-width: 1080px; 3 | .canned-replies-sort-usage, 4 | .canned-replies-sort-alpha { 5 | display: inline-block; 6 | 7 | .btn { 8 | float: none; 9 | } 10 | } 11 | 12 | .content { 13 | margin-top: 10px; 14 | } 15 | 16 | .btn.link, 17 | .btn.local-dates { 18 | display: none; 19 | } 20 | } 21 | 22 | .canned-replies-edit-bar { 23 | display: flex; 24 | border-bottom: 1px solid var(--primary-low); 25 | margin-bottom: 1em; 26 | padding-bottom: 1em; 27 | input { 28 | margin: 0 0 0 1em; 29 | } 30 | .close { 31 | margin-left: auto; 32 | } 33 | } 34 | 35 | .canned-replies-footer { 36 | align-items: stretch; 37 | margin-top: 3px; 38 | } 39 | 40 | .canned-reply-title { 41 | display: flex; 42 | align-items: center; 43 | button { 44 | margin-left: 0.5em; 45 | } 46 | 47 | &:hover { 48 | background-color: var(--highlight-medium); 49 | cursor: pointer; 50 | } 51 | } 52 | 53 | .canned-reply-title-text { 54 | font-weight: bold; 55 | margin-left: 0.5em; 56 | @include ellipsis; 57 | } 58 | 59 | #reply-control { 60 | .canned-replies { 61 | height: 100%; 62 | width: 100%; 63 | } 64 | 65 | .canned-reply { 66 | padding-bottom: 1em; 67 | 68 | .canned-reply-title { 69 | display: flex; 70 | align-items: center; 71 | 72 | .canned-reply-title-text { 73 | max-width: 75%; 74 | } 75 | 76 | .actions { 77 | margin-left: auto; 78 | } 79 | } 80 | } 81 | } 82 | 83 | .canned-replies-form-title-input { 84 | width: 100%; 85 | } 86 | 87 | .canned-replies-content { 88 | background: var(--primary-very-low); 89 | padding: 0.75em; 90 | margin-top: 0.5em; 91 | box-sizing: border-box; 92 | p:first-of-type { 93 | margin-top: 0; 94 | } 95 | p:last-of-type { 96 | margin-bottom: 0; 97 | } 98 | } 99 | 100 | .desktop-view { 101 | .canned-replies-edit-back { 102 | // This button is only needed on mobile because it's a modal within a modal 103 | display: none; 104 | } 105 | } 106 | 107 | .mobile-view .canned-replies-modal { 108 | min-height: 50vh; 109 | box-sizing: border-box; 110 | width: 100vw; 111 | overflow-x: hidden; 112 | display: flex; 113 | flex-direction: column; 114 | 115 | > div, 116 | form, 117 | .canned-replies-form-content-wrapper { 118 | // for making the edit input full-height 119 | flex: 1 1 100%; 120 | display: flex; 121 | flex-direction: column; 122 | } 123 | 124 | .content { 125 | max-height: 50vh; 126 | } 127 | 128 | .select-kit { 129 | width: 100%; 130 | } 131 | .d-editor-preview-wrapper { 132 | display: none; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /assets/javascripts/controllers/canned-replies.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import ModalFunctionality from "discourse/mixins/modal-functionality"; 3 | import showModal from "discourse/lib/show-modal"; 4 | import { ajax } from "discourse/lib/ajax"; 5 | import { observes } from "discourse-common/utils/decorators"; 6 | import { popupAjaxError } from "discourse/lib/ajax-error"; 7 | import applyReply from "discourse/plugins/discourse-canned-replies/lib/apply-reply"; 8 | 9 | export default Controller.extend(ModalFunctionality, { 10 | selectedReply: null, 11 | selectedReplyId: "", 12 | loadingReplies: true, 13 | canEdit: false, 14 | 15 | init() { 16 | this._super(...arguments); 17 | 18 | const currentUser = this.get("currentUser"); 19 | const everyoneCanEdit = 20 | this.siteSettings.canned_replies_everyone_enabled && 21 | this.siteSettings.canned_replies_everyone_can_edit; 22 | const currentUserCanEdit = 23 | this.siteSettings.canned_replies_enabled && 24 | currentUser && 25 | currentUser.can_edit_canned_replies; 26 | const canEdit = currentUserCanEdit ? currentUserCanEdit : everyoneCanEdit; 27 | this.set("canEdit", canEdit); 28 | 29 | this.replies = []; 30 | }, 31 | 32 | @observes("selectedReplyId") 33 | _updateSelection() { 34 | this.selectionChange(); 35 | }, 36 | 37 | onShow() { 38 | ajax("/canned_replies") 39 | .then((results) => { 40 | this.set("replies", results.replies); 41 | // trigger update of the selected reply 42 | this.selectionChange(); 43 | }) 44 | .catch(popupAjaxError) 45 | .finally(() => this.set("loadingReplies", false)); 46 | }, 47 | 48 | selectionChange() { 49 | const localSelectedReplyId = this.get("selectedReplyId"); 50 | 51 | let localSelectedReply = ""; 52 | this.get("replies").forEach((entry) => { 53 | if (entry.id === localSelectedReplyId) { 54 | localSelectedReply = entry; 55 | return; 56 | } 57 | }); 58 | 59 | this.set("selectedReply", localSelectedReply); 60 | }, 61 | 62 | actions: { 63 | apply() { 64 | applyReply( 65 | this.get("selectedReplyId"), 66 | this.selectedReply.title, 67 | this.selectedReply.content, 68 | this.composerModel 69 | ); 70 | 71 | this.send("closeModal"); 72 | }, 73 | 74 | newReply() { 75 | this.send("closeModal"); 76 | 77 | showModal("new-reply").set("newContent", this.composerModel.reply); 78 | }, 79 | 80 | editReply() { 81 | this.send("closeModal"); 82 | 83 | showModal("edit-reply").setProperties({ 84 | replyId: this.selectedReplyId, 85 | replyTitle: this.get("selectedReply.title"), 86 | replyContent: this.get("selectedReply.content"), 87 | }); 88 | }, 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /app/models/discourse_canned_replies/reply.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ::DiscourseCannedReplies 4 | class Reply 5 | def self.add(user_id, title, content) 6 | id = SecureRandom.hex(16) 7 | record = { id: id, title: title, content: content } 8 | 9 | replies = 10 | PluginStore.get(DiscourseCannedReplies::PLUGIN_NAME, DiscourseCannedReplies::STORE_NAME) || 11 | {} 12 | 13 | replies[id] = record 14 | PluginStore.set( 15 | DiscourseCannedReplies::PLUGIN_NAME, 16 | DiscourseCannedReplies::STORE_NAME, 17 | replies, 18 | ) 19 | 20 | record 21 | end 22 | 23 | def self.edit(user_id, reply_id, title, content) 24 | record = { id: reply_id, title: title, content: content } 25 | remove(user_id, reply_id) 26 | 27 | replies = 28 | PluginStore.get(DiscourseCannedReplies::PLUGIN_NAME, DiscourseCannedReplies::STORE_NAME) || 29 | {} 30 | 31 | replies[reply_id] = record 32 | PluginStore.set( 33 | DiscourseCannedReplies::PLUGIN_NAME, 34 | DiscourseCannedReplies::STORE_NAME, 35 | replies, 36 | ) 37 | 38 | record 39 | end 40 | 41 | def self.all(user_id) 42 | replies = 43 | PluginStore.get(DiscourseCannedReplies::PLUGIN_NAME, DiscourseCannedReplies::STORE_NAME) 44 | 45 | if replies.blank? 46 | add_default_reply 47 | replies = 48 | PluginStore.get(DiscourseCannedReplies::PLUGIN_NAME, DiscourseCannedReplies::STORE_NAME) 49 | end 50 | 51 | return [] if replies.blank? 52 | replies.values.sort_by { |reply| reply["title"] || "" } 53 | end 54 | 55 | def self.get_reply(user_id, reply_id) 56 | replies = all(user_id) 57 | 58 | replies.detect { |reply| reply["id"] == reply_id } 59 | end 60 | 61 | def self.remove(user_id, reply_id) 62 | replies = 63 | PluginStore.get(DiscourseCannedReplies::PLUGIN_NAME, DiscourseCannedReplies::STORE_NAME) 64 | replies.delete(reply_id) 65 | PluginStore.set( 66 | DiscourseCannedReplies::PLUGIN_NAME, 67 | DiscourseCannedReplies::STORE_NAME, 68 | replies, 69 | ) 70 | end 71 | 72 | def self.use(user_id, reply_id) 73 | replies = 74 | PluginStore.get(DiscourseCannedReplies::PLUGIN_NAME, DiscourseCannedReplies::STORE_NAME) 75 | reply = replies[reply_id] 76 | reply["usages"] ||= 0 77 | reply["usages"] += 1 78 | replies[reply_id] = reply 79 | PluginStore.set( 80 | DiscourseCannedReplies::PLUGIN_NAME, 81 | DiscourseCannedReplies::STORE_NAME, 82 | replies, 83 | ) 84 | end 85 | 86 | def self.add_default_reply() 87 | add(1, I18n.t("replies.default_reply.title"), I18n.t("replies.default_reply.body")) 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /assets/javascripts/connectors/editor-preview/canned-replies.js: -------------------------------------------------------------------------------- 1 | import showModal from "discourse/lib/show-modal"; 2 | import { ajax } from "discourse/lib/ajax"; 3 | import { popupAjaxError } from "discourse/lib/ajax-error"; 4 | import { getOwner } from "discourse-common/lib/get-owner"; 5 | import { schedule } from "@ember/runloop"; 6 | 7 | export default { 8 | setupComponent(args, component) { 9 | const currentUser = this.get("currentUser"); 10 | const everyoneCanEdit = 11 | this.get("siteSettings.canned_replies_everyone_enabled") && 12 | this.get("siteSettings.canned_replies_everyone_can_edit"); 13 | const currentUserCanEdit = 14 | this.get("siteSettings.canned_replies_enabled") && 15 | currentUser && 16 | currentUser.can_edit_canned_replies; 17 | const canEdit = currentUserCanEdit ? currentUserCanEdit : everyoneCanEdit; 18 | this.set("canEdit", canEdit); 19 | 20 | component.setProperties({ 21 | cannedVisible: false, 22 | loadingReplies: false, 23 | replies: [], 24 | filteredReplies: [], 25 | }); 26 | 27 | if (!component.appEvents.has("canned-replies:show")) { 28 | this.showCanned = () => component.send("show"); 29 | component.appEvents.on("canned-replies:show", this, this.showCanned); 30 | } 31 | 32 | if (!component.appEvents.has("canned-replies:hide")) { 33 | this.hideCanned = () => component.send("hide"); 34 | component.appEvents.on("canned-replies:hide", this, this.hideCanned); 35 | } 36 | 37 | component.addObserver("listFilter", function () { 38 | const filterTitle = component.listFilter.toLowerCase(); 39 | const filtered = component.replies 40 | .map((reply) => { 41 | /* Give a relevant score to each reply. */ 42 | reply.score = 0; 43 | if (reply.title.toLowerCase().indexOf(filterTitle) !== -1) { 44 | reply.score += 2; 45 | } else if (reply.content.toLowerCase().indexOf(filterTitle) !== -1) { 46 | reply.score += 1; 47 | } 48 | return reply; 49 | }) 50 | .filter((reply) => reply.score !== 0) // Filter irrelevant replies. 51 | .sort((a, b) => { 52 | /* Sort replies by relevance and title. */ 53 | if (a.score !== b.score) { 54 | return a.score > b.score ? -1 : 1; /* descending */ 55 | } else if (a.title !== b.title) { 56 | return a.title < b.title ? -1 : 1; /* ascending */ 57 | } 58 | return 0; 59 | }); 60 | component.set("filteredReplies", filtered); 61 | }); 62 | }, 63 | 64 | teardownComponent(component) { 65 | if (component.appEvents.has("canned-replies:show") && this.showCanned) { 66 | component.appEvents.off("canned-replies:show", this, this.showCanned); 67 | component.appEvents.off("canned-replies:hide", this, this.hideCanned); 68 | } 69 | }, 70 | 71 | actions: { 72 | show() { 73 | $("#reply-control .d-editor-preview-wrapper > .d-editor-preview").hide(); 74 | this.setProperties({ cannedVisible: true, loadingReplies: true }); 75 | 76 | ajax("/canned_replies") 77 | .then((results) => { 78 | this.setProperties({ 79 | replies: results.replies, 80 | filteredReplies: results.replies, 81 | }); 82 | }) 83 | .catch(popupAjaxError) 84 | .finally(() => { 85 | this.set("loadingReplies", false); 86 | 87 | if (this.canEdit) { 88 | schedule("afterRender", () => 89 | document.querySelector(".canned-replies-filter").focus() 90 | ); 91 | } 92 | }); 93 | }, 94 | 95 | hide() { 96 | $(".d-editor-preview-wrapper > .d-editor-preview").show(); 97 | this.set("cannedVisible", false); 98 | }, 99 | 100 | newReply() { 101 | const composer = getOwner(this).lookup("controller:composer"); 102 | getOwner(this).lookup("service:modal").close(); 103 | 104 | showModal("new-reply").set("newContent", composer.model.reply); 105 | }, 106 | }, 107 | }; 108 | -------------------------------------------------------------------------------- /test/javascripts/acceptance/canned-replies-test.js: -------------------------------------------------------------------------------- 1 | import I18n from "I18n"; 2 | import selectKit from "discourse/tests/helpers/select-kit-helper"; 3 | import { acceptance } from "discourse/tests/helpers/qunit-helpers"; 4 | import { click, fillIn, visit } from "@ember/test-helpers"; 5 | import { test } from "qunit"; 6 | import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer"; 7 | 8 | acceptance("Canned Replies", function (needs) { 9 | needs.user({ 10 | can_use_canned_replies: true, 11 | can_edit_canned_replies: true, 12 | }); 13 | needs.settings({ 14 | canned_replies_enabled: true, 15 | }); 16 | needs.pretender((server, helper) => { 17 | server.patch("/canned_replies/cd6680d7a04caaac1274e6f37429458c/use", () => { 18 | return helper.response({}); 19 | }); 20 | server.patch("/canned_replies/cd6680d7a04caaac1274e6f37429458c", () => { 21 | return helper.response({}); 22 | }); 23 | server.patch("/canned_replies/ce5fc200ab90dd0d5ac597ca9bb4708b", () => { 24 | return helper.response({}); 25 | }); 26 | server.patch("/canned_replies/ce5fc200ab90dd0d5ac597ca9bb4708b/use", () => { 27 | return helper.response({}); 28 | }); 29 | server.patch("/canned_replies/04697870e02acfef3c2130dab92fe6d8", () => { 30 | return helper.response({}); 31 | }); 32 | server.patch("/canned_replies/04697870e02acfef3c2130dab92fe6d8/use", () => { 33 | return helper.response({}); 34 | }); 35 | server.post("/canned_replies", () => { 36 | return helper.response({}); 37 | }); 38 | server.get("/canned_replies", () => { 39 | return helper.response({ 40 | replies: [ 41 | { 42 | id: "ce5fc200ab90dd0d5ac597ca9bb4708b", 43 | title: "Small markdown example", 44 | excerpt: "markdown", 45 | content: "**markdown**", 46 | }, 47 | { 48 | id: "cd6680d7a04caaac1274e6f37429458c", 49 | title: "My first canned reply", 50 | excerpt: "This is an example canned reply", 51 | content: 52 | "This is an example canned reply.\nYou can user **markdown** to style your replies. Click the **new** button to create new replies or the **edit** button to edit or remove an existing canned reply.\n\n*This canned reply will be added when the replies list is empty.*", 53 | }, 54 | { 55 | id: "1a1987620aa49135344abe65eb43302d", 56 | title: "Testing", 57 | excerpt: "", 58 | content: "", 59 | usages: 1, 60 | }, 61 | { 62 | id: "5317b7fd066d3d4c15acde92d70f0377", 63 | title: "This is a test", 64 | excerpt: "Testing", 65 | content: "Testing testin **123**", 66 | usages: 1, 67 | }, 68 | { 69 | id: "04697870e02acfef3c2130dab92fe6d8", 70 | title: "Using variables", 71 | excerpt: 72 | "Hi %{reply_to_username,fallback:there}, regards %{my_username}.", 73 | content: 74 | "Hi %{reply_to_username,fallback:there}, regards %{my_username}.", 75 | }, 76 | ], 77 | }); 78 | }); 79 | }); 80 | needs.hooks.beforeEach(() => clearPopupMenuOptionsCallback()); 81 | 82 | test("Inserting canned replies", async (assert) => { 83 | await visit("/"); 84 | 85 | await click("#create-topic"); 86 | const categoryChooser = selectKit(".category-chooser"); 87 | await categoryChooser.expand(); 88 | await categoryChooser.selectRowByValue(2); 89 | await fillIn(".d-editor-input", "beforeafter"); 90 | 91 | const editorInput = $(".d-editor-input")[0]; 92 | editorInput.selectionStart = editorInput.selectionEnd = "before".length; 93 | 94 | const popUpMenu = await selectKit(".toolbar-popup-menu-options"); 95 | await popUpMenu.expand(); 96 | await popUpMenu.selectRowByValue("showCannedRepliesButton"); 97 | await click(".canned-reply-title"); 98 | 99 | assert.ok( 100 | find(".canned-replies-content") 101 | .html() 102 | .indexOf("markdown") !== -1, 103 | "it should display the right cooked content" 104 | ); 105 | 106 | await click( 107 | "#canned-reply-ce5fc200ab90dd0d5ac597ca9bb4708b .canned-replies-apply" 108 | ); 109 | 110 | assert.equal( 111 | find(".d-editor-input").val(), 112 | "before\n\n**markdown**\n\nafter", 113 | "it should contain the right selected output" 114 | ); 115 | }); 116 | 117 | test("Editing a canned reply", async (assert) => { 118 | const popUpMenu = await selectKit(".toolbar-popup-menu-options"); 119 | 120 | await visit("/"); 121 | 122 | await click("#create-topic"); 123 | await popUpMenu.expand(); 124 | await popUpMenu.selectRowByValue("showCannedRepliesButton"); 125 | 126 | await click(".canned-replies-edit"); 127 | 128 | await fillIn(".canned-replies-form-title-input", "Some title"); 129 | await fillIn(".canned-replies-form-content-input textarea", "Some content"); 130 | 131 | await click(".edit-reply-save-btn"); 132 | 133 | assert.equal( 134 | find(".canned-replies-footer .edit-reply-save-btn").text().trim(), 135 | I18n.t("saved") 136 | ); 137 | }); 138 | 139 | test("Creating a new canned reply", async (assert) => { 140 | const popUpMenu = await selectKit(".toolbar-popup-menu-options"); 141 | 142 | await visit("/"); 143 | 144 | await click("#create-topic"); 145 | await popUpMenu.expand(); 146 | await popUpMenu.selectRowByValue("showCannedRepliesButton"); 147 | 148 | await click(".canned-replies-new"); 149 | 150 | await fillIn(".canned-replies-form-title-input", ""); 151 | await fillIn(".canned-replies-form-content-input textarea", ""); 152 | 153 | assert.equal( 154 | find(".btn.new-reply-save-btn[disabled]").length, 155 | 1, 156 | "save button should be disabled by default" 157 | ); 158 | 159 | await fillIn(".canned-replies-form-title-input", "Some title"); 160 | 161 | assert.equal( 162 | find(".btn.new-reply-save-btn[disabled]").length, 163 | 1, 164 | "save button should be disabled when content is blank" 165 | ); 166 | 167 | await fillIn(".canned-replies-form-content-input textarea", "Some content"); 168 | await click(".new-reply-save-btn"); 169 | }); 170 | 171 | test("Replacing variables", async (assert) => { 172 | const popUpMenu = await selectKit(".toolbar-popup-menu-options"); 173 | 174 | await visit("/"); 175 | 176 | await click("#create-topic"); 177 | const categoryChooser = selectKit(".category-chooser"); 178 | await categoryChooser.expand(); 179 | await categoryChooser.selectRowByValue(2); 180 | await popUpMenu.expand(); 181 | await popUpMenu.selectRowByValue("showCannedRepliesButton"); 182 | 183 | await click( 184 | "#canned-reply-04697870e02acfef3c2130dab92fe6d8 .canned-replies-apply" 185 | ); 186 | 187 | assert.equal( 188 | find(".d-editor-input").val().trim(), 189 | "Hi there, regards eviltrout.", 190 | "it should replace variables" 191 | ); 192 | }); 193 | 194 | test("Reset modal content", async (assert) => { 195 | const popUpMenu = await selectKit(".toolbar-popup-menu-options"); 196 | 197 | await visit("/"); 198 | 199 | await click("#create-topic"); 200 | await popUpMenu.expand(); 201 | await popUpMenu.selectRowByValue("showCannedRepliesButton"); 202 | 203 | await click(".canned-replies-new"); 204 | 205 | await fillIn(".canned-replies-form-title-input", "Some title"); 206 | await fillIn(".canned-replies-form-content-input textarea", "Some content"); 207 | 208 | await click(".modal-close"); 209 | 210 | await click(".canned-replies-new"); 211 | 212 | assert.equal( 213 | find(".canned-replies-form-title-input").val(), 214 | "", 215 | "it should clear title" 216 | ); 217 | assert.equal( 218 | find(".canned-replies-form-content-input textarea").val(), 219 | "", 220 | "it should clear content" 221 | ); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /spec/integration/canned_reply/canned_replies_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | RSpec.describe DiscourseCannedReplies::CannedRepliesController do 6 | let(:moderator) do 7 | user = Fabricate(:moderator) 8 | sign_in(user) 9 | user 10 | end 11 | 12 | let(:privileged_group) do 13 | group = Fabricate(:group, users: [privileged_user]) 14 | group.add(privileged_user) 15 | group.save 16 | group 17 | end 18 | 19 | let(:privileged_user) do 20 | user = Fabricate(:user) 21 | sign_in(user) 22 | user 23 | end 24 | 25 | let(:user) do 26 | user = Fabricate(:user) 27 | sign_in(user) 28 | user 29 | end 30 | 31 | let(:canned_reply) { DiscourseCannedReplies::Reply.add(moderator, "some title", "some content") } 32 | 33 | describe "listing canned replies" do 34 | context "as a normal user" do 35 | it "should raise the right error" do 36 | user 37 | 38 | get "/canned_replies" 39 | expect(response.status).to eq(403) 40 | end 41 | end 42 | 43 | context "as a normal user with everyone enabled" do 44 | it "should not raise an error" do 45 | SiteSetting.canned_replies_everyone_enabled = true 46 | user 47 | 48 | get "/canned_replies" 49 | expect(response.status).to eq(200) 50 | end 51 | end 52 | 53 | let(:list_canned_replies) do 54 | post "/canned_replies", params: { title: "Reply test title", content: "Reply test content" } 55 | 56 | expect(response).to be_successful 57 | 58 | get "/canned_replies" 59 | 60 | expect(response).to be_successful 61 | 62 | replies = JSON.parse(response.body)["replies"] 63 | reply = replies.first 64 | 65 | expect(replies.length).to eq(1) 66 | expect(reply["title"]).to eq "Reply test title" 67 | expect(reply["content"]).to eq "Reply test content" 68 | end 69 | 70 | context "as a staff" do 71 | it "should list all replies correctly" do 72 | moderator 73 | 74 | list_canned_replies 75 | end 76 | end 77 | 78 | context "as a privileged user" do 79 | before do 80 | privileged_user 81 | privileged_group 82 | SiteSetting.canned_replies_groups = privileged_group.name 83 | end 84 | 85 | it "should list all replies correctly" do 86 | list_canned_replies 87 | end 88 | end 89 | end 90 | 91 | describe "removing canned replies" do 92 | context "as a normal user" do 93 | it "should raise the right error" do 94 | user 95 | 96 | delete "/canned_replies/someid" 97 | expect(response.status).to eq(403) 98 | end 99 | 100 | it "should raise the right error with everyone enabled" do 101 | SiteSetting.canned_replies_everyone_enabled = true 102 | user 103 | 104 | delete "/canned_replies/someid" 105 | expect(response.status).to eq(403) 106 | end 107 | end 108 | 109 | let(:remove_canned_replies) do 110 | post "/canned_replies", params: { title: "Reply test title", content: "Reply test content" } 111 | 112 | expect(response).to be_successful 113 | 114 | id, _new_reply = 115 | PluginStore.get( 116 | DiscourseCannedReplies::PLUGIN_NAME, 117 | DiscourseCannedReplies::STORE_NAME, 118 | ).first 119 | 120 | delete "/canned_replies/#{id}" 121 | 122 | expect(response).to be_successful 123 | expect( 124 | PluginStore.get(DiscourseCannedReplies::PLUGIN_NAME, DiscourseCannedReplies::STORE_NAME), 125 | ).to eq({}) 126 | end 127 | 128 | context "as a staff" do 129 | it "should be able to remove reply" do 130 | moderator 131 | 132 | remove_canned_replies 133 | end 134 | end 135 | context "as a privileged user" do 136 | before do 137 | privileged_user 138 | privileged_group 139 | SiteSetting.canned_replies_groups = privileged_group.name 140 | end 141 | 142 | it "should be able to remove reply" do 143 | remove_canned_replies 144 | end 145 | end 146 | 147 | context "as a regular user with everyone can edit enabled" do 148 | it "should be able to remove reply" do 149 | SiteSetting.canned_replies_everyone_enabled = true 150 | SiteSetting.canned_replies_everyone_can_edit = true 151 | user 152 | 153 | remove_canned_replies 154 | end 155 | end 156 | end 157 | 158 | describe "editing a canned reply" do 159 | context "as a normal user" do 160 | it "should raise the right error" do 161 | user 162 | 163 | put "/canned_replies/someid" 164 | expect(response.status).to eq(403) 165 | end 166 | it "should raise the right error with everyone enabled" do 167 | SiteSetting.canned_replies_everyone_enabled = true 168 | user 169 | 170 | put "/canned_replies/someid" 171 | expect(response.status).to eq(403) 172 | end 173 | end 174 | 175 | let(:edit_canned_reply) do 176 | post "/canned_replies", params: { title: "Reply test title", content: "Reply test content" } 177 | 178 | expect(response).to be_successful 179 | 180 | id, _new_reply = 181 | PluginStore.get( 182 | DiscourseCannedReplies::PLUGIN_NAME, 183 | DiscourseCannedReplies::STORE_NAME, 184 | ).first 185 | 186 | patch "/canned_replies/#{id}", params: { title: "new title", content: "new content" } 187 | 188 | expect(response).to be_successful 189 | 190 | id, reply = 191 | PluginStore.get( 192 | DiscourseCannedReplies::PLUGIN_NAME, 193 | DiscourseCannedReplies::STORE_NAME, 194 | ).first 195 | 196 | expect(reply["title"]).to eq("new title") 197 | expect(reply["content"]).to eq("new content") 198 | end 199 | 200 | context "as a normal user" do 201 | before do 202 | SiteSetting.canned_replies_everyone_enabled = true 203 | 204 | canned_reply 205 | user 206 | end 207 | 208 | it "should not be able to edit a reply" do 209 | patch "/canned_replies/#{canned_reply[:id]}", 210 | params: { 211 | title: "new title", 212 | content: "new content", 213 | } 214 | 215 | expect(response.status).to eq(403) 216 | end 217 | 218 | it "should be able to edit a reply when SiteSetting is enabled" do 219 | SiteSetting.canned_replies_everyone_can_edit = true 220 | 221 | patch "/canned_replies/#{canned_reply[:id]}", 222 | params: { 223 | title: "new title", 224 | content: "new content", 225 | } 226 | 227 | expect(response).to be_successful 228 | 229 | id, reply = 230 | PluginStore.get( 231 | DiscourseCannedReplies::PLUGIN_NAME, 232 | DiscourseCannedReplies::STORE_NAME, 233 | ).first 234 | 235 | expect(reply["title"]).to eq("new title") 236 | expect(reply["content"]).to eq("new content") 237 | end 238 | end 239 | 240 | context "as a staff" do 241 | it "should be able to edit a reply" do 242 | moderator 243 | 244 | edit_canned_reply 245 | end 246 | end 247 | context "as a privileged user" do 248 | before do 249 | privileged_user 250 | privileged_group 251 | SiteSetting.canned_replies_groups = privileged_group.name 252 | end 253 | 254 | it "should be able to edit a reply" do 255 | edit_canned_reply 256 | end 257 | end 258 | context "as a regular user with everyone can edit enabled" do 259 | it "should be able to edit a reply" do 260 | SiteSetting.canned_replies_everyone_enabled = true 261 | SiteSetting.canned_replies_everyone_can_edit = true 262 | user 263 | 264 | edit_canned_reply 265 | end 266 | end 267 | end 268 | 269 | describe "recording canned replies usages" do 270 | context "as a normal user" do 271 | it "should raise the right error" do 272 | canned_reply 273 | user 274 | 275 | patch "/canned_replies/#{canned_reply[:id]}/use" 276 | expect(response.status).to eq(403) 277 | end 278 | 279 | it "should be able to record a user with everyone enabled" do 280 | SiteSetting.canned_replies_everyone_enabled = true 281 | canned_reply 282 | user 283 | 284 | patch "/canned_replies/#{canned_reply[:id]}/use" 285 | expect(response).to be_successful 286 | _id, reply = 287 | PluginStore.get( 288 | DiscourseCannedReplies::PLUGIN_NAME, 289 | DiscourseCannedReplies::STORE_NAME, 290 | ).first 291 | 292 | expect(reply["usages"]).to eq(1) 293 | end 294 | end 295 | 296 | context "as a staff" do 297 | it "should be able to record a usage" do 298 | patch "/canned_replies/#{canned_reply[:id]}/use" 299 | 300 | expect(response).to be_successful 301 | 302 | _id, reply = 303 | PluginStore.get( 304 | DiscourseCannedReplies::PLUGIN_NAME, 305 | DiscourseCannedReplies::STORE_NAME, 306 | ).first 307 | 308 | expect(reply["usages"]).to eq(1) 309 | end 310 | end 311 | end 312 | 313 | describe "retrieving a canned reply" do 314 | context "as a normal user" do 315 | it "should raise the right error" do 316 | canned_reply 317 | user 318 | 319 | get "/canned_replies/#{canned_reply[:id]}/reply" 320 | expect(response.status).to eq(403) 321 | end 322 | it "should succeed with everyone enabled" do 323 | SiteSetting.canned_replies_everyone_enabled = true 324 | canned_reply 325 | user 326 | 327 | get "/canned_replies/#{canned_reply[:id]}/reply" 328 | expect(response).to be_successful 329 | end 330 | end 331 | 332 | context "as a staff" do 333 | it "should fetch the right canned reply" do 334 | get "/canned_replies/#{canned_reply[:id]}/reply" 335 | 336 | expect(response).to be_successful 337 | 338 | reply = JSON.parse(response.body) 339 | 340 | expect(reply["title"]).to eq(canned_reply[:title]) 341 | expect(reply["content"]).to eq(canned_reply[:content]) 342 | end 343 | end 344 | end 345 | end 346 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. --------------------------------------------------------------------------------