diff --git a/LICENSE-ICONS-3RD-PARTY.json b/LICENSE-ICONS-3RD-PARTY.json index bfe559355..fe548fb63 100644 --- a/LICENSE-ICONS-3RD-PARTY.json +++ b/LICENSE-ICONS-3RD-PARTY.json @@ -244,6 +244,11 @@ "url": "", "license": "MIT" }, + "hashtag.svg": { + "author": "Felix Niklas", + "url": "", + "license": "MIT" + }, "help.svg": { "author": "Felix Niklas", "url": "", diff --git a/app/assets/javascripts/app/controllers/knowledge_base/reader_controller.coffee b/app/assets/javascripts/app/controllers/knowledge_base/reader_controller.coffee index 7897fee81..51a8f4141 100644 --- a/app/assets/javascripts/app/controllers/knowledge_base/reader_controller.coffee +++ b/app/assets/javascripts/app/controllers/knowledge_base/reader_controller.coffee @@ -2,13 +2,17 @@ class App.KnowledgeBaseReaderController extends App.Controller @extend App.PopoverProvidable @registerPopovers 'Ticket' + events: + 'click .js-tag': 'searchTag' + elements: - '.js-answer-title': 'answerTitle' - '.js-answer-body': 'answerBody' - '.js-answer-pagination': 'answerPagination' - '.js-answer-attachments': 'answerAttachments' + '.js-answer-title': 'answerTitle' + '.js-answer-body': 'answerBody' + '.js-answer-pagination': 'answerPagination' + '.js-answer-attachments': 'answerAttachments' + '.js-answer-tags': 'answerTags' '.js-answer-linked-tickets': 'answerLinkedTickets' - '.js-answer-meta': 'answerMeta' + '.js-answer-meta': 'answerMeta' constructor: -> super @@ -60,6 +64,7 @@ class App.KnowledgeBaseReaderController extends App.Controller return @renderAttachments(answer.attachments) + @renderTags(answer.tags) @renderLinkedTickets(answer.translation(kb_locale.id)?.linked_tickets()) paginator = new App.KnowledgeBaseReaderPagination(object: @object, kb_locale: kb_locale) @@ -129,6 +134,11 @@ class App.KnowledgeBaseReaderController extends App.Controller attachments: attachments ) + renderTags: (tags) -> + @answerTags.html App.view('knowledge_base/_reader_tags')( + tags: tags + ) + renderLinkedTickets: (linked_tickets) -> @answerLinkedTickets.html App.view('knowledge_base/_reader_linked_tickets')( tickets: linked_tickets @@ -154,3 +164,8 @@ class App.KnowledgeBaseReaderController extends App.Controller return decodeURIComponent @parentController.lastParams.arguments + + searchTag: (e) -> + e.preventDefault() + item = $(e.currentTarget).text() + App.GlobalSearchWidget.search(item, 'tags') diff --git a/app/assets/javascripts/app/controllers/knowledge_base/sidebar.coffee b/app/assets/javascripts/app/controllers/knowledge_base/sidebar.coffee index 9543adc76..dc7782a50 100644 --- a/app/assets/javascripts/app/controllers/knowledge_base/sidebar.coffee +++ b/app/assets/javascripts/app/controllers/knowledge_base/sidebar.coffee @@ -15,7 +15,9 @@ class App.KnowledgeBaseSidebar extends App.Controller true rerender: -> - @show(@savedParams, @savedAction) + @delay( => + @show(@savedParams, @savedAction) + , 300, 'rerender') contentActionClicked: (e) -> # coffeelint: disable=indentation @@ -59,5 +61,6 @@ class App.KnowledgeBaseSidebar extends App.Controller if object instanceof App.KnowledgeBaseAnswer output.push App.KnowledgeBaseSidebarLinkedTickets output.push App.KnowledgeBaseSidebarAttachments + output.push App.KnowledgeBaseSidebarTags output diff --git a/app/assets/javascripts/app/controllers/knowledge_base/sidebar/tags.coffee b/app/assets/javascripts/app/controllers/knowledge_base/sidebar/tags.coffee new file mode 100644 index 000000000..14f8ea9b8 --- /dev/null +++ b/app/assets/javascripts/app/controllers/knowledge_base/sidebar/tags.coffee @@ -0,0 +1,13 @@ +class App.KnowledgeBaseSidebarTags extends App.Controller + className: 'sidebar-block' + + constructor: -> + super + + @widget = new App.WidgetTag( + el: @el + templateName: 'knowledge_base/sidebar/tags' + object_type: 'KnowledgeBaseAnswer' + object: @object + tags: @object.tags + ) diff --git a/app/assets/javascripts/app/controllers/widget/tag.coffee b/app/assets/javascripts/app/controllers/widget/tag.coffee index 894816e03..128d9e37c 100644 --- a/app/assets/javascripts/app/controllers/widget/tag.coffee +++ b/app/assets/javascripts/app/controllers/widget/tag.coffee @@ -2,6 +2,7 @@ class App.WidgetTag extends App.Controller editMode: false pendingRefresh: false possibleTags: {} + templateName:'widget/tag' elements: '.js-newTagLabel': 'newTagLabel' '.js-newTagInput': 'newTagInput' @@ -50,7 +51,7 @@ class App.WidgetTag extends App.Controller render: => return if @lastLocalTags && _.isEqual(@lastLocalTags, @localTags) @lastLocalTags = _.clone(@localTags) - @html App.view('widget/tag')( + @html App.view(@templateName)( tags: @localTags || [], ) source = "#{App.Config.get('api_path')}/tag_search" diff --git a/app/assets/javascripts/app/lib/mixins/knowledge_base_translatable.coffee b/app/assets/javascripts/app/lib/mixins/knowledge_base_translatable.coffee index 535203852..bb1aff51f 100644 --- a/app/assets/javascripts/app/lib/mixins/knowledge_base_translatable.coffee +++ b/app/assets/javascripts/app/lib/mixins/knowledge_base_translatable.coffee @@ -45,6 +45,7 @@ InstanceMethods = if @ instanceof App.KnowledgeBaseAnswer attrs.icon = 'knowledge-base-answer' attrs.state = @can_be_published_state() + attrs.tags = @tags if options.isEditor attrs.editorOnly = !@is_internally_published(kb_locale) diff --git a/app/assets/javascripts/app/views/knowledge_base/_reader_tags.jst.eco b/app/assets/javascripts/app/views/knowledge_base/_reader_tags.jst.eco new file mode 100644 index 000000000..17de9dc6b --- /dev/null +++ b/app/assets/javascripts/app/views/knowledge_base/_reader_tags.jst.eco @@ -0,0 +1,14 @@ +<% if @tags?.length: %> +
+ <%- @Icon('hashtag') %> +
+ <%- @T('Tags') %> +
+ +
+ <% for tag, i in @tags: %> + <%= tag %> + <% end %> +
+
+<% end %> diff --git a/app/assets/javascripts/app/views/knowledge_base/reader.jst.eco b/app/assets/javascripts/app/views/knowledge_base/reader.jst.eco index 8f1b73c0a..1dab2b941 100644 --- a/app/assets/javascripts/app/views/knowledge_base/reader.jst.eco +++ b/app/assets/javascripts/app/views/knowledge_base/reader.jst.eco @@ -16,6 +16,7 @@
+
diff --git a/app/assets/javascripts/app/views/knowledge_base/sidebar/attachments.jst.eco b/app/assets/javascripts/app/views/knowledge_base/sidebar/attachments.jst.eco index b01b1b117..d4b4ea4e8 100644 --- a/app/assets/javascripts/app/views/knowledge_base/sidebar/attachments.jst.eco +++ b/app/assets/javascripts/app/views/knowledge_base/sidebar/attachments.jst.eco @@ -4,15 +4,12 @@
<% if @attachments.length > 0: %> -
    +
      <% for attachment in @attachments: %> -
    1. - - +
    2. + + <%= attachment.filename %> + <%- @Icon('diagonal-cross') %> diff --git a/app/assets/javascripts/app/views/knowledge_base/sidebar/tags.jst.eco b/app/assets/javascripts/app/views/knowledge_base/sidebar/tags.jst.eco new file mode 100644 index 000000000..6ad2ba174 --- /dev/null +++ b/app/assets/javascripts/app/views/knowledge_base/sidebar/tags.jst.eco @@ -0,0 +1,24 @@ + + +<% if @tags.length > 0: %> +
        + <% for elem in @tags: %> +
      1. + <%= elem %> +
        + <%- @Icon('diagonal-cross') %> +
        +
      2. + <% end %> +
      +<% end %> + + + + <%- @T('Add Tag') %> + + +
      + +
      diff --git a/app/assets/javascripts/app/views/popover/kb_generic.jst.eco b/app/assets/javascripts/app/views/popover/kb_generic.jst.eco index 3300ca6ac..c198c26bc 100644 --- a/app/assets/javascripts/app/views/popover/kb_generic.jst.eco +++ b/app/assets/javascripts/app/views/popover/kb_generic.jst.eco @@ -35,6 +35,13 @@ <%= App.KnowledgeBaseLocale.localeFor(@object).systemLocale().name %>
+ + <% if !_.isEmpty(@object.parent().tags): %> +
+ + <%= @object.parent().tags.map((elem) -> "##{elem}").join(' ') %> +
+ <% end %> <% end %> diff --git a/app/assets/javascripts/knowledge_base_public/search.js b/app/assets/javascripts/knowledge_base_public/search.js index 719464b04..9e480a763 100644 --- a/app/assets/javascripts/knowledge_base_public/search.js +++ b/app/assets/javascripts/knowledge_base_public/search.js @@ -74,15 +74,17 @@ this.el.classList.add('result') this.el.innerHTML = this.constructor.template - this.setTitle(data.title) + this.setTitle(data.title, data.tags) this.setSubtitle(data.subtitle) this.setPreview(data.body) this.setURL(data.url) this.setIcon(data.icon, data.type) } - this.setTitle = function(text) { - this.el.querySelector('.result-title').innerHTML = text || '' + this.setTitle = function(text, tags) { + var title = text || '' + + this.el.querySelector('.result-title').innerHTML = title } this.setSubtitle = function(text) { diff --git a/app/assets/stylesheets/knowledge_base.scss b/app/assets/stylesheets/knowledge_base.scss index ef6b81cfb..db35ae510 100644 --- a/app/assets/stylesheets/knowledge_base.scss +++ b/app/assets/stylesheets/knowledge_base.scss @@ -455,6 +455,12 @@ b { .main--categories { h1 { color: $dark-color; + + .icon-hashtag { + fill: hsl(208,13%,81%); + width: .7em; + height: .7em; + } } } @@ -566,6 +572,11 @@ b { width: 16px; height: 16px; + &-hashtag { + width: 14px; + height: 14px; + } + &-knowledge-base { width: 20px; height: 20px; @@ -627,6 +638,8 @@ b { } .sections--list { + padding: 0 !important; + &:first-child { margin-top: 20px; } @@ -948,9 +961,13 @@ b { } } -.attachments { +.article-tags { + margin-top: 1rem; +} + +.article-accessories { padding: 2rem 0 2rem 4rem; - margin: 2rem 0 !important; + margin: 2rem 0; list-style: none; border-top: 1px solid $border; position: relative; @@ -959,7 +976,7 @@ b { padding-left: 2.8rem; } - .icon-paperclip { + .icon { position: absolute; left: 1.2rem; top: 2rem; @@ -979,6 +996,30 @@ b { color: $light-color; padding: 0 .8rem .2rem; } + + &:not(:last-child) { + padding-bottom: 0; + margin-bottom: 0; + } +} + +.tags-content { + display: flex; + flex-wrap: wrap; + padding: .2rem .6rem 0; + + .tag { + margin: .2rem; + } +} + +.tag { + font-size: .8em; + display: inline-block; + color: white; + border-radius: 999px; + padding: 2px 12px 1px; + text-decoration: none; } .attachment { diff --git a/app/assets/stylesheets/svg-dimensions.css b/app/assets/stylesheets/svg-dimensions.css index 9ade588f5..c651bd31b 100644 --- a/app/assets/stylesheets/svg-dimensions.css +++ b/app/assets/stylesheets/svg-dimensions.css @@ -48,6 +48,7 @@ .icon-gitlab-logo { width: 24px; height: 24px; } .icon-google-button { width: 29px; height: 24px; } .icon-group { width: 24px; height: 24px; } +.icon-hashtag { width: 28px; height: 28px; } .icon-help { width: 16px; height: 16px; } .icon-horizontal-rule { width: 12px; height: 12px; } .icon-important { width: 16px; height: 16px; } diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 87ea77794..cacee02e8 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -5168,6 +5168,12 @@ footer { .btn-list { margin-bottom: 5px; } + + &-button { + .sidebar-block-header + &.text-muted { + margin-top: 3px; + } + } } .sidebar-git-issue-delete { @@ -6608,8 +6614,7 @@ footer { } } - .attachments .icon-paperclip, - .attachments .icon-overviews { + .attachments > .icon:first-child { position: absolute; left: 33px; top: 27px; @@ -7610,6 +7615,18 @@ footer { margin-top: 10px; } +.tag { + display: inline-block; + background: #0F94D6; + color: white; + border-radius: 999px; + padding: 2px 12px 1px; + + &:hover { + background: hsl(200deg, 87%, 34%); + } +} + .userNotifications label + .btn { margin-top: 1px; } @@ -12929,6 +12946,7 @@ span.is-disabled { } &-attachments, + &-tags, &-linked-tickets { margin: 0 -30px; @@ -12941,6 +12959,16 @@ span.is-disabled { @include bidi-style(margin-left, 7px, margin-right, 0); } + &-tags--container { + padding: 6px 5px 11px; + display: flex; + flex-wrap: wrap; + + .tag { + margin: 2px; + } + } + &-nav { display: flex; diff --git a/app/controllers/knowledge_base/public/tags_controller.rb b/app/controllers/knowledge_base/public/tags_controller.rb new file mode 100644 index 000000000..e66c79587 --- /dev/null +++ b/app/controllers/knowledge_base/public/tags_controller.rb @@ -0,0 +1,13 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +class KnowledgeBase::Public::TagsController < KnowledgeBase::Public::BaseController + def show + @object = [:tag, params[:tag]] + + all_tagged = KnowledgeBase::Answer.tag_objects(params[:tag]) + + @answers = policy_scope(all_tagged) + .localed(system_locale_via_uri) + .sorted + end +end diff --git a/app/controllers/knowledge_base/search_controller.rb b/app/controllers/knowledge_base/search_controller.rb index 076422a44..d8d926c99 100644 --- a/app/controllers/knowledge_base/search_controller.rb +++ b/app/controllers/knowledge_base/search_controller.rb @@ -100,7 +100,8 @@ class KnowledgeBase::SearchController < ApplicationController url: url, title: meta.dig(:highlight, 'title')&.first || object.title, subtitle: subtitle, - body: meta.dig(:highlight, 'content.body')&.first || strip_tags(object.content.body).truncate(100) + body: meta.dig(:highlight, 'content.body')&.first || strip_tags(object.content.body).truncate(100), + tags: object.answer.tag_list } end diff --git a/app/helpers/knowledge_base_breadcrumb_helper.rb b/app/helpers/knowledge_base_breadcrumb_helper.rb index 4f5afe18c..0205fbd4c 100644 --- a/app/helpers/knowledge_base_breadcrumb_helper.rb +++ b/app/helpers/knowledge_base_breadcrumb_helper.rb @@ -2,7 +2,11 @@ module KnowledgeBaseBreadcrumbHelper def render_breadcrumb_if_needed(knowledge_base, object, alternative) - objects = calculate_breadcrumb_path(object, alternative) + objects = if object.is_a? Array + calculate_breadcrumb_nonpath(object) + else + calculate_breadcrumb_path(object, alternative) + end return if objects.empty? @@ -25,6 +29,10 @@ module KnowledgeBaseBreadcrumbHelper objects + [last].compact end + def calculate_breadcrumb_nonpath(object) + [object] + end + def calculate_breadcrumb_to_category(category) return [] if category.blank? @@ -53,6 +61,8 @@ module KnowledgeBaseBreadcrumbHelper case object when HasTranslations object.translation.title + when Array + object[1] else object end diff --git a/app/helpers/knowledge_base_helper.rb b/app/helpers/knowledge_base_helper.rb index ba0fd7be5..aaf302758 100644 --- a/app/helpers/knowledge_base_helper.rb +++ b/app/helpers/knowledge_base_helper.rb @@ -73,7 +73,7 @@ module KnowledgeBaseHelper def kb_public_system_path(*objects) objects .compact - .map { |elem| elem.translation.to_param } + .map { |elem| elem.is_a?(HasTranslations) ? elem.translation.to_param : elem } .unshift(help_root_path) .join('/') end diff --git a/app/helpers/knowledge_base_icon_helper.rb b/app/helpers/knowledge_base_icon_helper.rb index 60eee2522..afa31390d 100644 --- a/app/helpers/knowledge_base_icon_helper.rb +++ b/app/helpers/knowledge_base_icon_helper.rb @@ -9,6 +9,8 @@ module KnowledgeBaseIconHelper icon 'knowledge-base-answer' when KnowledgeBase icon 'knowledge-base' + when Array + icon 'hashtag' # object[0] override while tag icon is available end end diff --git a/app/helpers/knowledge_base_public_page_title_helper.rb b/app/helpers/knowledge_base_public_page_title_helper.rb index 50c8b7231..0441eeacb 100644 --- a/app/helpers/knowledge_base_public_page_title_helper.rb +++ b/app/helpers/knowledge_base_public_page_title_helper.rb @@ -9,15 +9,22 @@ module KnowledgeBasePublicPageTitleHelper end def kb_public_page_title_suffix(item, exception) - return item&.translation&.title if exception.blank? + case item + when HasTranslations + return item&.translation&.title if exception.blank? - suffix = case exception - when :not_found - 'Not Found' - when :alternatives - 'Alternative Translations' - end + zt kb_public_page_title_suffix_exception(exception) + when String + item + end + end - zt(suffix) + def kb_public_page_title_suffix_exception(exception) + case exception + when :not_found + 'Not Found' + when :alternatives + 'Alternative Translations' + end end end diff --git a/app/helpers/knowledge_base_top_bar_helper.rb b/app/helpers/knowledge_base_top_bar_helper.rb index c412647a2..1e92b20b7 100644 --- a/app/helpers/knowledge_base_top_bar_helper.rb +++ b/app/helpers/knowledge_base_top_bar_helper.rb @@ -34,4 +34,14 @@ module KnowledgeBaseTopBarHelper 'Published' end end + + def render_top_bar_if_needed(object, knowledge_base) + return if !policy(:knowledge_base).edit? + + editable = object || knowledge_base + + return if !editable.is_a? HasTranslations + + render 'knowledge_base/public/top_banner', object: editable + end end diff --git a/app/models/knowledge_base/answer.rb b/app/models/knowledge_base/answer.rb index 17b939216..d74ebb54b 100644 --- a/app/models/knowledge_base/answer.rb +++ b/app/models/knowledge_base/answer.rb @@ -3,6 +3,7 @@ class KnowledgeBase::Answer < ApplicationModel include HasTranslations include HasAgentAllowedParams + include HasTags include CanBePublished include ChecksKbClientNotification include CanCloneAttachments @@ -33,6 +34,7 @@ class KnowledgeBase::Answer < ApplicationModel attrs = super attrs[:attachments] = attachments_sorted.map { |elem| self.class.attachment_to_hash(elem) } + attrs[:tags] = tag_list Cache.write(key, attrs) @@ -112,6 +114,11 @@ class KnowledgeBase::Answer < ApplicationModel end before_save :reordering_callback + def touch_translations + translations.each(&:touch) # move to #touch_all when migrationg to Rails 6 + end + after_touch :touch_translations + class << self def attachment_to_hash(attachment) url = Rails.application.routes.url_helpers.attachment_path(attachment.id) diff --git a/app/models/knowledge_base/answer/translation.rb b/app/models/knowledge_base/answer/translation.rb index 4957588a3..2f09cfcef 100644 --- a/app/models/knowledge_base/answer/translation.rb +++ b/app/models/knowledge_base/answer/translation.rb @@ -56,12 +56,13 @@ class KnowledgeBase::Answer::Translation < ApplicationModel def search_index_attribute_lookup(include_references: true) attrs = super - attrs['title'] = ActionController::Base.helpers.strip_tags attrs['title'] - attrs['content'] = content.search_index_attribute_lookup if content - attrs['scope_id'] = answer.category_id - attrs['attachment'] = answer.attachments_for_search_index_attribute_lookup - - attrs + attrs.merge({ + title: ActionController::Base.helpers.strip_tags(attrs['title']), + content: content&.search_index_attribute_lookup, + scope_id: answer.category_id, + attachment: answer.attachments_for_search_index_attribute_lookup, + tags: answer.tag_list + }) end def linked_references diff --git a/app/views/knowledge_base/public/_inline_stylesheet.html.erb b/app/views/knowledge_base/public/_inline_stylesheet.html.erb index 3a8ce8563..0bcbfbeda 100644 --- a/app/views/knowledge_base/public/_inline_stylesheet.html.erb +++ b/app/views/knowledge_base/public/_inline_stylesheet.html.erb @@ -21,6 +21,10 @@ color: <%= knowledge_base.color_highlight %>; } + .tag { + background: <%= knowledge_base.color_highlight %>; + } + .header { background-color: <%= knowledge_base.color_header %>; } diff --git a/app/views/knowledge_base/public/answers/show.html.erb b/app/views/knowledge_base/public/answers/show.html.erb index 2c57cf04b..ec1c17924 100644 --- a/app/views/knowledge_base/public/answers/show.html.erb +++ b/app/views/knowledge_base/public/answers/show.html.erb @@ -12,9 +12,9 @@ data-available-locales='<%= @object_locales.map(&:locale).join(',') %>'> <% if (attachments = @object.attachments_sorted) && attachments.present? %> -
+
<%= icon 'paperclip' %> -
<%= zt('Attached Files') %>
+
<%= zt('Attached Files') %>
<% attachments.each do |attachment| %> <%= link_to custom_path_if_needed(attachment_path(attachment), @knowledge_base), class: 'attachment', download: true do %> <%= attachment.filename %> @@ -23,6 +23,18 @@ data-available-locales='<%= @object_locales.map(&:locale).join(',') %>'> <% end %>
<% end %> + + <% if (tags = @object.tag_list) && tags.present? %> +
+ <%= icon 'hashtag' %> +
<%= zt('Tags') %>
+
+ <% tags.each do |tag| %> + <%= link_to tag, custom_path_if_needed(help_tag_path(tag, locale: params[:locale]), @knowledge_base), class: 'tag' %> + <% end %> +
+
+ <% end %>
diff --git a/app/views/knowledge_base/public/categories/_answer.html.erb b/app/views/knowledge_base/public/categories/_answer.html.erb index 289302aa5..e5dc83663 100644 --- a/app/views/knowledge_base/public/categories/_answer.html.erb +++ b/app/views/knowledge_base/public/categories/_answer.html.erb @@ -1,6 +1,9 @@ -<%= link_to custom_path_if_needed help_answer_path(answer.category.translation, answer.translation, locale: params[:locale]), @knowledge_base do %> - - <%= icon('knowledge-base-answer') %> - <%= answer.translation.title %> <%= visibility_note(answer) %> - -<% end %> +
  • + <%= link_to custom_path_if_needed help_answer_path(answer.category.translation, answer.translation, locale: params[:locale]), @knowledge_base do %> + + <%= icon('knowledge-base-answer') %> + <%= answer.translation.title %> + <%= visibility_note(answer) %> + + <% end %> +
  • diff --git a/app/views/knowledge_base/public/categories/index.html.erb b/app/views/knowledge_base/public/categories/index.html.erb index 7584a76e0..a6e0deb32 100644 --- a/app/views/knowledge_base/public/categories/index.html.erb +++ b/app/views/knowledge_base/public/categories/index.html.erb @@ -27,15 +27,13 @@ <% end %> <% if @categories&.present? && @answers&.present? %> -
    +
    <% end %> <% if @answers&.present? %> <% end %> diff --git a/app/views/knowledge_base/public/tags/show.html.erb b/app/views/knowledge_base/public/tags/show.html.erb new file mode 100644 index 000000000..a7972384f --- /dev/null +++ b/app/views/knowledge_base/public/tags/show.html.erb @@ -0,0 +1,19 @@ +
    +
    +

    + <%= icon 'hashtag' %> <%= @object[1] %> +

    + + <% if @answers&.present? %> + + <% else %> +
    + <%= zt('No content to show') %> +
    + <% end %> +
    +
    diff --git a/app/views/layouts/knowledge_base.html.erb b/app/views/layouts/knowledge_base.html.erb index 642aca299..47ac6a8ef 100644 --- a/app/views/layouts/knowledge_base.html.erb +++ b/app/views/layouts/knowledge_base.html.erb @@ -16,7 +16,7 @@ <%= canonical_link_tag @knowledge_base, @category, @object %>
    - <%= render 'knowledge_base/public/top_banner', object: @object || @knowledge_base if policy(:knowledge_base).edit? %> + <%= render_top_bar_if_needed @object, @knowledge_base %>
    diff --git a/config/routes/knowledge_base.rb b/config/routes/knowledge_base.rb index 209b4b0e6..0b7ea76d0 100644 --- a/config/routes/knowledge_base.rb +++ b/config/routes/knowledge_base.rb @@ -68,6 +68,8 @@ Zammad::Application.routes.draw do get '', to: 'knowledge_base/public/categories#forward_root', as: :help_no_locale get ':locale', to: 'knowledge_base/public/categories#index', as: :help_root + get ':locale/tag/:tag', to: 'knowledge_base/public/tags#show', as: :help_tag + get ':locale/:category', to: 'knowledge_base/public/categories#show', as: :help_category get ':locale/:category/:answer', to: 'knowledge_base/public/answers#show', as: :help_answer end diff --git a/lib/search_knowledge_base_backend.rb b/lib/search_knowledge_base_backend.rb index b3e179768..61484ce42 100644 --- a/lib/search_knowledge_base_backend.rb +++ b/lib/search_knowledge_base_backend.rb @@ -173,7 +173,7 @@ class SearchKnowledgeBaseBackend if @params.fetch(:highlight_enabled, true) output[:highlight_fields_by_indexes] = { - 'KnowledgeBase::Answer::Translation': %w[title content.body attachment.content], + 'KnowledgeBase::Answer::Translation': %w[title content.body attachment.content tags], 'KnowledgeBase::Category::Translation': %w[title], 'KnowledgeBase::Translation': %w[title] } diff --git a/public/assets/images/icons.svg b/public/assets/images/icons.svg index 922167867..34d8e364f 100644 --- a/public/assets/images/icons.svg +++ b/public/assets/images/icons.svg @@ -350,6 +350,11 @@ group + + + hashtag + + help diff --git a/public/assets/images/icons/hashtag.svg b/public/assets/images/icons/hashtag.svg new file mode 100644 index 000000000..b989c751c --- /dev/null +++ b/public/assets/images/icons/hashtag.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 63.1 (92452) - https://sketch.com --> + <title>hashtag + Created with Sketch. + + + + \ No newline at end of file diff --git a/spec/factories/knowledge_base/answer.rb b/spec/factories/knowledge_base/answer.rb index a0b589430..909362695 100644 --- a/spec/factories/knowledge_base/answer.rb +++ b/spec/factories/knowledge_base/answer.rb @@ -3,10 +3,10 @@ FactoryBot.define do factory 'knowledge_base/answer', aliases: %i[knowledge_base_answer] do transient do - add_translation { true } - translation_traits { [] } + add_translation { true } + translation_traits { [] } translation_attributes { {} } - knowledge_base { nil } + knowledge_base { nil } end category { create(:knowledge_base_category, { knowledge_base: knowledge_base }.compact) } @@ -43,6 +43,16 @@ FactoryBot.define do end end + trait :with_tag do + transient do + tag_names { %w[example_kb_tag] } + end + + after(:create) do |answer, context| + context.tag_names.each { |tag| answer.tag_add tag } + end + end + trait :with_attachment do transient do attachment { File.open('spec/fixtures/upload/hello_world.txt') } diff --git a/spec/models/knowledge_base/answer_spec.rb b/spec/models/knowledge_base/answer_spec.rb index bed059945..d17bf7a7d 100644 --- a/spec/models/knowledge_base/answer_spec.rb +++ b/spec/models/knowledge_base/answer_spec.rb @@ -2,11 +2,14 @@ require 'rails_helper' require 'models/concerns/checks_kb_client_notification_examples' +require 'models/concerns/has_tags_examples' require 'models/contexts/factory_context' RSpec.describe KnowledgeBase::Answer, type: :model, current_user_id: 1 do subject!(:kb_answer) { create(:knowledge_base_answer) } + it_behaves_like 'HasTags' + include_context 'factory' it_behaves_like 'ChecksKbClientNotification' diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index af040f720..cbc8c9f09 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -167,7 +167,7 @@ RSpec.describe Tag, type: :model do let(:object_1) { create(:ticket) } let(:object_2) { create(:knowledge_base_answer) } - let(:tag) { 'foo' } + let(:tag) { 'foo' } it 'returns references' do expect(described_class.tag_references(tag: tag)).to match_array [ diff --git a/spec/support/knowledge_base_contexts.rb b/spec/support/knowledge_base_contexts.rb index d1db022be..55c9e9e00 100644 --- a/spec/support/knowledge_base_contexts.rb +++ b/spec/support/knowledge_base_contexts.rb @@ -9,6 +9,10 @@ RSpec.shared_context 'basic Knowledge Base', current_user_id: 1 do # rubocop:dis knowledge_base.translation_primary.kb_locale end + let :locale_name do + primary_locale.system_locale.locale + end + let :alternative_locale do create(:knowledge_base_locale, knowledge_base: knowledge_base, system_locale: Locale.find_by(locale: 'lt')) end @@ -29,6 +33,14 @@ RSpec.shared_context 'basic Knowledge Base', current_user_id: 1 do # rubocop:dis create(:knowledge_base_answer, :published, :with_video, category: category) end + let :published_answer_with_tag do + create(:knowledge_base_answer, :published, :with_tag, tag_names: [published_answer_tag_name], category: category) + end + + let(:published_answer_tag_name) do + 'example_kb_tag' + end + let :internal_answer do create(:knowledge_base_answer, :internal, category: category) end diff --git a/spec/system/knowledge_base/locale/answer/edit_spec.rb b/spec/system/knowledge_base/locale/answer/edit_spec.rb index bc1f333e9..be7ca43b2 100644 --- a/spec/system/knowledge_base/locale/answer/edit_spec.rb +++ b/spec/system/knowledge_base/locale/answer/edit_spec.rb @@ -87,4 +87,64 @@ RSpec.describe 'Knowledge Base Locale Answer Edit', type: :system do expect(iframe['src']).to start_with('https://www.youtube.com/embed/') end end + + context 'tags' do + before do + visit "#knowledge_base/#{knowledge_base.id}/locale/#{locale_name}/answer/#{published_answer_with_tag.id}/edit" + end + + let(:new_tag_name) { 'capybara_kb_tag' } + + it 'adds a new tag' do + within :active_content do + click '.js-newTagLabel' + + elem = find('.js-newTagInput') + elem.fill_in with: new_tag_name + elem.send_keys :return + + expect(page).to have_css('a.js-tag', text: new_tag_name) + end + end + + it 'saves new tag to the database' do + within :active_content do + click '.js-newTagLabel' + + elem = find('.js-newTagInput') + elem.fill_in with: new_tag_name + elem.send_keys :return + + wait.until_exists { published_answer_with_tag.reload.tag_list.include? new_tag_name } + end + end + + it 'shows an existing tag' do + within :active_content do + expect(page).to have_css('a.js-tag', text: published_answer_tag_name) + end + end + + it 'deletes a tag' do + within :active_content do + click '.js-newTagLabel' + + find('.list-item', text: published_answer_tag_name) + .find('.js-delete').click + + expect(page).to have_no_css('a.js-tag', text: published_answer_tag_name) + end + end + + it 'deletes the tag from the database' do + within :active_content do + click '.js-newTagLabel' + + find('.list-item', text: published_answer_tag_name) + .find('.js-delete').click + + wait.until_exists { published_answer_with_tag.reload.tag_list.exclude? published_answer_tag_name } + end + end + end end diff --git a/spec/system/knowledge_base/locale/answer/read_spec.rb b/spec/system/knowledge_base/locale/answer/read_spec.rb new file mode 100644 index 000000000..6a97318aa --- /dev/null +++ b/spec/system/knowledge_base/locale/answer/read_spec.rb @@ -0,0 +1,51 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe 'Knowledge Base Locale Answer Read', type: :system, authenticated_as: true do + include_context 'basic Knowledge Base' + + describe 'tags' do + context 'when answer has tags' do + before do + visit "#knowledge_base/#{knowledge_base.id}/locale/#{locale_name}/answer/#{published_answer_with_tag.id}" + end + + it 'has tags container' do + within :active_content do + expect(page).to have_css('.knowledge-base-article-tags--container') + end + end + + it 'shows tag' do + within :active_content do + within '.knowledge-base-article-tags--container' do + expect(page).to have_css('a', text: published_answer_tag_name) + end + end + end + + it 'opens search on clicking' do + within :active_content do + find('.knowledge-base-article-tags--container a', text: published_answer_tag_name).click + end + + search_bar = find '#global-search' + + expect(search_bar.value).to eq "tags:#{published_answer_tag_name}" + end + end + + context 'when answer has no tags' do + before do + visit "#knowledge_base/#{knowledge_base.id}/locale/#{locale_name}/answer/#{published_answer.id}" + end + + it 'has no tags container' do + within :active_content do + expect(page).to have_no_css('.knowledge-base-article-tags--container') + end + end + end + end +end diff --git a/spec/system/knowledge_base_public/answer_spec.rb b/spec/system/knowledge_base_public/answer_spec.rb index 546f4dc52..a11fc9aa5 100644 --- a/spec/system/knowledge_base_public/answer_spec.rb +++ b/spec/system/knowledge_base_public/answer_spec.rb @@ -18,4 +18,20 @@ RSpec.describe 'Public Knowledge Base answer', type: :system, authenticated_as: expect(iframe['src']).to start_with('https://www.youtube.com/embed/') end end + + context 'tags' do + before do + visit help_answer_path(locale_name, category, published_answer_with_tag) + end + + it 'shows an associated tag' do + expect(page).to have_css('.tags a', text: published_answer_tag_name) + end + + it 'links to tag page' do + click '.tags a' + + expect(current_url).to end_with help_tag_path(locale_name, published_answer_tag_name) + end + end end diff --git a/spec/system/knowledge_base_public/canonical_link_spec.rb b/spec/system/knowledge_base_public/canonical_link_spec.rb index 8bbec6f26..f32ffd515 100644 --- a/spec/system/knowledge_base_public/canonical_link_spec.rb +++ b/spec/system/knowledge_base_public/canonical_link_spec.rb @@ -31,6 +31,11 @@ RSpec.describe 'Public Knowledge Base canonical link', type: :system, current_us visit help_answer_path(locale, published_answer.category, published_answer) expect(page).to have_canonical_url("#{prefix}/#{locale}/#{category_slug}/#{answer_slug}") end + + it 'includes canonical link on tag page' do + visit help_tag_path(locale, published_answer_tag_name) + expect(page).to have_canonical_url("#{prefix}/#{locale}/tag/#{published_answer_tag_name}") + end end shared_examples 'core locations' do diff --git a/spec/system/knowledge_base_public/tag_spec.rb b/spec/system/knowledge_base_public/tag_spec.rb new file mode 100644 index 000000000..d963ff89a --- /dev/null +++ b/spec/system/knowledge_base_public/tag_spec.rb @@ -0,0 +1,51 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe 'Public Knowledge Base tag', type: :system, authenticated_as: false do + include_context 'basic Knowledge Base' + + context 'when answer with the tag exists' do + before do + published_answer && published_answer_with_tag + + visit help_tag_path(locale_name, published_answer_tag_name) + end + + it 'displays tag name' do + expect(page).to have_css('h1', text: published_answer_tag_name) + end + + it 'lists an answer with the tag' do + expect(page).to have_css('a', text: published_answer_with_tag.translations.first.title) + end + + it 'does not list another answer' do + expect(page).to have_no_css('a', text: published_answer.translations.first.title) + end + + it 'does not show empty placeholder' do + expect(page).to have_no_css('.sections-empty') + end + end + + context 'when no answers with the tag exists' do + before do + published_answer + + visit help_tag_path(locale_name, published_answer_tag_name) + end + + it 'shows empty placeholder' do + expect(page).to have_css('.sections-empty') + end + + it 'shows no links' do + expect(page).to have_no_css('.main a') + end + + it 'displays tag name' do + expect(page).to have_css('h1', text: published_answer_tag_name) + end + end +end