Fixes #2612 - Tag KB Answers
This commit is contained in:
parent
467bc03224
commit
dde8046470
43 changed files with 522 additions and 58 deletions
|
@ -244,6 +244,11 @@
|
|||
"url": "",
|
||||
"license": "MIT"
|
||||
},
|
||||
"hashtag.svg": {
|
||||
"author": "Felix Niklas",
|
||||
"url": "",
|
||||
"license": "MIT"
|
||||
},
|
||||
"help.svg": {
|
||||
"author": "Felix Niklas",
|
||||
"url": "",
|
||||
|
|
|
@ -2,11 +2,15 @@ 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-tags': 'answerTags'
|
||||
'.js-answer-linked-tickets': 'answerLinkedTickets'
|
||||
'.js-answer-meta': 'answerMeta'
|
||||
|
||||
|
@ -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')
|
||||
|
|
|
@ -15,7 +15,9 @@ class App.KnowledgeBaseSidebar extends App.Controller
|
|||
true
|
||||
|
||||
rerender: ->
|
||||
@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
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<% if @tags?.length: %>
|
||||
<div class="attachments attachments--list">
|
||||
<%- @Icon('hashtag') %>
|
||||
<div class="attachments-title">
|
||||
<%- @T('Tags') %>
|
||||
</div>
|
||||
|
||||
<div class="knowledge-base-article-tags--container">
|
||||
<% for tag, i in @tags: %>
|
||||
<a href="#" class="tag js-tag"><%= tag %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
|
@ -16,6 +16,7 @@
|
|||
<div class="knowledge-base-article-content richtext-content js-answer-body"></div>
|
||||
<div class="knowledge-base-article-attachments js-answer-attachments"></div>
|
||||
<div class="knowledge-base-article-linked-tickets js-answer-linked-tickets"></div>
|
||||
<div class="knowledge-base-article-tags js-answer-tags"></div>
|
||||
</div>
|
||||
|
||||
<div class="js-answer-pagination"></div>
|
||||
|
|
|
@ -4,15 +4,12 @@
|
|||
|
||||
<div class="dropContainer">
|
||||
<% if @attachments.length > 0: %>
|
||||
<ol class="tasks tasks--standalone">
|
||||
<ol class="list list--sidebar">
|
||||
<% for attachment in @attachments: %>
|
||||
<li class="task">
|
||||
<div class="task-text">
|
||||
<a class="name" target=_blank href="<%= attachment.url %>">
|
||||
<li class="list-item">
|
||||
<a class="list-item-name" target=_blank href="<%= attachment.url %>">
|
||||
<%= attachment.filename %>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a class="list-item-delete js-delete" data-object-id="<%= attachment.id %>" href="#">
|
||||
<%- @Icon('diagonal-cross') %>
|
||||
</a>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<div class="sidebar-block-header">
|
||||
<h2><%- @T('Tags') %></h2>
|
||||
</div>
|
||||
|
||||
<% if @tags.length > 0: %>
|
||||
<ol class="list list--sidebar">
|
||||
<% for elem in @tags: %>
|
||||
<li class="list-item" data-id="<%= elem %>">
|
||||
<a class="list-item-name js-tag"><%= elem %></a>
|
||||
<div class="list-item-delete js-delete" data-type="remove">
|
||||
<%- @Icon('diagonal-cross') %>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ol>
|
||||
<% end %>
|
||||
|
||||
<a class="sidebar-block-button text-muted js-newTagLabel" href="#">
|
||||
+ <%- @T('Add Tag') %>
|
||||
</a>
|
||||
|
||||
<form class="ui-front">
|
||||
<input type="text" name="new_tag" class="hide js-newTagInput" autocomplete="off">
|
||||
</form>
|
|
@ -35,6 +35,13 @@
|
|||
<label><%- @T('Language') %></label>
|
||||
<%= App.KnowledgeBaseLocale.localeFor(@object).systemLocale().name %>
|
||||
</div>
|
||||
|
||||
<% if !_.isEmpty(@object.parent().tags): %>
|
||||
<div class="column">
|
||||
<label><%- @T('Tags') %></label>
|
||||
<%= @object.parent().tags.map((elem) -> "##{elem}").join(' ') %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
13
app/controllers/knowledge_base/public/tags_controller.rb
Normal file
13
app/controllers/knowledge_base/public/tags_controller.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -9,15 +9,22 @@ module KnowledgeBasePublicPageTitleHelper
|
|||
end
|
||||
|
||||
def kb_public_page_title_suffix(item, exception)
|
||||
case item
|
||||
when HasTranslations
|
||||
return item&.translation&.title if exception.blank?
|
||||
|
||||
suffix = case exception
|
||||
zt kb_public_page_title_suffix_exception(exception)
|
||||
when String
|
||||
item
|
||||
end
|
||||
end
|
||||
|
||||
def kb_public_page_title_suffix_exception(exception)
|
||||
case exception
|
||||
when :not_found
|
||||
'Not Found'
|
||||
when :alternatives
|
||||
'Alternative Translations'
|
||||
end
|
||||
|
||||
zt(suffix)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
color: <%= knowledge_base.color_highlight %>;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: <%= knowledge_base.color_highlight %>;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: <%= knowledge_base.color_header %>;
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@ data-available-locales='<%= @object_locales.map(&:locale).join(',') %>'>
|
|||
</div>
|
||||
|
||||
<% if (attachments = @object.attachments_sorted) && attachments.present? %>
|
||||
<div class="attachments">
|
||||
<div class="attachments article-accessories">
|
||||
<%= icon 'paperclip' %>
|
||||
<div class="attachments-title"><%= zt('Attached Files') %></div>
|
||||
<div class="article-accessories-title"><%= zt('Attached Files') %></div>
|
||||
<% attachments.each do |attachment| %>
|
||||
<%= link_to custom_path_if_needed(attachment_path(attachment), @knowledge_base), class: 'attachment', download: true do %>
|
||||
<span class="attachment-name u-highlight"><%= attachment.filename %></span>
|
||||
|
@ -23,6 +23,18 @@ data-available-locales='<%= @object_locales.map(&:locale).join(',') %>'>
|
|||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if (tags = @object.tag_list) && tags.present? %>
|
||||
<div class="tags article-accessories">
|
||||
<%= icon 'hashtag' %>
|
||||
<div class="article-accessories-title"><%= zt('Tags') %></div>
|
||||
<div class="tags-content">
|
||||
<% tags.each do |tag| %>
|
||||
<%= link_to tag, custom_path_if_needed(help_tag_path(tag, locale: params[:locale]), @knowledge_base), class: 'tag' %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<%= link_to custom_path_if_needed help_answer_path(answer.category.translation, answer.translation, locale: params[:locale]), @knowledge_base do %>
|
||||
<li class="section <%= visibility_class_name(answer) %>">
|
||||
<%= link_to custom_path_if_needed help_answer_path(answer.category.translation, answer.translation, locale: params[:locale]), @knowledge_base do %>
|
||||
<span class="section-inner">
|
||||
<%= icon('knowledge-base-answer') %>
|
||||
<%= answer.translation.title %> <%= visibility_note(answer) %>
|
||||
<%= answer.translation.title %>
|
||||
<%= visibility_note(answer) %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</li>
|
||||
|
|
|
@ -33,9 +33,7 @@
|
|||
<% if @answers&.present? %>
|
||||
<ul class="sections sections--list" data-less-than-four="<%= @answers.length < 4 %>">
|
||||
<% @answers.each do |translation| %>
|
||||
<li class="section <%= visibility_class_name(translation) %>">
|
||||
<%= render 'answer', { answer: translation } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
|
19
app/views/knowledge_base/public/tags/show.html.erb
Normal file
19
app/views/knowledge_base/public/tags/show.html.erb
Normal file
|
@ -0,0 +1,19 @@
|
|||
<main class="main main--categories">
|
||||
<div class="container">
|
||||
<h1>
|
||||
<%= icon 'hashtag' %> <%= @object[1] %>
|
||||
</h1>
|
||||
|
||||
<% if @answers&.present? %>
|
||||
<ul class="sections sections--list" data-less-than-four="<%= @answers.length < 4 %>">
|
||||
<% @answers.each do |translation| %>
|
||||
<%= render 'knowledge_base/public/categories/answer', { answer: translation } %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<div class="sections-empty">
|
||||
<%= zt('No content to show') %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</main>
|
|
@ -16,7 +16,7 @@
|
|||
<%= canonical_link_tag @knowledge_base, @category, @object %>
|
||||
|
||||
<div class="wrapper js-wrapper">
|
||||
<%= render 'knowledge_base/public/top_banner', object: @object || @knowledge_base if policy(:knowledge_base).edit? %>
|
||||
<%= render_top_bar_if_needed @object, @knowledge_base %>
|
||||
|
||||
<header class="header js-header">
|
||||
<div class="container">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -350,6 +350,11 @@
|
|||
group
|
||||
</title>
|
||||
<path d="M5.221 5.43c1.453 0 2.63-1.215 2.63-2.715C7.852 1.215 6.675 0 5.222 0c-1.453 0-2.63 1.216-2.63 2.715 0 1.5 1.177 2.715 2.63 2.715zm6.98 0c1.453 0 2.63-1.215 2.63-2.715 0-1.5-1.177-2.715-2.63-2.715C10.748 0 9.57 1.216 9.57 2.715c0 1.5 1.178 2.716 2.63 2.716zm5.982 0c1.453 0 2.631-1.215 2.631-2.715 0-1.5-1.178-2.715-2.63-2.715-1.454 0-2.632 1.216-2.632 2.715 0 1.5 1.178 2.716 2.631 2.716zm-2.408.863H8.273c-.641 0-1.161.536-1.161 1.197v6.846c0 .662.52 1.197 1.16 1.197h.358V24h6.65v-8.467h.495c.64 0 1.16-.535 1.16-1.197V7.49c0-.66-.52-1.197-1.16-1.197zm-10.3 8.043V7.49c0-.428.097-.83.26-1.197H1.86C1.22 6.293.7 6.829.7 7.49v6.846c0 .662.52 1.198 1.16 1.198h.358V24h4.775v-7.097a2.902 2.902 0 0 1-1.518-2.567zM22.29 6.293h-3.977c.163.366.26.77.26 1.197v6.846c0 1.172-.68 2.183-1.655 2.635V24h4.878v-8.467h.493c.641 0 1.161-.535 1.161-1.197V7.49c0-.66-.52-1.197-1.16-1.197z" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-hashtag" viewBox="0 0 28 28">
|
||||
<title>
|
||||
hashtag
|
||||
</title>
|
||||
<path d="M12.5 1l-.78 6.5h5.5L18 1h3.5l-.78 6.5H26V11h-5.7l-.66 5.5H25V20h-5.78l-.72 6H15l.72-6h-5.5l-.72 6H6l.72-6H2v-3.5h5.14L7.8 11H3V7.5h5.22L9 1h3.5zm4.3 10h-5.5l-.66 5.5h5.5l.66-5.5z" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-help" viewBox="0 0 16 16">
|
||||
<title>
|
||||
help
|
||||
|
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
9
public/assets/images/icons/hashtag.svg
Normal file
9
public/assets/images/icons/hashtag.svg
Normal file
|
@ -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</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="hashtag" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M12.5,1 L11.72,7.5 L17.22,7.5 L18,1 L21.5,1 L20.72,7.5 L26,7.5 L26,11 L20.3,11 L19.64,16.5 L25,16.5 L25,20 L19.22,20 L18.5,26 L15,26 L15.72,20 L10.22,20 L9.5,26 L6,26 L6.72,20 L2,20 L2,16.5 L7.14,16.5 L7.8,11 L3,11 L3,7.5 L8.22,7.5 L9,1 L12.5,1 Z M16.8,11 L11.3,11 L10.64,16.5 L16.14,16.5 L16.8,11 Z" id="Combined-Shape" fill="#50E3C2"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 774 B |
|
@ -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') }
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
51
spec/system/knowledge_base/locale/answer/read_spec.rb
Normal file
51
spec/system/knowledge_base/locale/answer/read_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
51
spec/system/knowledge_base_public/tag_spec.rb
Normal file
51
spec/system/knowledge_base_public/tag_spec.rb
Normal file
|
@ -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
|
Loading…
Reference in a new issue