Skip to content

Commit

Permalink
Fixes #4482 - Pre-defined webhooks ready to use.
Browse files Browse the repository at this point in the history
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Tobias Schäfer <ts@zammad.com>
  • Loading branch information
3 people committed Apr 18, 2023
1 parent 388d806 commit ac4c740
Show file tree
Hide file tree
Showing 38 changed files with 1,027 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
class App.ControllerGenericIndex extends App.Controller
events:
'click [data-type=edit]': 'edit'
'click [data-type=new]': 'new'
'click [data-type=edit]': 'edit'
'click [data-type=new]': 'new'
'click [data-type=payload]': 'payload'
'click [data-type=import]': 'import'
'click .js-description': 'description'
'click [data-type=import]': 'import'
'click .js-description': 'description'

constructor: ->
super
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ class App.ControllerForm extends App.Controller
attribute.id = "#{idPrefix}_#{attribute.name}"

# set label class name
attribute.label_class = @model.labelClass
attribute.label_class = @model.labelClass or attribute.label_class

# set autofocus
if @autofocus && attributeCount is 1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# coffeelint: disable=camel_case_classes
class App.UiElement.switch
@render: (attributeConfig) ->
item = $( App.view('generic/switch')( attribute: attributeConfig ) )
item.find('input').data('field-type', 'boolean')
item
26 changes: 0 additions & 26 deletions app/assets/javascripts/app/controllers/layout_ref.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -1664,39 +1664,13 @@ App.Config.set( 'layout_ref/calendar_subscriptions', CalendarSubscriptionsRef, '

class ButtonsRef extends App.ControllerAppContent

elements:
'.js-submitDropdown': 'buttonDropdown'

events:
'click .js-openDropdown': 'toggleMenu'
'mouseenter .js-dropdownAction': 'onActionMouseEnter'
'mouseleave .js-dropdownAction': 'onActionMouseLeave'

constructor: ->
super
@render()

render: ->
@html App.view('layout_ref/buttons')

toggleMenu: =>
if @buttonDropdown.hasClass('is-open')
@closeMenu()
return
@openMenu()

closeMenu: =>
@buttonDropdown.removeClass 'is-open'

openMenu: =>
@buttonDropdown.addClass 'is-open'

onActionMouseEnter: (e) =>
@$(e.currentTarget).addClass('is-active')

onActionMouseLeave: (e) =>
@$(e.currentTarget).removeClass('is-active')

App.Config.set( 'layout_ref/buttons', ButtonsRef, 'Routes' )

class MergeCustomerRef extends App.ControllerAppContent
Expand Down
183 changes: 181 additions & 2 deletions app/assets/javascripts/app/controllers/webhook.coffee
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
class Index extends App.ControllerSubContent
requiredPermission: 'admin.webhook'
header: __('Webhooks')

events:
'click [data-type=predefined]': 'choosePreDefinedWebhook'

constructor: ->
super

@genericController = new App.ControllerGenericIndex(
@genericController = new WebhookIndex(
el: @el
id: @id
genericObject: 'Webhook'
Expand All @@ -23,12 +27,20 @@ class Index extends App.ControllerSubContent
]
buttons: [
{ name: __('Example Payload'), 'data-type': 'payload', class: 'btn' }
{ name: __('New Webhook'), 'data-type': 'new', class: 'btn--success' }
{
name: __('New Webhook')
'data-type': 'new'
class: 'btn--success'
menu: [
{ name: __('Pre-defined Webhook'), 'data-type': 'predefined' }
]
}
]
logFacility: 'webhook'
payloadExampleUrl: '/api/v1/webhooks/preview'
container: @el.closest('.content')
veryLarge: true
handlers: [@customPayloadCollapseHandler]
validateOnSubmit: @validateOnSubmit
)

Expand All @@ -39,6 +51,35 @@ class Index extends App.ControllerSubContent

@genericController.paginate( @page || 1 )

disableSwitchCallback: ->
$(@).parents('form').find('[data-attribute-name="customized_payload"] label').css('pointer-events', 'none')

enableSwitchCallback: ->
$(@).parents('form').find('[data-attribute-name="customized_payload"] label').css('pointer-events', '')

customPayloadCollapseHandler: (params, attribute, attributes, classname, form, ui) =>
return if attribute.name isnt 'customized_payload'

customPayloadCollapseWidget = form.find('[data-attribute-name="custom_payload"] .panel-collapse')

# Prevent triggering duplicate events by disabling switch pointer events during collapsing.
customPayloadCollapseWidget
.off('show.bs.collapse hide.bs.collapse', @disableSwitchCallback)
.on('show.bs.collapse hide.bs.collapse', @disableSwitchCallback)

# Make sure the pointer events are re-enabled after collapsing.
customPayloadCollapseWidget
.off('shown.bs.collapse hidden.bs.collapse', @enableSwitchCallback)
.on('shown.bs.collapse hidden.bs.collapse', @enableSwitchCallback)

# Show or hide the custom payload widget depending on the switch value.
if params.customized_payload
customPayloadCollapseWidget.collapse('show')
form.find('[data-attribute-name="custom_payload"]').css('margin-bottom', '')
else
customPayloadCollapseWidget.collapse('hide')
form.find('[data-attribute-name="custom_payload"]').css('margin-bottom', '0')

validateOnSubmit: (params) ->
return if _.isEmpty(params['custom_payload'])

Expand All @@ -56,4 +97,142 @@ class Index extends App.ControllerSubContent

errors

choosePreDefinedWebhook: (e) =>
e.preventDefault()

new ChoosePreDefinedWebhook(
container: @el.closest('.content')
callback: @newPreDefinedWebhook
)

newPreDefinedWebhook: (webhook) =>
new NewPreDefinedWebhook(
genericObject: 'Webhook'
pageData:
object: __('Webhook')
container: @el.closest('.content')
veryLarge: true
handlers: [@customPayloadCollapseHandler]
validateOnSubmit: @validateOnSubmit
preDefinedWebhook: webhook
)

class WebhookIndex extends App.ControllerGenericIndex
editControllerClass: -> EditWebhook

class ChoosePreDefinedWebhook extends App.ControllerModal
buttonClose: true
buttonCancel: true
buttonSubmit: __('Next')
buttonClass: 'btn--primary'
head: __('Pre-defined Webhook')
veryLarge: true
shown: false

constructor: ->
super

App.PreDefinedWebhook.subscribe(@render, initFetch: true)

content: ->
content = $(App.view('pre_defined_webhook')())

preDefinedWebhooksSelection = (el) ->
selection = App.UiElement.select.render(
id: 'preDefinedWebhooks'
name: 'pre_defined_webhook_id'
multiple: false
limit: 100
null: false
relation: 'PreDefinedWebhook'
nulloption: false
)
el.html(selection)

preDefinedWebhooksSelection(content.find('.js-preDefinedWebhooks'))

content

onSubmit: (e) =>
@formDisable(e)
params = @formParam(e.target)
webhook = App.PreDefinedWebhook.find(params.pre_defined_webhook_id)
@close()
@callback(webhook)

PreDefinedWebhookMixin =
field_prefix: 'preferences::pre_defined_webhook'

preDefinedWebhookAttributes: ->

# Make a deep clone of the pre-defined webhook field definition.
fields = $.extend(true, {}, @preDefinedWebhook.fields)

# Include pre-defined webhook type as a disabled field.
attrs = [
name: 'pre_defined_webhook_type'
display: __('Pre-defined Webhook')
null: true
tag: 'select'
relation: 'PreDefinedWebhook'
value: @preDefinedWebhook.id
disabled: true
]

# Append preferences field prefix to all field names.
attrs = attrs.concat(
_.map fields,
(field) =>
field.name = "#{@field_prefix}::#{field.name}"
field
)

attrs

contentFormModel: ->

# Make a deep clone of the pre-defined webhook field definition.
attrs = $.extend(true, [], App[@genericObject].configure_attributes)

# Process edit forms conditionally, in case we are dealing with a pre-defined webhook.
if not @preDefinedWebhook and @item?.pre_defined_webhook_type
@preDefinedWebhook = App.PreDefinedWebhook.find(@item.pre_defined_webhook_type)

# Add pre-defined webhook fields as additional attributes.
if @preDefinedWebhook
customizedPayloadIndex = _.findIndex(attrs, (attr) -> attr.name is 'customized_payload')

# Inject the fields right above the regular `customized_payload` attribute.
if customizedPayloadIndex isnt -1
attrs.splice(customizedPayloadIndex, 0, @preDefinedWebhookAttributes()...)

# As a fallback, inject the fields to the end of the form.
else
attrs = attrs.concat @preDefinedWebhookAttributes()

{ configure_attributes: attrs }

class NewPreDefinedWebhook extends App.ControllerGenericNew
@include PreDefinedWebhookMixin

# Inject the pre-defined webhook data into the form.
contentFormParams: ->
name: App.i18n.translatePlain(@preDefinedWebhook.name)
custom_payload: @preDefinedWebhook.custom_payload
note: App.i18n.translatePlain('Pre-defined webhook for %s.', App.i18n.translatePlain(@preDefinedWebhook.name))

class EditWebhook extends App.ControllerGenericEdit
shown: false

@include PreDefinedWebhookMixin

constructor: ->
super

App.PreDefinedWebhook.subscribe(@render, initFetch: true)

# Inject the pre-defined webhook data into the form.
contentFormParams: ->
$.extend(true, @item, { custom_payload: @preDefinedWebhook?.custom_payload if not @item.customized_payload })

App.Config.set('Webhook', { prio: 3350, name: __('Webhook'), parent: '#manage', target: '#manage/webhook', controller: Index, permission: ['admin.webhook'] }, 'NavBarAdmin')
4 changes: 4 additions & 0 deletions app/assets/javascripts/app/models/pre_defined_webhook.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class App.PreDefinedWebhook extends App.Model
@configure 'PreDefinedWebhook', 'name', 'custom_payload', 'fields'
@extend Spine.Model.Ajax
@url: @apiPath + '/webhooks/pre_defined'
19 changes: 10 additions & 9 deletions app/assets/javascripts/app/models/webhook.coffee
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
class App.Webhook extends App.Model
@configure 'Webhook', 'name', 'endpoint', 'signature_token', 'ssl_verify', 'basic_auth_username', 'basic_auth_password', 'custom_payload', 'note', 'active'
@configure 'Webhook', 'name', 'endpoint', 'signature_token', 'ssl_verify', 'basic_auth_username', 'basic_auth_password', 'pre_defined_webhook_type', 'customized_payload', 'custom_payload', 'note', 'preferences', 'active'
@extend Spine.Model.Ajax
@url: @apiPath + '/webhooks'
@configure_attributes = [
{ name: 'name', display: __('Name'), tag: 'input', type: 'text', limit: 250, null: false },
{ name: 'endpoint', display: __('Endpoint'), tag: 'input', type: 'text', limit: 300, null: false, placeholder: 'https://target.example.com/webhook' },
{ name: 'signature_token', display: __('HMAC SHA1 Signature Token'), tag: 'input', type: 'text', limit: 100, null: true },
{ name: 'ssl_verify', display: __('SSL Verify'), tag: 'boolean', null: true, translate: true, options: { true: 'yes', false: 'no' }, default: true },
{ name: 'name', display: __('Name'), tag: 'input', type: 'text', limit: 250, null: false },
{ name: 'endpoint', display: __('Endpoint'), tag: 'input', type: 'text', limit: 300, null: false, placeholder: 'https://target.example.com/webhook' },
{ name: 'signature_token', display: __('HMAC SHA1 Signature Token'), tag: 'input', type: 'text', limit: 100, null: true },
{ name: 'ssl_verify', display: __('SSL Verify'), tag: 'boolean', null: true, translate: true, options: { true: 'yes', false: 'no' }, default: true },
{ name: 'basic_auth_username', display: __('HTTP Basic Authentication Username'), tag: 'input', type: 'text', limit: 250, null: true, item_class: 'formGroup--halfSize' },
{ name: 'basic_auth_password', display: __('HTTP Basic Authentication Password'), tag: 'input', type: 'text', limit: 250, null: true, item_class: 'formGroup--halfSize' },
{ name: 'custom_payload', display: __('Custom Payload'), tag: 'code_editor', null: true, collapsible: true },
{ name: 'note', display: __('Note'), tag: 'textarea', note: '', limit: 250, null: true },
{ name: 'active', display: __('Active'), tag: 'active', default: true },
{ name: 'updated_at', display: __('Updated'), tag: 'datetime', readonly: 1 },
{ name: 'customized_payload', display: __('Custom Payload'), tag: 'switch', null: true, label_class: 'hidden' },
{ name: 'custom_payload', display: __('Custom Payload'), tag: 'code_editor', null: true, collapsible: true, label_class: 'hidden' },
{ name: 'note', display: __('Note'), tag: 'textarea', null: true, note: '', limit: 250 },
{ name: 'active', display: __('Active'), tag: 'active', default: true },
{ name: 'updated_at', display: __('Updated'), tag: 'datetime', readonly: 1 },
]
@configure_delete = true
@configure_clone = true
Expand Down
14 changes: 13 additions & 1 deletion app/assets/javascripts/app/views/generic/admin/index.jst.eco
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@
<% end %>
<% if @buttons: %>
<% for button in @buttons: %>
<a data-type="<%= button['data-type'] %>" class="btn <%= button.class %>" href="<%= button.href %>"><%- @T(button.name) %></a>
<% if button.menu: %>
<div class="buttonDropdown dropdown">
<button data-type="<%= button['data-type'] %>" class="btn btn--split--first <%= button.class %>" href="<%= button.href %>"><%- @T(button.name) %></button>
<button class="btn btn--slim btn--split--last <%= button.class %>" data-toggle="dropdown" data-bs-auto-close="outside"><%- @Icon('arrow-down') %></button>
<ul class="dropdown-menu dropdown-menu" role="menu" aria-labelledby="userAction">
<% for item in button.menu: %>
<li class="<%= item.class %>" role="menuitem" data-type="<%= item['data-type'] %>"><%- @T(item.name) %></li>
<% end %>
</ul>
</div>
<% else: %>
<a data-type="<%= button['data-type'] %>" class="btn <%= button.class %>" href="<%= button.href %>"><%- @T(button.name) %></a>
<% end %>
<% end %>
<% end %>
</div>
Expand Down
10 changes: 10 additions & 0 deletions app/assets/javascripts/app/views/generic/switch.jst.eco
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div class="<%= @attribute.class %> horizontal-filters-switch horizontal-filters-switch--align-start">
<label>
<%- @T(@attribute.display) %>
<div class="zammad-switch zammad-switch--small js-switch">
<input name="<%= @attribute.name %>" type="checkbox" value="true" id="attribute-<%= @attribute.name %>" <% if @attribute.value: %>checked<% end %> <% if @attribute.disabled: %>disabled<% end %>>
<label for="attribute-<%= @attribute.name %>"></label>
</div>
</label>
<% if @attribute.note: %><span class="help-text"><%- @T(@attribute.note) %></span><% end %>
</div>
24 changes: 12 additions & 12 deletions app/assets/javascripts/app/views/layout_ref/buttons.jst.eco
Original file line number Diff line number Diff line change
Expand Up @@ -60,29 +60,29 @@

<h3>Dropdown</h3>

<div class="buttonDropdown dropup js-submitDropdown" style="margin-left: 0px;">
<button class="btn btn--primary btn--split--first js-submit">Dropdown UP</button>
<button class="btn btn--primary btn--slim btn--split--last js-openDropdown">
<svg class="icon icon-arrow-up "><use xlink:href="assets/images/icons.svg#icon-arrow-up"></use></svg>
<div class="buttonDropdown dropup" style="margin-left: 0px;">
<button class="btn btn--primary btn--split--first js-submit">Dropdown Up</button>
<button class="btn btn--primary btn--slim btn--split--last" data-toggle="dropdown" data-bs-auto-close="outside">
<svg class="icon icon-arrow-up"><use xlink:href="assets/images/icons.svg#icon-arrow-up"></use></svg>
</button>
<ul class="dropdown-menu dropdown-menu" role="menu" aria-labelledby="userAction" style="min-width: 195px;">
<li class="js-dropdownAction" role="menuitem" data-id="1">Value 1</li>
<li class="js-dropdownAction" role="menuitem" data-id="2">Value 2</li>
<li class="js-dropdownAction" role="menuitem" data-id="3">Value 3</li>
<li role="menuitem" data-id="1">Value 1</li>
<li role="menuitem" data-id="2">Value 2</li>
<li role="menuitem" data-id="3">Value 3</li>
</ul>
</div>

<br>

<div class="buttonDropdown dropdown js-submitDropdownDown" style="margin-left: 0px;">
<div class="buttonDropdown dropdown" style="margin-left: 0px;">
<button class="btn btn--primary btn--split--first js-submit">Dropdown Down</button>
<button class="btn btn--primary btn--slim btn--split--last js-openDropdownDown">
<button class="btn btn--primary btn--slim btn--split--last" data-toggle="dropdown" data-bs-auto-close="outside">
<svg class="icon icon-arrow-down"><use xlink:href="assets/images/icons.svg#icon-arrow-down"></use></svg>
</button>
<ul class="dropdown-menu dropdown-menu" role="menu" aria-labelledby="userAction" style="min-width: 195px;">
<li class="js-dropdownActionDown" role="menuitem" data-id="1">Value 1</li>
<li class="js-dropdownActionDown" role="menuitem" data-id="2">Value 2</li>
<li class="js-dropdownActionDown" role="menuitem" data-id="3">Value 3</li>
<li role="menuitem" data-id="1">Value 1</li>
<li role="menuitem" data-id="2">Value 2</li>
<li role="menuitem" data-id="3">Value 3</li>
</ul>
</div>

Expand Down
Loading

0 comments on commit ac4c740

Please sign in to comment.