Added tag autocompletion and tag management in admin interface.
This commit is contained in:
parent
5977b9e348
commit
7c0893ab36
18 changed files with 1044 additions and 159 deletions
|
@ -1,9 +1,27 @@
|
||||||
# coffeelint: disable=camel_case_classes
|
# coffeelint: disable=camel_case_classes
|
||||||
class App.UiElement.tag
|
class App.UiElement.tag
|
||||||
@render: (attribute) ->
|
@render: (attribute) ->
|
||||||
item = $( App.view('generic/input')( attribute: attribute ) )
|
item = $( App.view('generic/input')(attribute: attribute) )
|
||||||
|
source = "#{App.Config.get('api_path')}/tag_search"
|
||||||
|
possibleTags = {}
|
||||||
a = ->
|
a = ->
|
||||||
$('#' + attribute.id ).tokenfield(createTokensOnBlur: true)
|
$('#' + attribute.id ).tokenfield(
|
||||||
|
createTokensOnBlur: true
|
||||||
|
autocomplete: {
|
||||||
|
source: source
|
||||||
|
minLength: 2
|
||||||
|
response: (e, ui) ->
|
||||||
|
return if !ui
|
||||||
|
return if !ui.content
|
||||||
|
for item in ui.content
|
||||||
|
possibleTags[item.value] = true
|
||||||
|
},
|
||||||
|
).on('tokenfield:createtoken', (e) ->
|
||||||
|
if App.Config.get('tag_new') is false && !possibleTags[e.attrs.value]
|
||||||
|
e.preventDefault()
|
||||||
|
return false
|
||||||
|
true
|
||||||
|
)
|
||||||
$('#' + attribute.id ).parent().css('height', 'auto')
|
$('#' + attribute.id ).parent().css('height', 'auto')
|
||||||
App.Delay.set(a, 120, undefined, 'tags')
|
App.Delay.set(a, 120, undefined, 'tags')
|
||||||
item
|
item
|
||||||
|
|
149
app/assets/javascripts/app/controllers/tag.coffee
Normal file
149
app/assets/javascripts/app/controllers/tag.coffee
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
class Index extends App.ControllerContent
|
||||||
|
events:
|
||||||
|
'change .js-newTagSetting input': 'setTagNew'
|
||||||
|
'submit .js-create': 'create'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-newTagSetting input': 'tagNewSetting'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
# check authentication
|
||||||
|
return if !@authenticate(false, 'Admin')
|
||||||
|
|
||||||
|
@title 'Tags', true
|
||||||
|
|
||||||
|
@subscribeId = App.Setting.subscribe(@render, initFetch: true, clear: false)
|
||||||
|
|
||||||
|
release: =>
|
||||||
|
App.Setting.unsubscribe(@subscribeId)
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
currentNewTagSetting = @Config.get('tag_new') || true
|
||||||
|
return if currentNewTagSetting is @lastNewTagSetting
|
||||||
|
@lastNewTagSetting = currentNewTagSetting
|
||||||
|
|
||||||
|
@html App.view('tag/index')()
|
||||||
|
new Table(
|
||||||
|
el: @$('.js-Table')
|
||||||
|
)
|
||||||
|
|
||||||
|
setTagNew: (e) =>
|
||||||
|
value = @tagNewSetting.prop('checked')
|
||||||
|
console.log('aa', value)
|
||||||
|
App.Setting.set('tag_new', value)
|
||||||
|
|
||||||
|
create: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
field = $(e.currentTarget).find('input[name]')
|
||||||
|
name = field.val().trim()
|
||||||
|
return if !name
|
||||||
|
@ajax(
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/tag_list"
|
||||||
|
data: JSON.stringify(name: name)
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
field.val('')
|
||||||
|
new Table(
|
||||||
|
el: @$('.js-Table')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Table extends App.Controller
|
||||||
|
events:
|
||||||
|
'click .js-delete': 'destroy'
|
||||||
|
'click .js-edit': 'edit'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@load()
|
||||||
|
|
||||||
|
load: =>
|
||||||
|
@ajax(
|
||||||
|
id: 'tag_admin_list'
|
||||||
|
type: 'GET'
|
||||||
|
url: "#{@apiPath}/tag_list"
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@render(data)
|
||||||
|
)
|
||||||
|
|
||||||
|
render: (list) =>
|
||||||
|
@html App.view('tag/table')(
|
||||||
|
list: list
|
||||||
|
)
|
||||||
|
|
||||||
|
edit: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
row = $(e.currentTarget).closest('tr')
|
||||||
|
name = row.find('.js-name').text()
|
||||||
|
id = row.data('id')
|
||||||
|
new Edit(
|
||||||
|
id: id
|
||||||
|
name: name
|
||||||
|
callback: @load
|
||||||
|
)
|
||||||
|
|
||||||
|
destroy: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
row = $(e.currentTarget).closest('tr')
|
||||||
|
id = row.data('id')
|
||||||
|
new DestroyConfirm(
|
||||||
|
id: id
|
||||||
|
row: row
|
||||||
|
)
|
||||||
|
|
||||||
|
class Edit extends App.ControllerModal
|
||||||
|
buttonClose: true
|
||||||
|
buttonCancel: true
|
||||||
|
buttonSubmit: 'Submit'
|
||||||
|
head: 'Edit'
|
||||||
|
small: true
|
||||||
|
|
||||||
|
content: ->
|
||||||
|
App.view('tag/edit')(
|
||||||
|
id: @id
|
||||||
|
name: @name
|
||||||
|
)
|
||||||
|
|
||||||
|
onSubmit: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
params = @formParam(e.target)
|
||||||
|
|
||||||
|
@ajax(
|
||||||
|
id: 'tag_admin_list'
|
||||||
|
type: 'PUT'
|
||||||
|
url: "#{@apiPath}/tag_list/#{@id}"
|
||||||
|
data: JSON.stringify(
|
||||||
|
id: @id
|
||||||
|
name: params.name
|
||||||
|
)
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@callback()
|
||||||
|
@close()
|
||||||
|
)
|
||||||
|
|
||||||
|
class DestroyConfirm extends App.ControllerModal
|
||||||
|
buttonClose: true
|
||||||
|
buttonCancel: true
|
||||||
|
buttonSubmit: 'yes'
|
||||||
|
buttonClass: 'btn--danger'
|
||||||
|
head: 'Confirm'
|
||||||
|
small: true
|
||||||
|
|
||||||
|
content: ->
|
||||||
|
App.i18n.translateContent('Sure to delete this object?')
|
||||||
|
|
||||||
|
onSubmit: =>
|
||||||
|
@ajax(
|
||||||
|
id: 'tag_admin_list'
|
||||||
|
type: 'DELETE'
|
||||||
|
url: "#{@apiPath}/tag_list/#{@id}"
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@row.remove()
|
||||||
|
@close()
|
||||||
|
)
|
||||||
|
|
||||||
|
App.Config.set('Tags', { prio: 2320, name: 'Tags', parent: '#manage', target: '#manage/tags', controller: Index, role: ['Admin'] }, 'NavBarAdmin')
|
|
@ -1,4 +1,6 @@
|
||||||
class App.WidgetTag extends App.Controller
|
class App.WidgetTag extends App.Controller
|
||||||
|
possibleTags: {}
|
||||||
|
shiftHeld: false
|
||||||
elements:
|
elements:
|
||||||
'.js-newTagLabel': 'newTagLabel'
|
'.js-newTagLabel': 'newTagLabel'
|
||||||
'.js-newTagInput': 'newTagInput'
|
'.js-newTagInput': 'newTagInput'
|
||||||
|
@ -9,6 +11,8 @@ class App.WidgetTag extends App.Controller
|
||||||
'click .js-newTagInput': 'onAddTag'
|
'click .js-newTagInput': 'onAddTag'
|
||||||
'submit form': 'onAddTag'
|
'submit form': 'onAddTag'
|
||||||
'click .js-delete': 'onRemoveTag'
|
'click .js-delete': 'onRemoveTag'
|
||||||
|
'mousedown .js-tag': 'shiftHeldToogle'
|
||||||
|
'click .js-tag': 'searchTag'
|
||||||
|
|
||||||
constructor: ->
|
constructor: ->
|
||||||
super
|
super
|
||||||
|
@ -40,6 +44,17 @@ class App.WidgetTag extends App.Controller
|
||||||
tags: @tags || [],
|
tags: @tags || [],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
source = "#{App.Config.get('api_path')}/tag_search"
|
||||||
|
@el.find('.js-newTagInput').autocomplete(
|
||||||
|
source: source
|
||||||
|
minLength: 2
|
||||||
|
response: (e, ui) =>
|
||||||
|
return if !ui
|
||||||
|
return if !ui.content
|
||||||
|
for item in ui.content
|
||||||
|
@possibleTags[item.value] = true
|
||||||
|
)
|
||||||
|
|
||||||
showInput: (e) ->
|
showInput: (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@newTagLabel.addClass('hide')
|
@newTagLabel.addClass('hide')
|
||||||
|
@ -66,16 +81,16 @@ class App.WidgetTag extends App.Controller
|
||||||
if _.contains(@tags, item)
|
if _.contains(@tags, item)
|
||||||
@render()
|
@render()
|
||||||
return
|
return
|
||||||
|
return if App.Config.get('tag_new') is false && !@possibleTags[item]
|
||||||
@tags.push item
|
@tags.push item
|
||||||
@render()
|
@render()
|
||||||
|
|
||||||
@ajax(
|
@ajax(
|
||||||
type: 'GET',
|
type: 'GET'
|
||||||
url: @apiPath + '/tags/add',
|
url: @apiPath + '/tags/add'
|
||||||
data:
|
data:
|
||||||
object: @object_type,
|
object: @object_type
|
||||||
o_id: @object.id,
|
o_id: @object.id
|
||||||
item: item
|
item: item
|
||||||
processData: true,
|
processData: true,
|
||||||
success: (data, status, xhr) =>
|
success: (data, status, xhr) =>
|
||||||
|
@ -104,3 +119,24 @@ class App.WidgetTag extends App.Controller
|
||||||
success: (data, status, xhr) =>
|
success: (data, status, xhr) =>
|
||||||
@fetch()
|
@fetch()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
searchTag: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
item = $(e.target).text()
|
||||||
|
item = item.replace('"', '')
|
||||||
|
if item.match(/\W/)
|
||||||
|
item = "\"#{item}\""
|
||||||
|
searchAttribute = "tag:#{item}"
|
||||||
|
currentValue = $('#global-search').val()
|
||||||
|
if @shiftHeld && currentValue
|
||||||
|
currentValue += ' AND '
|
||||||
|
currentValue += searchAttribute
|
||||||
|
else
|
||||||
|
currentValue = searchAttribute
|
||||||
|
$('#global-search').val(currentValue)
|
||||||
|
delay = ->
|
||||||
|
$('#global-search').focus()
|
||||||
|
@delay(delay, 20)
|
||||||
|
|
||||||
|
shiftHeldToogle: (e) =>
|
||||||
|
@shiftHeld = e.shiftKey
|
||||||
|
|
5
app/assets/javascripts/app/views/tag/edit.jst.eco
Normal file
5
app/assets/javascripts/app/views/tag/edit.jst.eco
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<form>
|
||||||
|
<div class="form-item">
|
||||||
|
<input type="text" name="name" class="" value="<%= @name %>" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</form>
|
29
app/assets/javascripts/app/views/tag/index.jst.eco
Normal file
29
app/assets/javascripts/app/views/tag/index.jst.eco
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-title">
|
||||||
|
<h1><%- @T('Tags') %><small></small></h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-content">
|
||||||
|
|
||||||
|
<div class="settings-entry">
|
||||||
|
<div class="page-header-title">
|
||||||
|
<div class="zammad-switch zammad-switch--small js-newTagSetting">
|
||||||
|
<input name="chat" type="checkbox" id="tag-new" <% if @C('tag_new'): %>checked<% end %>>
|
||||||
|
<label for="tag-new"></label>
|
||||||
|
</div>
|
||||||
|
<h2><%- @T('New Tags') %></h2>
|
||||||
|
</div>
|
||||||
|
<p>⚠ <%- @T('Allow users to add new tags.') %></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-entry">
|
||||||
|
<h2><%- @T('Manage Tags') %></h2>
|
||||||
|
<form class="horizontal form-group formGroup--halfSize js-create">
|
||||||
|
<div class="form-item">
|
||||||
|
<input type="text" name="name" class="" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn--primary js-submit"><%- @T('Add') %></button>
|
||||||
|
</form>
|
||||||
|
<div class="js-Table"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
18
app/assets/javascripts/app/views/tag/table.jst.eco
Normal file
18
app/assets/javascripts/app/views/tag/table.jst.eco
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><%- @T('Name') %></th>
|
||||||
|
<th><%- @T('Count') %></th>
|
||||||
|
<th><%- @T('Action') %></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% for item in @list: %>
|
||||||
|
<tr data-id="<%= item.id %>" class="js-edit u-clickable">
|
||||||
|
<td class="js-name"><%= item.name %></td>
|
||||||
|
<td><%= item.count %></td>
|
||||||
|
<td><a href="#" class="js-delete" title="<%- @Ti('Delete') %>"><%- @Icon('trash') %></a></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
|
@ -1,8 +1,8 @@
|
||||||
<label><%- @T( 'Tags' ) %></label>
|
<label><%- @T('Tags') %></label>
|
||||||
<ul class="list list--sidebar">
|
<ul class="list list--sidebar">
|
||||||
<% for tag in @tags: %>
|
<% for tag in @tags: %>
|
||||||
<li class="list-item">
|
<li class="list-item">
|
||||||
<div class="list-item-name js-tag"><%= tag %></div>
|
<a href="#" class="list-item-name js-tag"><%= tag %></a>
|
||||||
<div class="list-item-delete js-delete">
|
<div class="list-item-delete js-delete">
|
||||||
<%- @Icon('diagonal-cross') %>
|
<%- @Icon('diagonal-cross') %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* the top of the compiled file, but it's generally better to create a new file per style scope.
|
* the top of the compiled file, but it's generally better to create a new file per style scope.
|
||||||
*= require_self
|
*= require_self
|
||||||
*= require ./bootstrap.css
|
*= require ./bootstrap.css
|
||||||
|
*= require ./jquery-ui.css
|
||||||
*= require ./cropper.css
|
*= require ./cropper.css
|
||||||
*= require ./fineuploader.css
|
*= require ./fineuploader.css
|
||||||
*= require ./font.css
|
*= require ./font.css
|
||||||
|
|
332
app/assets/stylesheets/jquery-ui.css
vendored
Normal file
332
app/assets/stylesheets/jquery-ui.css
vendored
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
/*! jQuery UI - v1.11.4 - 2016-06-01
|
||||||
|
* http://jqueryui.com
|
||||||
|
* Includes: core.css, autocomplete.css, menu.css, theme.css
|
||||||
|
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&fwDefault=normal&cornerRadius=3px&bgColorHeader=e9e9e9&bgTextureHeader=flat&borderColorHeader=dddddd&fcHeader=333333&iconColorHeader=444444&bgColorContent=ffffff&bgTextureContent=flat&borderColorContent=dddddd&fcContent=333333&iconColorContent=444444&bgColorDefault=f6f6f6&bgTextureDefault=flat&borderColorDefault=c5c5c5&fcDefault=454545&iconColorDefault=777777&bgColorHover=ededed&bgTextureHover=flat&borderColorHover=cccccc&fcHover=2b2b2b&iconColorHover=555555&bgColorActive=007fff&bgTextureActive=flat&borderColorActive=003eff&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=fffa90&bgTextureHighlight=flat&borderColorHighlight=dad55e&fcHighlight=777620&iconColorHighlight=777620&bgColorError=fddfdf&bgTextureError=flat&borderColorError=f1a899&fcError=5f3f3f&iconColorError=cc0000&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px
|
||||||
|
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||||
|
|
||||||
|
/* Layout helpers
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-helper-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ui-helper-hidden-accessible {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
.ui-helper-reset {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 100%;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.ui-helper-clearfix:before,
|
||||||
|
.ui-helper-clearfix:after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.ui-helper-clearfix:after {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.ui-helper-clearfix {
|
||||||
|
min-height: 0; /* support: IE7 */
|
||||||
|
}
|
||||||
|
.ui-helper-zfix {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
filter:Alpha(Opacity=0); /* support: IE8 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-front {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Interaction Cues
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-state-disabled {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Icons
|
||||||
|
----------------------------------*/
|
||||||
|
|
||||||
|
/* states and images */
|
||||||
|
.ui-icon {
|
||||||
|
display: block;
|
||||||
|
text-indent: -99999px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Misc visuals
|
||||||
|
----------------------------------*/
|
||||||
|
|
||||||
|
/* Overlays */
|
||||||
|
.ui-widget-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.ui-autocomplete {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.ui-menu {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.ui-menu .ui-menu {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.ui-menu .ui-menu-item {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
padding: 3px 1em 3px .4em;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 0; /* support: IE7 */
|
||||||
|
/* support: IE10, see #8844 */
|
||||||
|
list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
|
||||||
|
}
|
||||||
|
.ui-menu .ui-menu-divider {
|
||||||
|
margin: 5px 0;
|
||||||
|
height: 0;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
border-width: 1px 0 0 0;
|
||||||
|
}
|
||||||
|
.ui-menu .ui-state-focus,
|
||||||
|
.ui-menu .ui-state-active {
|
||||||
|
margin: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* icon support */
|
||||||
|
.ui-menu-icons {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.ui-menu-icons .ui-menu-item {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* left-aligned */
|
||||||
|
.ui-menu .ui-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: .2em;
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* right-aligned */
|
||||||
|
.ui-menu .ui-menu-icon {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Component containers
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-widget {
|
||||||
|
font-family: Arial,Helvetica,sans-serif;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.ui-widget .ui-widget {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.ui-widget input,
|
||||||
|
.ui-widget select,
|
||||||
|
.ui-widget textarea,
|
||||||
|
.ui-widget button {
|
||||||
|
font-family: Arial,Helvetica,sans-serif;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.ui-widget-content {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.ui-widget-content a {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.ui-widget-header {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
background: #e9e9e9;
|
||||||
|
color: #333333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.ui-widget-header a {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interaction states
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-state-default,
|
||||||
|
.ui-widget-content .ui-state-default,
|
||||||
|
.ui-widget-header .ui-state-default {
|
||||||
|
border: 1px solid #c5c5c5;
|
||||||
|
background: #f6f6f6;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #454545;
|
||||||
|
}
|
||||||
|
.ui-state-default a,
|
||||||
|
.ui-state-default a:link,
|
||||||
|
.ui-state-default a:visited {
|
||||||
|
color: #454545;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.ui-state-hover,
|
||||||
|
.ui-widget-content .ui-state-hover,
|
||||||
|
.ui-widget-header .ui-state-hover,
|
||||||
|
.ui-state-focus,
|
||||||
|
.ui-widget-content .ui-state-focus,
|
||||||
|
.ui-widget-header .ui-state-focus {
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
background: #ededed;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #2b2b2b;
|
||||||
|
}
|
||||||
|
.ui-state-hover a,
|
||||||
|
.ui-state-hover a:hover,
|
||||||
|
.ui-state-hover a:link,
|
||||||
|
.ui-state-hover a:visited,
|
||||||
|
.ui-state-focus a,
|
||||||
|
.ui-state-focus a:hover,
|
||||||
|
.ui-state-focus a:link,
|
||||||
|
.ui-state-focus a:visited {
|
||||||
|
color: #2b2b2b;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.ui-state-active,
|
||||||
|
.ui-widget-content .ui-state-active,
|
||||||
|
.ui-widget-header .ui-state-active {
|
||||||
|
border: 1px solid #003eff;
|
||||||
|
background: #007fff;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.ui-state-active a,
|
||||||
|
.ui-state-active a:link,
|
||||||
|
.ui-state-active a:visited {
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interaction Cues
|
||||||
|
----------------------------------*/
|
||||||
|
.ui-state-highlight,
|
||||||
|
.ui-widget-content .ui-state-highlight,
|
||||||
|
.ui-widget-header .ui-state-highlight {
|
||||||
|
border: 1px solid #dad55e;
|
||||||
|
background: #fffa90;
|
||||||
|
color: #777620;
|
||||||
|
}
|
||||||
|
.ui-state-highlight a,
|
||||||
|
.ui-widget-content .ui-state-highlight a,
|
||||||
|
.ui-widget-header .ui-state-highlight a {
|
||||||
|
color: #777620;
|
||||||
|
}
|
||||||
|
.ui-state-error,
|
||||||
|
.ui-widget-content .ui-state-error,
|
||||||
|
.ui-widget-header .ui-state-error {
|
||||||
|
border: 1px solid #f1a899;
|
||||||
|
background: #fddfdf;
|
||||||
|
color: #5f3f3f;
|
||||||
|
}
|
||||||
|
.ui-state-error a,
|
||||||
|
.ui-widget-content .ui-state-error a,
|
||||||
|
.ui-widget-header .ui-state-error a {
|
||||||
|
color: #5f3f3f;
|
||||||
|
}
|
||||||
|
.ui-state-error-text,
|
||||||
|
.ui-widget-content .ui-state-error-text,
|
||||||
|
.ui-widget-header .ui-state-error-text {
|
||||||
|
color: #5f3f3f;
|
||||||
|
}
|
||||||
|
.ui-priority-primary,
|
||||||
|
.ui-widget-content .ui-priority-primary,
|
||||||
|
.ui-widget-header .ui-priority-primary {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.ui-priority-secondary,
|
||||||
|
.ui-widget-content .ui-priority-secondary,
|
||||||
|
.ui-widget-header .ui-priority-secondary {
|
||||||
|
opacity: .7;
|
||||||
|
filter:Alpha(Opacity=70); /* support: IE8 */
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.ui-state-disabled,
|
||||||
|
.ui-widget-content .ui-state-disabled,
|
||||||
|
.ui-widget-header .ui-state-disabled {
|
||||||
|
opacity: .35;
|
||||||
|
filter:Alpha(Opacity=35); /* support: IE8 */
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
.ui-state-disabled .ui-icon {
|
||||||
|
filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc visuals
|
||||||
|
----------------------------------*/
|
||||||
|
|
||||||
|
/* Corner radius */
|
||||||
|
.ui-corner-all,
|
||||||
|
.ui-corner-top,
|
||||||
|
.ui-corner-left,
|
||||||
|
.ui-corner-tl {
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
}
|
||||||
|
.ui-corner-all,
|
||||||
|
.ui-corner-top,
|
||||||
|
.ui-corner-right,
|
||||||
|
.ui-corner-tr {
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
}
|
||||||
|
.ui-corner-all,
|
||||||
|
.ui-corner-bottom,
|
||||||
|
.ui-corner-left,
|
||||||
|
.ui-corner-bl {
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
}
|
||||||
|
.ui-corner-all,
|
||||||
|
.ui-corner-bottom,
|
||||||
|
.ui-corner-right,
|
||||||
|
.ui-corner-br {
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlays */
|
||||||
|
.ui-widget-overlay {
|
||||||
|
background: #aaaaaa;
|
||||||
|
opacity: .3;
|
||||||
|
filter: Alpha(Opacity=30); /* support: IE8 */
|
||||||
|
}
|
||||||
|
.ui-widget-shadow {
|
||||||
|
margin: 0px 0 0 0px;
|
||||||
|
padding: 5px;
|
||||||
|
background: #666666;
|
||||||
|
opacity: .3;
|
||||||
|
filter: Alpha(Opacity=30); /* support: IE8 */
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
|
@ -3,14 +3,18 @@
|
||||||
class TagsController < ApplicationController
|
class TagsController < ApplicationController
|
||||||
before_action :authentication_check
|
before_action :authentication_check
|
||||||
|
|
||||||
# GET /api/v1/tags
|
# GET /api/v1/tag_search?term=abc
|
||||||
def index
|
def search
|
||||||
list = Tag.list()
|
list = Tag::Item.where('name_downcase LIKE ?', "#{params[:term].strip.downcase}%").order('name ASC').limit(params[:limit] || 10)
|
||||||
|
results = []
|
||||||
# return result
|
list.each {|item|
|
||||||
render json: {
|
result = {
|
||||||
tags: list,
|
id: item.id,
|
||||||
|
value: item.name,
|
||||||
|
}
|
||||||
|
results.push result
|
||||||
}
|
}
|
||||||
|
render json: results
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /api/v1/tags
|
# GET /api/v1/tags
|
||||||
|
@ -31,7 +35,7 @@ class TagsController < ApplicationController
|
||||||
success = Tag.tag_add(
|
success = Tag.tag_add(
|
||||||
object: params[:object],
|
object: params[:object],
|
||||||
o_id: params[:o_id],
|
o_id: params[:o_id],
|
||||||
item: params[:item],
|
item: params[:item].strip,
|
||||||
)
|
)
|
||||||
if success
|
if success
|
||||||
render json: success, status: :created
|
render json: success, status: :created
|
||||||
|
@ -45,7 +49,7 @@ class TagsController < ApplicationController
|
||||||
success = Tag.tag_remove(
|
success = Tag.tag_remove(
|
||||||
object: params[:object],
|
object: params[:object],
|
||||||
o_id: params[:o_id],
|
o_id: params[:o_id],
|
||||||
item: params[:item],
|
item: params[:item].strip,
|
||||||
)
|
)
|
||||||
if success
|
if success
|
||||||
render json: success, status: :created
|
render json: success, status: :created
|
||||||
|
@ -54,4 +58,60 @@ class TagsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# GET /api/v1/tag_list
|
||||||
|
def admin_list
|
||||||
|
list = Tag::Item.order('name ASC').limit(params[:limit] || 1000)
|
||||||
|
results = []
|
||||||
|
list.each {|item|
|
||||||
|
result = {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
count: Tag.where(tag_item_id: item.id).count
|
||||||
|
}
|
||||||
|
results.push result
|
||||||
|
}
|
||||||
|
render json: results
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/tag_list
|
||||||
|
def admin_create
|
||||||
|
return if deny_if_not_role('Admin')
|
||||||
|
name = params[:name].strip
|
||||||
|
if !Tag.tag_item_lookup(name)
|
||||||
|
Tag::Item.create(name: name)
|
||||||
|
end
|
||||||
|
render json: {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# PUT /api/v1/tag_list/:id
|
||||||
|
def admin_rename
|
||||||
|
return if deny_if_not_role('Admin')
|
||||||
|
name = params[:name].strip
|
||||||
|
tag_item = Tag::Item.find(params[:id])
|
||||||
|
existing_tag_id = Tag.tag_item_lookup(name)
|
||||||
|
if existing_tag_id
|
||||||
|
|
||||||
|
# assign to already existing tag
|
||||||
|
Tag.where(tag_item_id: tag_item.id).update_all(tag_item_id: existing_tag_id)
|
||||||
|
|
||||||
|
# delete not longer used tag
|
||||||
|
tag_item.destroy
|
||||||
|
|
||||||
|
# update new tag name
|
||||||
|
else
|
||||||
|
tag_item.name = name
|
||||||
|
tag_item.save
|
||||||
|
end
|
||||||
|
render json: {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /api/v1/tag_list/:id
|
||||||
|
def admin_delete
|
||||||
|
return if deny_if_not_role('Admin')
|
||||||
|
tag_item = Tag::Item.find(params[:id])
|
||||||
|
Tag.where(tag_item_id: tag_item.id).destroy_all
|
||||||
|
tag_item.destroy
|
||||||
|
render json: {}
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Tag < ApplicationModel
|
||||||
|
|
||||||
# return in duplicate
|
# return in duplicate
|
||||||
current_tags = tag_list(data)
|
current_tags = tag_list(data)
|
||||||
return true if current_tags.include?(data[:item].downcase.strip)
|
return true if current_tags.include?(data[:item].strip)
|
||||||
|
|
||||||
# create history
|
# create history
|
||||||
Tag.create(
|
Tag.create(
|
||||||
|
@ -79,17 +79,16 @@ class Tag < ApplicationModel
|
||||||
|
|
||||||
def self.tag_item_lookup(name)
|
def self.tag_item_lookup(name)
|
||||||
|
|
||||||
name = name.downcase
|
|
||||||
|
|
||||||
# use cache
|
# use cache
|
||||||
return @@cache_item[name] if @@cache_item[name]
|
return @@cache_item[name] if @@cache_item[name]
|
||||||
|
|
||||||
# lookup
|
# lookup
|
||||||
tag_item = Tag::Item.find_by(name: name)
|
tag_items = Tag::Item.where(name: name)
|
||||||
if tag_item
|
tag_items.each {|tag_item|
|
||||||
|
next if tag_item.name != name
|
||||||
@@cache_item[name] = tag_item.id
|
@@cache_item[name] = tag_item.id
|
||||||
return tag_item.id
|
return tag_item.id
|
||||||
end
|
}
|
||||||
|
|
||||||
# create
|
# create
|
||||||
tag_item = Tag::Item.create(name: name)
|
tag_item = Tag::Item.create(name: name)
|
||||||
|
@ -130,6 +129,12 @@ class Tag < ApplicationModel
|
||||||
end
|
end
|
||||||
|
|
||||||
class Item < ActiveRecord::Base
|
class Item < ActiveRecord::Base
|
||||||
|
before_save :fill_namedowncase
|
||||||
|
|
||||||
|
def fill_namedowncase
|
||||||
|
self.name_downcase = name.downcase
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
Zammad::Application.routes.draw do
|
Zammad::Application.routes.draw do
|
||||||
api_path = Rails.configuration.api_path
|
api_path = Rails.configuration.api_path
|
||||||
|
|
||||||
# links
|
match api_path + '/tags', to: 'tags#list', via: :get
|
||||||
match api_path + '/tags', to: 'tags#list', via: :get
|
match api_path + '/tags/add', to: 'tags#add', via: :get
|
||||||
match api_path + '/tags/add', to: 'tags#add', via: :get
|
match api_path + '/tags/remove', to: 'tags#remove', via: :get
|
||||||
match api_path + '/tags/remove', to: 'tags#remove', via: :get
|
match api_path + '/tag_search', to: 'tags#search', via: :get
|
||||||
|
|
||||||
|
match api_path + '/tag_list', to: 'tags#admin_list', via: :get
|
||||||
|
match api_path + '/tag_list', to: 'tags#admin_create', via: :post
|
||||||
|
match api_path + '/tag_list/:id', to: 'tags#admin_rename', via: :put
|
||||||
|
match api_path + '/tag_list/:id', to: 'tags#admin_delete', via: :delete
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -244,9 +244,10 @@ class CreateBase < ActiveRecord::Migration
|
||||||
|
|
||||||
create_table :tag_items do |t|
|
create_table :tag_items do |t|
|
||||||
t.string :name, limit: 250, null: false
|
t.string :name, limit: 250, null: false
|
||||||
|
t.string :name_downcase, limit: 250, null: false
|
||||||
t.timestamps null: false
|
t.timestamps null: false
|
||||||
end
|
end
|
||||||
add_index :tag_items, [:name], unique: true
|
add_index :tag_items, [:name_downcase]
|
||||||
|
|
||||||
create_table :recent_views do |t|
|
create_table :recent_views do |t|
|
||||||
t.references :recent_view_object, null: false
|
t.references :recent_view_object, null: false
|
||||||
|
|
35
db/migrate/20160602000001_update_tag.rb
Normal file
35
db/migrate/20160602000001_update_tag.rb
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
class UpdateTag < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.find_by(name: 'system_init_done')
|
||||||
|
|
||||||
|
remove_index :tag_items, column: [:name]
|
||||||
|
add_column :tag_items, :name_downcase, :string, limit: 250
|
||||||
|
add_index :tag_items, [:name_downcase]
|
||||||
|
Tag.reset_column_information
|
||||||
|
Tag::Item.all.each(&:save)
|
||||||
|
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'New Tags',
|
||||||
|
name: 'tag_new',
|
||||||
|
area: 'Web::Base',
|
||||||
|
description: 'Allow users to crate new tags.',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'tag_new',
|
||||||
|
tag: 'boolean',
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: true,
|
||||||
|
frontend: true,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
23
db/seeds.rb
23
db/seeds.rb
|
@ -1513,6 +1513,29 @@ Setting.create_if_not_exists(
|
||||||
frontend: false
|
frontend: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'New Tags',
|
||||||
|
name: 'tag_new',
|
||||||
|
area: 'Web::Base',
|
||||||
|
description: 'Allow users to crate new tags.',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'tag_new',
|
||||||
|
tag: 'boolean',
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: true,
|
||||||
|
frontend: true
|
||||||
|
)
|
||||||
|
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
title: 'Default calendar Tickets subscriptions',
|
title: 'Default calendar Tickets subscriptions',
|
||||||
name: 'defaults_calendar_subscriptions_tickets',
|
name: 'defaults_calendar_subscriptions_tickets',
|
||||||
|
|
|
@ -27,9 +27,7 @@ class AgentTicketActionLevel8Test < TestCase
|
||||||
css: '.active .ticket-form-bottom .token-input',
|
css: '.active .ticket-form-bottom .token-input',
|
||||||
value: 'tag1, tag2',
|
value: 'tag1, tag2',
|
||||||
)
|
)
|
||||||
sendkey(
|
sendkey(value: :tab)
|
||||||
value: :tab,
|
|
||||||
)
|
|
||||||
|
|
||||||
# reload browser
|
# reload browser
|
||||||
sleep 6
|
sleep 6
|
||||||
|
@ -45,25 +43,14 @@ class AgentTicketActionLevel8Test < TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
# verify tags
|
# verify tags
|
||||||
tags = @browser.find_elements({ css: '.content.active .js-tag' })
|
tags_verify(
|
||||||
assert(tags)
|
tags: {
|
||||||
assert(tags[0])
|
'tag1' => true,
|
||||||
tag1_found = false
|
'tag2' => true,
|
||||||
tag2_found = false
|
'tag3' => false,
|
||||||
tags.each {|element|
|
'tag4' => false,
|
||||||
text = element.text
|
}
|
||||||
if text == 'tag1'
|
)
|
||||||
tag1_found = true
|
|
||||||
assert(true, 'tag1 exists')
|
|
||||||
elsif text == 'tag2'
|
|
||||||
tag2_found = true
|
|
||||||
assert(true, 'tag2 exists')
|
|
||||||
else
|
|
||||||
assert(false, "invalid tag '#{text}'")
|
|
||||||
end
|
|
||||||
}
|
|
||||||
assert(tag1_found, 'tag1 exists')
|
|
||||||
assert(tag2_found, 'tag2 exists')
|
|
||||||
|
|
||||||
# set tag (by blur)
|
# set tag (by blur)
|
||||||
ticket2 = ticket_create(
|
ticket2 = ticket_create(
|
||||||
|
@ -80,38 +67,22 @@ class AgentTicketActionLevel8Test < TestCase
|
||||||
css: '.active .ticket-form-bottom .token-input',
|
css: '.active .ticket-form-bottom .token-input',
|
||||||
value: 'tag3, tag4',
|
value: 'tag3, tag4',
|
||||||
)
|
)
|
||||||
click(
|
click(css: '#global-search')
|
||||||
css: '#global-search',
|
click(css: '.active .newTicket button.js-submit')
|
||||||
)
|
|
||||||
|
|
||||||
click(
|
|
||||||
css: '.active .newTicket button.js-submit',
|
|
||||||
)
|
|
||||||
sleep 5
|
sleep 5
|
||||||
if @browser.current_url !~ /#{Regexp.quote('#ticket/zoom/')}/
|
if @browser.current_url !~ /#{Regexp.quote('#ticket/zoom/')}/
|
||||||
raise 'Unable to create ticket!'
|
raise 'Unable to create ticket!'
|
||||||
end
|
end
|
||||||
|
|
||||||
# verify tags
|
# verify tags
|
||||||
tags = @browser.find_elements({ css: '.content.active .js-tag' })
|
tags_verify(
|
||||||
assert(tags)
|
tags: {
|
||||||
assert(tags[0])
|
'tag1' => false,
|
||||||
tag3_found = false
|
'tag2' => false,
|
||||||
tag4_found = false
|
'tag3' => true,
|
||||||
tags.each {|element|
|
'tag4' => true,
|
||||||
text = element.text
|
}
|
||||||
if text == 'tag3'
|
)
|
||||||
tag3_found = true
|
|
||||||
assert(true, 'tag 3 exists')
|
|
||||||
elsif text == 'tag4'
|
|
||||||
tag4_found = true
|
|
||||||
assert(true, 'tag 4 exists')
|
|
||||||
else
|
|
||||||
assert(false, "invalid tag '#{text}'")
|
|
||||||
end
|
|
||||||
}
|
|
||||||
assert(tag3_found, 'tag3 exists')
|
|
||||||
assert(tag4_found, 'tag4 exists')
|
|
||||||
|
|
||||||
ticket3 = ticket_create(
|
ticket3 = ticket_create(
|
||||||
data: {
|
data: {
|
||||||
|
@ -175,82 +146,202 @@ class AgentTicketActionLevel8Test < TestCase
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
|
|
||||||
# verify tags
|
# verify tags
|
||||||
tags = @browser.find_elements({ css: '.content.active .js-tag' })
|
tags_verify(
|
||||||
assert(tags)
|
tags: {
|
||||||
assert(tags[0])
|
'tag1' => true,
|
||||||
tag1_found = false
|
'tag 2' => true,
|
||||||
tag2_found = false
|
'tag2' => false,
|
||||||
tag3_found = false
|
'tag3' => true,
|
||||||
tag4_found = false
|
'tag4' => true,
|
||||||
tag5_found = false
|
'tag5' => true,
|
||||||
tags.each {|element|
|
}
|
||||||
text = element.text
|
)
|
||||||
if text == 'tag1'
|
|
||||||
tag1_found = true
|
|
||||||
assert(true, 'tag1 exists')
|
|
||||||
elsif text == 'tag 2'
|
|
||||||
tag2_found = true
|
|
||||||
assert(true, 'tag 2 exists')
|
|
||||||
elsif text == 'tag3'
|
|
||||||
tag3_found = true
|
|
||||||
assert(true, 'tag3 exists')
|
|
||||||
elsif text == 'tag4'
|
|
||||||
tag4_found = true
|
|
||||||
assert(true, 'tag4 exists')
|
|
||||||
elsif text == 'tag5'
|
|
||||||
tag5_found = true
|
|
||||||
assert(true, 'tag5 exists')
|
|
||||||
else
|
|
||||||
assert(false, "invalid tag '#{text}'")
|
|
||||||
end
|
|
||||||
}
|
|
||||||
assert(tag1_found, 'tag1 exists')
|
|
||||||
assert(tag2_found, 'tag2 exists')
|
|
||||||
assert(tag3_found, 'tag3 exists')
|
|
||||||
assert(tag4_found, 'tag4 exists')
|
|
||||||
assert(tag5_found, 'tag5 exists')
|
|
||||||
|
|
||||||
# reload browser
|
# reload browser
|
||||||
reload()
|
reload()
|
||||||
|
sleep 2
|
||||||
|
|
||||||
# verify tags
|
# verify tags
|
||||||
tags = @browser.find_elements({ css: '.content.active .js-tag' })
|
tags_verify(
|
||||||
assert(tags)
|
tags: {
|
||||||
assert(tags[0])
|
'tag1' => true,
|
||||||
tag1_found = false
|
'tag 2' => true,
|
||||||
tag2_found = false
|
'tag2' => false,
|
||||||
tag3_found = false
|
'tag3' => true,
|
||||||
tag4_found = false
|
'tag4' => true,
|
||||||
tag5_found = false
|
'tag5' => true,
|
||||||
tags.each {|element|
|
}
|
||||||
text = element.text
|
)
|
||||||
if text == 'tag1'
|
|
||||||
tag1_found = true
|
|
||||||
assert(true, 'tag1 exists')
|
|
||||||
elsif text == 'tag 2'
|
|
||||||
tag2_found = true
|
|
||||||
assert(true, 'tag 2 exists')
|
|
||||||
elsif text == 'tag3'
|
|
||||||
tag3_found = true
|
|
||||||
assert(true, 'tag3 exists')
|
|
||||||
elsif text == 'tag4'
|
|
||||||
tag4_found = true
|
|
||||||
assert(true, 'tag4 exists')
|
|
||||||
elsif text == 'tag5'
|
|
||||||
tag5_found = true
|
|
||||||
assert(true, 'tag5 exists')
|
|
||||||
else
|
|
||||||
assert(false, "invalid tag '#{text}'")
|
|
||||||
end
|
|
||||||
}
|
|
||||||
assert(tag1_found, 'tag1 exists')
|
|
||||||
assert(tag2_found, 'tag2 exists')
|
|
||||||
assert(tag3_found, 'tag3 exists')
|
|
||||||
assert(tag4_found, 'tag4 exists')
|
|
||||||
assert(tag5_found, 'tag5 exists')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_b_link
|
def test_b_tags
|
||||||
|
tag_prefix = "tag#{rand(999_999_999)}"
|
||||||
|
|
||||||
|
@browser = browser_instance
|
||||||
|
login(
|
||||||
|
username: 'master@example.com',
|
||||||
|
password: 'test',
|
||||||
|
url: browser_url,
|
||||||
|
)
|
||||||
|
tasks_close_all()
|
||||||
|
|
||||||
|
click(css: 'a[href="#manage"]')
|
||||||
|
click(css: 'a[href="#manage/tags"]')
|
||||||
|
switch(
|
||||||
|
css: '#content .js-newTagSetting',
|
||||||
|
type: 'off',
|
||||||
|
)
|
||||||
|
|
||||||
|
set(
|
||||||
|
css: '#content .js-create input[name="name"]',
|
||||||
|
value: tag_prefix + ' A',
|
||||||
|
)
|
||||||
|
click(css: '#content .js-create .js-submit')
|
||||||
|
set(
|
||||||
|
css: '#content .js-create input[name="name"]',
|
||||||
|
value: tag_prefix + ' a',
|
||||||
|
)
|
||||||
|
click(css: '#content .js-create .js-submit')
|
||||||
|
set(
|
||||||
|
css: '#content .js-create input[name="name"]',
|
||||||
|
value: tag_prefix + ' B',
|
||||||
|
)
|
||||||
|
click(css: '#content .js-create .js-submit')
|
||||||
|
set(
|
||||||
|
css: '#content .js-create input[name="name"]',
|
||||||
|
value: tag_prefix + ' C',
|
||||||
|
)
|
||||||
|
click(css: '#content .js-create .js-submit')
|
||||||
|
|
||||||
|
# set tag (by tab)
|
||||||
|
ticket1 = ticket_create(
|
||||||
|
data: {
|
||||||
|
customer: 'nico',
|
||||||
|
group: 'Users',
|
||||||
|
title: 'some subject 123äöü - tags no new 1',
|
||||||
|
body: 'some body 123äöü - tags no new 1',
|
||||||
|
},
|
||||||
|
do_not_submit: true,
|
||||||
|
)
|
||||||
|
sleep 1
|
||||||
|
set(
|
||||||
|
css: '.active .ticket-form-bottom .token-input',
|
||||||
|
value: "#{tag_prefix} A",
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
sendkey(value: :tab)
|
||||||
|
sleep 1
|
||||||
|
set(
|
||||||
|
css: '.active .ticket-form-bottom .token-input',
|
||||||
|
value: "#{tag_prefix} a",
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
sendkey(value: :tab)
|
||||||
|
sleep 1
|
||||||
|
set(
|
||||||
|
css: '.active .ticket-form-bottom .token-input',
|
||||||
|
value: "#{tag_prefix} B",
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
sendkey(value: :tab)
|
||||||
|
sleep 1
|
||||||
|
set(
|
||||||
|
css: '.active .ticket-form-bottom .token-input',
|
||||||
|
value: 'NOT EXISTING',
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
sendkey(value: :tab)
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
click(
|
||||||
|
css: '.active .newTicket button.js-submit',
|
||||||
|
)
|
||||||
|
sleep 5
|
||||||
|
if @browser.current_url !~ /#{Regexp.quote('#ticket/zoom/')}/
|
||||||
|
raise 'Unable to create ticket!'
|
||||||
|
end
|
||||||
|
|
||||||
|
# verify tags
|
||||||
|
tags_verify(
|
||||||
|
tags: {
|
||||||
|
"#{tag_prefix} A" => true,
|
||||||
|
"#{tag_prefix} a" => true,
|
||||||
|
"#{tag_prefix} B" => true,
|
||||||
|
'NOT EXISTING' => false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# new ticket with tags in zoom
|
||||||
|
ticket1 = ticket_create(
|
||||||
|
data: {
|
||||||
|
customer: 'nico',
|
||||||
|
group: 'Users',
|
||||||
|
title: 'some subject 123äöü - tags no new 2',
|
||||||
|
body: 'some body 223äöü - tags no new 1',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
click(css: '.active .sidebar .js-newTagLabel')
|
||||||
|
set(
|
||||||
|
css: '.active .sidebar .js-newTagInput',
|
||||||
|
value: "#{tag_prefix} A",
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
sendkey(value: :tab)
|
||||||
|
click(css: '.active .sidebar .js-newTagLabel')
|
||||||
|
set(
|
||||||
|
css: '.active .sidebar .js-newTagInput',
|
||||||
|
value: "#{tag_prefix} a",
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
sendkey(value: :tab)
|
||||||
|
click(css: '.active .sidebar .js-newTagLabel')
|
||||||
|
set(
|
||||||
|
css: '.active .sidebar .js-newTagInput',
|
||||||
|
value: "#{tag_prefix} B",
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
sendkey(value: :tab)
|
||||||
|
click(css: '.active .sidebar .js-newTagLabel')
|
||||||
|
set(
|
||||||
|
css: '.active .sidebar .js-newTagInput',
|
||||||
|
value: 'NOT EXISTING',
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
sendkey(value: :tab)
|
||||||
|
|
||||||
|
# verify tags
|
||||||
|
tags_verify(
|
||||||
|
tags: {
|
||||||
|
"#{tag_prefix} A" => true,
|
||||||
|
"#{tag_prefix} a" => true,
|
||||||
|
"#{tag_prefix} B" => true,
|
||||||
|
'NOT EXISTING' => false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
reload()
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# verify tags
|
||||||
|
tags_verify(
|
||||||
|
tags: {
|
||||||
|
"#{tag_prefix} A" => true,
|
||||||
|
"#{tag_prefix} a" => true,
|
||||||
|
"#{tag_prefix} B" => true,
|
||||||
|
'NOT EXISTING' => false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
click(css: 'a[href="#manage"]')
|
||||||
|
click(css: 'a[href="#manage/tags"]')
|
||||||
|
switch(
|
||||||
|
css: '#content .js-newTagSetting',
|
||||||
|
type: 'on',
|
||||||
|
)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_c_link
|
||||||
|
|
||||||
@browser = browser_instance
|
@browser = browser_instance
|
||||||
login(
|
login(
|
||||||
|
|
|
@ -3019,6 +3019,47 @@ wait untill text in selector disabppears
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
tags_verify(
|
||||||
|
browser: browser2,
|
||||||
|
tags: {
|
||||||
|
'tag 1' => true,
|
||||||
|
'tag 2' => true,
|
||||||
|
'tag 3' => false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def tags_verify(params = {})
|
||||||
|
switch_window_focus(params)
|
||||||
|
log('tags_verify', params)
|
||||||
|
|
||||||
|
instance = params[:browser] || @browser
|
||||||
|
|
||||||
|
tags = instance.find_elements({ css: '.content.active .js-tag' })
|
||||||
|
assert(tags)
|
||||||
|
assert(tags[0])
|
||||||
|
|
||||||
|
tags_found = {}
|
||||||
|
params[:tags].each {|key, _value|
|
||||||
|
tags_found[key] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.each {|element|
|
||||||
|
text = element.text
|
||||||
|
if tags_found.key?(text)
|
||||||
|
tags_found[text] = true
|
||||||
|
else
|
||||||
|
assert(false, "tag exists but is not in check to verify '#{text}'")
|
||||||
|
end
|
||||||
|
}
|
||||||
|
params[:tags].each {|key, value|
|
||||||
|
assert_equal(value, tags_found[key], "tag '#{key}'")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def quote(string)
|
def quote(string)
|
||||||
string_quoted = string
|
string_quoted = string
|
||||||
string_quoted.gsub!(/&/, '&')
|
string_quoted.gsub!(/&/, '&')
|
||||||
|
|
|
@ -39,6 +39,23 @@ class TagTest < ActiveSupport::TestCase
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
tag_add: {
|
||||||
|
item: 'TAG2',
|
||||||
|
object: 'Object1',
|
||||||
|
o_id: 123,
|
||||||
|
created_by_id: 1
|
||||||
|
},
|
||||||
|
verify: {
|
||||||
|
object: 'Object1',
|
||||||
|
items: {
|
||||||
|
'tag1' => true,
|
||||||
|
'tag2' => true,
|
||||||
|
'TAG2' => true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
# test 2
|
# test 2
|
||||||
{
|
{
|
||||||
tag_add: {
|
tag_add: {
|
||||||
|
@ -68,7 +85,7 @@ class TagTest < ActiveSupport::TestCase
|
||||||
object: 'Object2',
|
object: 'Object2',
|
||||||
items: {
|
items: {
|
||||||
'tagöäüß1' => true,
|
'tagöäüß1' => true,
|
||||||
'tagöäüß2' => true,
|
'Tagöäüß2' => true,
|
||||||
'tagöäüß3' => false,
|
'tagöäüß3' => false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -87,6 +104,25 @@ class TagTest < ActiveSupport::TestCase
|
||||||
items: {
|
items: {
|
||||||
'tag1' => false,
|
'tag1' => false,
|
||||||
'tag2' => true,
|
'tag2' => true,
|
||||||
|
'TAG2' => true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
# test 5
|
||||||
|
{
|
||||||
|
tag_remove: {
|
||||||
|
item: 'TAG2',
|
||||||
|
object: 'Object1',
|
||||||
|
o_id: 123,
|
||||||
|
created_by_id: 1
|
||||||
|
},
|
||||||
|
verify: {
|
||||||
|
object: 'Object1',
|
||||||
|
items: {
|
||||||
|
'tag1' => false,
|
||||||
|
'tag2' => true,
|
||||||
|
'TAG2' => false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -96,19 +132,19 @@ class TagTest < ActiveSupport::TestCase
|
||||||
tags = nil
|
tags = nil
|
||||||
if test[:tag_add]
|
if test[:tag_add]
|
||||||
tags = test[:tag_add]
|
tags = test[:tag_add]
|
||||||
success = Tag.tag_add( tags )
|
success = Tag.tag_add(tags)
|
||||||
assert( success, 'Tag.tag_add successful')
|
assert(success, 'Tag.tag_add successful')
|
||||||
else
|
else
|
||||||
tags = test[:tag_remove]
|
tags = test[:tag_remove]
|
||||||
success = Tag.tag_remove( tags )
|
success = Tag.tag_remove(tags)
|
||||||
assert( success, 'Tag.tag_remove successful')
|
assert(success, 'Tag.tag_remove successful')
|
||||||
end
|
end
|
||||||
list = Tag.tag_list( tags )
|
list = Tag.tag_list(tags)
|
||||||
test[:verify][:items].each {|key, value|
|
test[:verify][:items].each {|key, value|
|
||||||
if value == true
|
if value == true
|
||||||
assert( list.include?( key ), "Tag verify - should exists but exists #{key}")
|
assert(list.include?(key), "Tag verify - should exists but exists #{key}")
|
||||||
else
|
else
|
||||||
assert( !list.include?( key ), "Tag verify - exists but should not #{key}")
|
assert(!list.include?(key), "Tag verify - exists but should not #{key}")
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,10 +157,10 @@ class TagTest < ActiveSupport::TestCase
|
||||||
else
|
else
|
||||||
test[:tag_remove]
|
test[:tag_remove]
|
||||||
end
|
end
|
||||||
success = Tag.tag_remove( tags )
|
success = Tag.tag_remove(tags)
|
||||||
assert( success, 'Tag.tag_remove successful')
|
assert(success, 'Tag.tag_remove successful')
|
||||||
list = Tag.tag_list( tags )
|
list = Tag.tag_list(tags)
|
||||||
assert( !list.include?( tags[:item] ), 'Tag entry destroyed')
|
assert(!list.include?(tags[:item]), 'Tag entry destroyed')
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue