Browse Source

Fixes #4482 - Pre-defined webhooks ready to use.

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>
Dominik Klein 1 year ago
parent
commit
ac4c740440

+ 4 - 4
app/assets/javascripts/app/controllers/_application_controller/_generic_index.coffee

@@ -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

+ 1 - 1
app/assets/javascripts/app/controllers/_application_controller/form.coffee

@@ -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

+ 6 - 0
app/assets/javascripts/app/controllers/_ui_element/switch.coffee

@@ -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

+ 0 - 26
app/assets/javascripts/app/controllers/layout_ref.coffee

@@ -1664,14 +1664,6 @@ 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()
@@ -1679,24 +1671,6 @@ class ButtonsRef extends App.ControllerAppContent
   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

+ 181 - 2
app/assets/javascripts/app/controllers/webhook.coffee

@@ -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'
@@ -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
     )
 
@@ -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'])
 
@@ -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 - 0
app/assets/javascripts/app/models/pre_defined_webhook.coffee

@@ -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'

+ 10 - 9
app/assets/javascripts/app/models/webhook.coffee

@@ -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

+ 13 - 1
app/assets/javascripts/app/views/generic/admin/index.jst.eco

@@ -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>

+ 10 - 0
app/assets/javascripts/app/views/generic/switch.jst.eco

@@ -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>

+ 12 - 12
app/assets/javascripts/app/views/layout_ref/buttons.jst.eco

@@ -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>
 

Some files were not shown because too many files changed in this diff