From 848175a290e69a3903a4ebbe88bcb52d8413e382 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 12 Aug 2019 09:53:24 +0200 Subject: [PATCH] Fixes issue #2103 - clickable URL (template) for ObjectManager Attribute. --- .../_application_controller_form.coffee | 5 ++++ .../object_manager_attribute.coffee | 24 +++++++++++++++ app/assets/javascripts/app/index.coffee | 26 +++++++++++++---- .../javascripts/app/lib/app_post/utils.coffee | 3 +- .../app/lib/mixins/view_helpers.coffee | 3 ++ .../app/views/generic/attribute.jst.eco | 9 ++++-- .../object_manager/attribute/input.jst.eco | 1 + .../object_manager/attribute/select.jst.eco | 1 + app/assets/stylesheets/zammad.scss | 20 ++++++++++++- app/models/object_manager/attribute.rb | 2 ++ public/assets/tests/form.js | 29 +++++++++++++++++++ public/assets/tests/html_utils.js | 10 ++++++- 12 files changed, 122 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/app/controllers/_application_controller_form.coffee b/app/assets/javascripts/app/controllers/_application_controller_form.coffee index 5e0c39982..2771d92b1 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_form.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_form.coffee @@ -360,11 +360,16 @@ class App.ControllerForm extends App.Controller return item else + placeholderObjects = {} + if @model.className && !_.isEmpty(attribute.linktemplate) && !_.isEmpty(@params[attribute.name]) + placeholderObjects = { attribute: attribute, user: App.Session.get(), config: App.Config.all() } + placeholderObjects[@model.className.toLowerCase()] = @params fullItem = $( App.view('generic/attribute')( attribute: attribute, item: '', bookmarkable: @bookmarkable + placeholderObjects: placeholderObjects ) ) fullItem.find('.controls').prepend(item) diff --git a/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee b/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee index 76eec780d..149c78caf 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee @@ -194,9 +194,21 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi noFieldset: true params: params ) + configureAttributes = [ + # coffeelint: disable=no_interpolation_in_single_quotes + { name: 'data_option::linktemplate', display: 'Link-Template', tag: 'input', type: 'text', null: true, default: '', placeholder: 'https://example.com/?q=#{object.attribute_name} - use ticket, user or organization as object' }, + # coffeelint: enable=no_interpolation_in_single_quotes + ] + inputLinkTemplate = new App.ControllerForm( + model: + configure_attributes: configureAttributes + noFieldset: true + params: params + ) item.find('.js-inputDefault').html(inputDefault.form) item.find('.js-inputType').html(inputType.form) item.find('.js-inputMaxlength').html(inputMaxlength.form) + item.find('.js-inputLinkTemplate').html(inputLinkTemplate.form) @datetime: (item, localParams, params) -> configureAttributes = [ @@ -311,6 +323,18 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi return lastSelected = value ) + configureAttributes = [ + # coffeelint: disable=no_interpolation_in_single_quotes + { name: 'data_option::linktemplate', display: 'Link-Template', tag: 'input', type: 'text', null: true, default: '', placeholder: 'https://example.com/?q=#{ticket.attribute_name}' }, + # coffeelint: enable=no_interpolation_in_single_quotes + ] + inputLinkTemplate = new App.ControllerForm( + model: + configure_attributes: configureAttributes + noFieldset: true + params: params + ) + item.find('.js-inputLinkTemplate').html(inputLinkTemplate.form) @buildRow: (element, child, level = 0, parentElement) -> newRow = element.find('.js-template').clone().removeClass('js-template') diff --git a/app/assets/javascripts/app/index.coffee b/app/assets/javascripts/app/index.coffee index 4ec67da63..786d7846f 100644 --- a/app/assets/javascripts/app/index.coffee +++ b/app/assets/javascripts/app/index.coffee @@ -46,10 +46,10 @@ class App extends Spine.Controller if object[attributeNameWithoutRef] valueRef = object[attributeNameWithoutRef] - @viewPrintItem(value, attributeConfig, valueRef, table) + @viewPrintItem(value, attributeConfig, valueRef, table, object) # define print name helper - @viewPrintItem: (item, attributeConfig = {}, valueRef, table) -> + @viewPrintItem: (item, attributeConfig = {}, valueRef, table, object) -> return '-' if item is undefined return '-' if item is '' return item if item is null @@ -107,18 +107,23 @@ class App extends Spine.Controller # translate content if attributeConfig.translate || (isObject && item.translate && item.translate()) isHtmlEscape = true - resultLocal = App.i18n.translateContent(resultLocal) + resultLocal = App.i18n.translateContent(resultLocal) # transform date if attributeConfig.tag is 'date' isHtmlEscape = true resultLocal = App.i18n.translateDate(resultLocal) + linktemplate = @_placeholderReplacement(object, attributeConfig, resultLocal) + if linktemplate && isHtmlEscape is false + resultLocal = linktemplate + isHtmlEscape = true + # transform input tel|url to make it clickable - if attributeConfig.tag is 'input' + if attributeConfig.tag is 'input' && !linktemplate if attributeConfig.type is 'tel' resultLocal = "#{App.Utils.htmlEscape(resultLocal)}" - else if attributeConfig.type is 'url' + else if attributeConfig.type is 'url' && !linktemplate resultLocal = App.Utils.linkify(resultLocal) else resultLocal = App.Utils.htmlEscape(resultLocal) @@ -146,6 +151,17 @@ class App extends Spine.Controller result + @_placeholderReplacement: (object, attributeConfig, resultLocal) -> + return if !object + return if !attributeConfig + return if _.isEmpty(attributeConfig.linktemplate) + return if !object.constructor + return if !object.constructor.className + return if _.isEmpty(object[attributeConfig.name]) + placeholderObjects = { attribute: attributeConfig, user: App.Session.get(), config: App.Config.all() } + placeholderObjects[object.constructor.className.toLowerCase()] = object + "#{App.Utils.htmlEscape(resultLocal)}" + @view: (name) -> template = (params = {}) -> JST["app/views/#{name}"](_.extend(params, App.ViewHelpers)) diff --git a/app/assets/javascripts/app/lib/app_post/utils.coffee b/app/assets/javascripts/app/lib/app_post/utils.coffee index f29dbdfa6..4efc203b1 100644 --- a/app/assets/javascripts/app/lib/app_post/utils.coffee +++ b/app/assets/javascripts/app/lib/app_post/utils.coffee @@ -704,7 +704,7 @@ class App.Utils res.join('') # textReplaced = App.Utils.replaceTags( template, { user: { firstname: 'Bob', lastname: 'Smith' } } ) - @replaceTags: (template, objects) -> + @replaceTags: (template, objects, encodeLink = false) -> template = template.replace( /#\{\s{0,2}(.+?)\s{0,2}\}/g, (index, key) -> key = key.replace(/<.+?>/g, '') levels = key.split(/\./) @@ -744,6 +744,7 @@ class App.Utils else value = '' value = '-' if value is '' + value = encodeURIComponent(value) if encodeLink value ) diff --git a/app/assets/javascripts/app/lib/mixins/view_helpers.coffee b/app/assets/javascripts/app/lib/mixins/view_helpers.coffee index ea09574f9..e10f34de9 100644 --- a/app/assets/javascripts/app/lib/mixins/view_helpers.coffee +++ b/app/assets/javascripts/app/lib/mixins/view_helpers.coffee @@ -228,3 +228,6 @@ App.ViewHelpers = className: params.className iconset: params.iconset ) + + replacePlaceholder: (template, items, encodeLink = false) -> + App.Utils.replaceTags(template, items, encodeLink) diff --git a/app/assets/javascripts/app/views/generic/attribute.jst.eco b/app/assets/javascripts/app/views/generic/attribute.jst.eco index 4954cb96b..d039fec18 100644 --- a/app/assets/javascripts/app/views/generic/attribute.jst.eco +++ b/app/assets/javascripts/app/views/generic/attribute.jst.eco @@ -20,9 +20,12 @@

<% if @attribute.help: %><%- @T(@attribute.help) %><% end %><%- @attribute.helpLink %>

<% end %> -
- <% if @attribute.remove: %><% end %> - <% if @attribute.add: %><% end %> +
+ <% if !_.isEmpty(@attribute.linktemplate) && !_.isEmpty(@placeholderObjects): %> + + <%- @Icon('external') %> + + <% end %> <% if @attribute.style != 'block': %> <% if @attribute.help: %><%- @T(@attribute.help) %><% end %><%- @attribute.helpLink %> diff --git a/app/assets/javascripts/app/views/object_manager/attribute/input.jst.eco b/app/assets/javascripts/app/views/object_manager/attribute/input.jst.eco index fcf25f609..7b4e90d85 100644 --- a/app/assets/javascripts/app/views/object_manager/attribute/input.jst.eco +++ b/app/assets/javascripts/app/views/object_manager/attribute/input.jst.eco @@ -2,4 +2,5 @@
+
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco b/app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco index 3328f631e..0ec8ac85e 100644 --- a/app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco +++ b/app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco @@ -50,4 +50,5 @@ <%- @Icon('trash') %> <%- @T('Remove') %>
+
\ No newline at end of file diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 91ace3dba..5b12a7893 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -2102,10 +2102,20 @@ input.has-error { .controls--button { display: flex; + flex-wrap: wrap; + + .controls { + flex: 1; + } + + .help-inline, + .help-block { + flex-basis: 100%; + } input, .form-control { - flex: 1; + flex: 1 1 0%; @include bidi-style(border-right-width, 0, border-left-width, 1px); @include bidi-style(border-top-right-radius, 0, border-top-left-radius, 3px); @include bidi-style(border-bottom-right-radius, 0, border-bottom-left-radius, 3px); @@ -2154,6 +2164,14 @@ input.has-error { position: relative; border: 1px solid hsl(0, 0%, 90%); @include bidi-style(border-radius, 0 3px 3px 0, border-radius, 3px 0 0 3px); + + .icon { + fill: hsl(0,0%,61%); + } + + &:hover .icon { + fill: hsl(0,0%,33%); + } } .searchfield { diff --git a/app/models/object_manager/attribute.rb b/app/models/object_manager/attribute.rb index ef9a7dad0..2edf9a740 100644 --- a/app/models/object_manager/attribute.rb +++ b/app/models/object_manager/attribute.rb @@ -133,6 +133,7 @@ possible types maxlength: 200, null: true, note: 'some additional comment', # optional + link_template: '', # optional }, # select @@ -150,6 +151,7 @@ possible types multiple: false, # currently only "false" supported translate: true, # optional note: 'some additional comment', # optional + link_template: '', # optional }, # tree_select diff --git a/public/assets/tests/form.js b/public/assets/tests/form.js index 7f5e12b7f..4f14cddf7 100644 --- a/public/assets/tests/form.js +++ b/public/assets/tests/form.js @@ -1107,6 +1107,7 @@ test("object manager form 1", function() { var test_params = { data_option: { default: "", + linktemplate: "", maxlength: 120, type: "text" }, @@ -1218,6 +1219,7 @@ test("object manager form 2", function() { var test_params = { data_option: { default: "", + linktemplate: "", maxlength: 120, type: "text" }, @@ -1271,6 +1273,7 @@ test("object manager form 3", function() { var test_params = { data_option: { default: "", + linktemplate: "", maxlength: 120, type: "text" }, @@ -1308,6 +1311,7 @@ test("object manager form 3", function() { test_params = { data_option: { default: "", + linktemplate: "", maxlength: 120, type: "text" }, @@ -1596,3 +1600,28 @@ test("form deep nesting", function() { params = App.ControllerForm.params(el) deepEqual(params, defaults, 'nested params') }); + +test("form with external links", function() { + $('#forms').append('

form with external links

') + var el = $('#form20') + var defaults = { + a: '133', + b: 'abc d', + } + new App.ControllerForm({ + el: el, + model: { + configure_attributes: [ + { name: 'a', display: 'Input1', tag: 'input', type: 'text', limit: 100, null: true, linktemplate: "https://example.com/?q=#{ticket.a}" }, + { name: 'b', display: 'Select1', tag: 'select', type: 'text', options: { a: 1, b: 2 }, limit: 100, null: true, linktemplate: "https://example.com/?q=#{ticket.b}" }, + ], + className: 'Ticket', + }, + params: defaults, + }); + + params = App.ControllerForm.params(el) + deepEqual(params, defaults) + equal('https://example.com/?q=133', el.find('input[name="a"]').parents('.controls').find('a[href]').attr('href')) + equal('https://example.com/?q=abc%20d', el.find('select[name="b"]').parents('.controls').find('a[href]').attr('href')) +}); diff --git a/public/assets/tests/html_utils.js b/public/assets/tests/html_utils.js index 5b3bd77c3..36edadca4 100644 --- a/public/assets/tests/html_utils.js +++ b/public/assets/tests/html_utils.js @@ -1425,7 +1425,7 @@ test("check replace tags", function() { user = new App.User({ firstname: 'Bob', - lastname: 'Smith', + lastname: 'Smith Good', created_at: '2018-10-31T10:00:00Z', }) message = "
#{user.firstname} #{user.created_at}
" @@ -1451,6 +1451,14 @@ test("check replace tags", function() { } verify = App.Utils.replaceTags(message, data) equal(verify, result) + + message = "some text" + result = 'some text' + data = { + user: user + } + verify = App.Utils.replaceTags(message, data, true) + equal(verify, result) }); // check attibute validation