Просмотр исходного кода

Fixes #5470 - Unify destination group setting and include e-mail-address in channel setup.

Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Dusan Vuckovic 3 недель назад
Родитель
Сommit
340d9c1014

+ 72 - 19
app/assets/javascripts/app/controllers/_channel/email.coffee

@@ -187,7 +187,7 @@ class ChannelEmailAccountOverview extends App.Controller
     e.preventDefault()
     id   = $(e.target).closest('.action').data('id')
     item = App.Channel.find(id)
-    new ChannelEmailEdit(
+    new ChannelGroupEdit(
       container: @el.closest('.content')
       item: item
       callback: @load
@@ -250,8 +250,9 @@ class ChannelEmailAccountOverview extends App.Controller
     id = $(e.target).closest('.action').data('id')
     @navigate "#channels/microsoft365/#{id}"
 
+class ChannelGroupEdit extends App.ControllerModal
+  @include App.DestinationGroupEmailAddressesMixin
 
-class ChannelEmailEdit extends App.ControllerModal
   buttonClose: true
   buttonCancel: true
   buttonSubmit: true
@@ -259,14 +260,18 @@ class ChannelEmailEdit extends App.ControllerModal
 
   content: =>
     configureAttributesBase = [
-      { name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', nulloption: true, filter: { active: true } },
+      { name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
+      { name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', options: @emailAddressOptions(@item.id, @item.group_id) },
     ]
+
     @form = new App.ControllerForm(
       model:
         configure_attributes: configureAttributesBase
         className: ''
       params: @item
+      handlers: [@destinationGroupEmailAddressFormHandler(@item)]
     )
+
     @form.form
 
   onSubmit: (e) =>
@@ -283,6 +288,8 @@ class ChannelEmailEdit extends App.ControllerModal
       @formValidate(form: e.target, errors: errors)
       return false
 
+    @processDestinationGroupEmailAddressParams(params)
+
     # disable form
     @formDisable(e)
 
@@ -303,6 +310,8 @@ class ChannelEmailEdit extends App.ControllerModal
     )
 
 class ChannelEmailAccountWizard extends App.ControllerWizardModal
+  @include App.DestinationGroupEmailAddressesMixin
+
   elements:
     '.modal-body': 'body'
   events:
@@ -382,8 +391,10 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
       { name: 'realname', display: __('Organization & Department Name'), tag: 'input',  type: 'text', limit: 160, null: false, placeholder: __('Organization Support'), autocomplete: 'off' },
       { name: 'email',    display: __('Email'),    tag: 'input',  type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, autocomplete: 'off' },
       { name: 'password', display: __('Password'), tag: 'input',  type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
-      { name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', nulloption: true },
+      { name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group' },
+      { name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', null: false, options: @emailAddressOptions(@channel?.id, @channel?.group_id) },
     ]
+
     @formMeta = new App.ControllerForm(
       el:    @$('.base-settings'),
       model:
@@ -408,6 +419,8 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
 
     # inbound
     configureAttributesInbound = [
+      { name: 'group_id',                display: __('Destination Group'), tag: 'select', null: false, relation: 'Group' },
+      { name: 'group_email_address_id',  display: __('Destination Group Email Address'), tag: 'select', null: false, options: @emailAddressOptions(@channel?.id, @channel?.group_id) },
       { name: 'adapter',                 display: __('Type'),     tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound, translate: true },
       { name: 'options::host',           display: __('Host'),     tag: 'input',  type: 'text', limit: 120, null: false, autocapitalize: false },
       { name: 'options::user',           display: __('User'),     tag: 'input',  type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off' },
@@ -419,15 +432,13 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
       { name: 'options::keep_on_server', display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' },
     ]
 
+    # If email inbound form is opened from the new email wizard, show additional fields on top.
     if !@channel
-      #Email Inbound form opened from new email wizard, show full settings
       configureAttributesInbound = [
         { name: 'options::realname', display: __('Organization & Department Name'), tag: 'input',  type: 'text', limit: 160, null: false, placeholder: __('Organization Support'), autocomplete: 'off' },
         { name: 'options::email',    display: __('Email'),    tag: 'input',  type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, autocomplete: 'off' },
-        { name: 'options::group_id', display: __('Destination Group'), tag: 'select', null: false, relation: 'Group', nulloption: true },
       ].concat(configureAttributesInbound)
 
-
     showHideFolder = (params, attribute, attributes, classname, form, ui) ->
       return if !params
       if params.adapter is 'imap'
@@ -437,23 +448,29 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
       ui.hide('options::folder')
       ui.hide('options::keep_on_server')
 
-    form = new App.ControllerForm(
+    @form = new App.ControllerForm(
       el:    @$('.base-inbound-settings'),
       model:
         configure_attributes: configureAttributesInbound
         className: ''
-      params: @account.inbound
+      params: _.extend(
+        @account.inbound
+        group_id: @account?.meta?.group_id or @channel?.group_id
+        group_email_address_id: @account?.meta?.group_email_address_id
+      )
       handlers: [
-        showHideFolder,
+        showHideFolder
+        @destinationGroupEmailAddressFormHandler(@channel)
       ]
     )
+
     @toggleInboundAdapter()
 
-    form.el.find("select[name='options::ssl']").off('change').on('change', (e) ->
+    @form.el.find("select[name='options::ssl']").off('change').on('change', (e) =>
       if $(e.target).val() is 'ssl'
-        form.el.find("[name='options::port']").val('993')
+        @form.el.find("[name='options::port']").val('993')
       else if $(e.target).val() is 'off'
-        form.el.find("[name='options::port']").val('143')
+        @form.el.find("[name='options::port']").val('143')
     )
 
   toggleInboundAdapter: =>
@@ -537,8 +554,21 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
 
   probeBasedOnIntro: (e) =>
     e.preventDefault()
+
+    # get params
     params = @formParam(e.target)
 
+    if not $(e.currentTarget).hasClass('js-expert')
+
+      # validate form
+      errors = @formMeta.validate(params)
+
+      # show errors in form
+      if errors
+        @log 'error', errors
+        @formValidate(form: e.target, errors: errors)
+        return false
+
     # remember account settings
     @account.meta = params
 
@@ -552,18 +582,21 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
       @$('.js-inbound [name="options::password"]').val(params.password)
       @$('.js-inbound [name="options::email"]').val(params.email)
       @$('.js-inbound [name="options::realname"]').val(params.realname)
-      @$('.js-inbound [name="options::group_id"]').val(params.group_id)
+      @$('.js-inbound [name="group_id"]').val(params.group_id)
+      @$('.js-inbound [name="group_email_address_id"]').val(params.group_email_address_id)
       return
 
     @disable(e)
     @$('.js-probe .js-email').text(params.email)
     @showSlide('js-probe')
 
+    data = _.pick(params, 'email', 'password')
+
     @ajax(
       id:   'email_probe'
       type: 'POST'
       url:  "#{@apiPath}/channels_email_probe"
-      data: JSON.stringify(params)
+      data: JSON.stringify(data)
       processData: true
       success: (data, status, xhr) =>
         if data.result is 'ok'
@@ -586,7 +619,8 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
           @$('.js-inbound [name="options::password"]').val(@account['meta']['password'])
           @$('.js-inbound [name="options::email"]').val(@account['meta']['email'])
           @$('.js-inbound [name="options::realname"]').val(@account['meta']['realname'])
-          @$('.js-inbound [name="options::group_id"]').val(@account['meta']['group_id'])
+          @$('.js-inbound [name="group_id"]').val(@account['meta']['group_id'])
+          @$('.js-inbound [name="group_email_address_id"]').val(@account['meta']['group_email_address_id'])
 
         @enable(e)
       error: =>
@@ -600,13 +634,25 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
     # get params
     params = @formParam(e.target)
 
+    # validate form
+    errors = @form.validate(params)
+
+    # show errors in form
+    if errors
+      @log 'error', errors
+      @formValidate(form: e.target, errors: errors)
+      return false
+
     if params.options && params.options.password is @passwordPlaceholder
       params.options.password = @inboundPassword
 
     # Update meta as the one from AttributesBase could be outdated
     @account.meta.realname = params.options.realname
     @account.meta.email = params.options.email
-    @account.meta.group_id = params.options.group_id
+    @account.meta.group_id = params.group_id
+    @account.meta.group_email_address_id = params.group_email_address_id
+    delete params.group_id
+    delete params.group_email_address_id
 
     # let backend know about the channel
     if @channel
@@ -817,11 +863,18 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
     if @channel
       params.channel_id = @channel.id
 
-    if params.meta.group_id
+    if params.meta?.group_id
       params.group_id = params.meta.group_id
-    else if @channel.group_id
+    else if @channel?.group_id
       params.group_id = @channel.group_id
 
+    # Copy group email address parameter from meta key to the root.
+    if not _.isUndefined(params.meta?.group_email_address_id)
+      params.group_email_address = params.meta.group_email_address_id isnt 'false'
+
+      if params.group_email_address and params.meta.group_email_address_id isnt 'true'
+        params.group_email_address_id = params.meta.group_email_address_id
+
     if !params.email && @channel
       email_addresses = App.EmailAddress.search(filter: { channel_id: @channel.id })
       if email_addresses && email_addresses[0]

+ 35 - 5
app/assets/javascripts/app/controllers/_channel/google.coffee

@@ -269,6 +269,8 @@ class ChannelAccountOverview extends App.ControllerSubContent
     )
 
 class ChannelInboundEdit extends App.ControllerModal
+  @include App.DestinationGroupEmailAddressesMixin
+
   buttonClose: true
   buttonCancel: true
   buttonSubmit: true
@@ -276,14 +278,20 @@ class ChannelInboundEdit extends App.ControllerModal
 
   content: =>
     configureAttributesBase = [
-      { name: 'options::folder',          display: __('Folder'),   tag: 'input',  type: 'text', limit: 120, null: true, autocapitalize: false, placeholder: __('optional') },
-      { name: 'options::keep_on_server',  display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false },
+      { name: 'group_id',                display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
+      { name: 'group_email_address_id',  display: __('Destination Group Email Address'), tag: 'select', null: false, options: @emailAddressOptions(@item.id, @item.group_id) },
+      { name: 'options::folder',         display: __('Folder'),   tag: 'input',  type: 'text', limit: 120, null: true, autocapitalize: false, placeholder: __('optional') },
+      { name: 'options::keep_on_server', display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false },
     ]
     @form = new App.ControllerForm(
       model:
         configure_attributes: configureAttributesBase
         className: ''
-      params: @item.options.inbound
+      params: _.extend(
+        @item.options.inbound
+        group_id: @item.group_id
+      )
+      handlers: [@destinationGroupEmailAddressFormHandler(@item)]
     )
     @form.form
 
@@ -302,6 +310,9 @@ class ChannelInboundEdit extends App.ControllerModal
       @formValidate(form: e.target, errors: errors)
       return false
 
+    data =
+      options: params.options
+
     # disable form
     @formDisable(e)
 
@@ -310,7 +321,7 @@ class ChannelInboundEdit extends App.ControllerModal
       id:   'channel_email_inbound'
       type: 'POST'
       url:  "#{@apiPath}/channels_google_inbound/#{@item.id}"
-      data: JSON.stringify(params)
+      data: JSON.stringify(data)
       processData: true
       success: (data, status, xhr) =>
         if data.content_messages or not @set_active
@@ -340,6 +351,8 @@ class ChannelInboundEdit extends App.ControllerModal
     if @set_active
       params['active'] = true
 
+    @processDestinationGroupEmailAddressParams(params)
+
     # update
     @ajax(
       id:   'channel_email_verify'
@@ -357,6 +370,8 @@ class ChannelInboundEdit extends App.ControllerModal
     )
 
 class ChannelGroupEdit extends App.ControllerModal
+  @include App.DestinationGroupEmailAddressesMixin
+
   buttonClose: true
   buttonCancel: true
   buttonSubmit: true
@@ -364,13 +379,15 @@ class ChannelGroupEdit extends App.ControllerModal
 
   content: =>
     configureAttributesBase = [
-      { name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', nulloption: true, filter: { active: true } },
+      { name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
+      { name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', options: @emailAddressOptions(@item.id, @item.group_id) },
     ]
     @form = new App.ControllerForm(
       model:
         configure_attributes: configureAttributesBase
         className: ''
       params: @item
+      handlers: [@destinationGroupEmailAddressFormHandler(@item)]
     )
     @form.form
 
@@ -388,6 +405,8 @@ class ChannelGroupEdit extends App.ControllerModal
       @formValidate(form: e.target, errors: errors)
       return false
 
+    @processDestinationGroupEmailAddressParams(params)
+
     # disable form
     @formDisable(e)
 
@@ -413,6 +432,8 @@ class AppConfig extends App.ControllerModal
   button: 'Connect'
   buttonCancel: true
   small: true
+  events:
+    'click .js-copy': 'copyToClipboard'
 
   content: ->
     @external_credential = App.ExternalCredential.findByAttribute('name', 'google')
@@ -425,6 +446,15 @@ class AppConfig extends App.ControllerModal
     )
     content
 
+  copyToClipboard: (e) =>
+    e.preventDefault()
+
+    button = $(e.target).parents('[role="button"]')
+    field_name = button.data('targetField')
+    value = $(@container).find("input[name='#{jQuery.escapeSelector(field_name)}']").val()
+
+    @copyToClipboardWithTooltip(value, e.target,'.modal-body', true)
+
   onClosed: =>
     return if !@isChanged
     @isChanged = false

+ 35 - 5
app/assets/javascripts/app/controllers/_channel/microsoft365.coffee

@@ -291,6 +291,8 @@ class ChannelAccountOverview extends App.ControllerSubContent
     )
 
 class ChannelInboundEdit extends App.ControllerModal
+  @include App.DestinationGroupEmailAddressesMixin
+
   buttonClose: true
   buttonCancel: true
   buttonSubmit: true
@@ -298,14 +300,20 @@ class ChannelInboundEdit extends App.ControllerModal
 
   content: =>
     configureAttributesBase = [
-      { name: 'options::folder',          display: __('Folder'),   tag: 'input',  type: 'text', limit: 120, null: true, autocapitalize: false },
-      { name: 'options::keep_on_server',  display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false },
+      { name: 'group_id',                display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
+      { name: 'group_email_address_id',  display: __('Destination Group Email Address'), tag: 'select', null: false, options: @emailAddressOptions(@item.id, @item.group_id) },
+      { name: 'options::folder',         display: __('Folder'),   tag: 'input',  type: 'text', limit: 120, null: true, autocapitalize: false },
+      { name: 'options::keep_on_server', display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false },
     ]
     @form = new App.ControllerForm(
       model:
         configure_attributes: configureAttributesBase
         className: ''
-      params: @item.options.inbound
+      params: _.extend(
+        @item.options.inbound
+        group_id: @item.group_id
+      )
+      handlers: [@destinationGroupEmailAddressFormHandler(@item)]
     )
     @form.form
 
@@ -324,6 +332,9 @@ class ChannelInboundEdit extends App.ControllerModal
       @formValidate(form: e.target, errors: errors)
       return false
 
+    data =
+      options: params.options
+
     # disable form
     @formDisable(e)
 
@@ -332,7 +343,7 @@ class ChannelInboundEdit extends App.ControllerModal
       id:   'channel_email_inbound'
       type: 'POST'
       url:  "#{@apiPath}/channels_microsoft365_inbound/#{@item.id}"
-      data: JSON.stringify(params)
+      data: JSON.stringify(data)
       processData: true
       success: (data, status, xhr) =>
         if data.content_messages or not @set_active
@@ -362,6 +373,8 @@ class ChannelInboundEdit extends App.ControllerModal
     if @set_active
       params['active'] = true
 
+    @processDestinationGroupEmailAddressParams(params)
+
     # update
     @ajax(
       id:   'channel_email_verify'
@@ -384,6 +397,8 @@ class ChannelInboundEdit extends App.ControllerModal
     @navigate '#channels/microsoft365'
 
 class ChannelGroupEdit extends App.ControllerModal
+  @include App.DestinationGroupEmailAddressesMixin
+
   buttonClose: true
   buttonCancel: true
   buttonSubmit: true
@@ -391,13 +406,15 @@ class ChannelGroupEdit extends App.ControllerModal
 
   content: =>
     configureAttributesBase = [
-      { name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', nulloption: true, filter: { active: true } },
+      { name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
+      { name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', options: @emailAddressOptions(@item.id, @item.group_id) },
     ]
     @form = new App.ControllerForm(
       model:
         configure_attributes: configureAttributesBase
         className: ''
       params: @item
+      handlers: [@destinationGroupEmailAddressFormHandler(@item)]
     )
     @form.form
 
@@ -415,6 +432,8 @@ class ChannelGroupEdit extends App.ControllerModal
       @formValidate(form: e.target, errors: errors)
       return false
 
+    @processDestinationGroupEmailAddressParams(params)
+
     # disable form
     @formDisable(e)
 
@@ -440,6 +459,8 @@ class AppConfig extends App.ControllerModal
   button: 'Connect'
   buttonCancel: true
   small: true
+  events:
+    'click .js-copy': 'copyToClipboard'
 
   content: ->
     @external_credential = App.ExternalCredential.findByAttribute('name', 'microsoft365')
@@ -452,6 +473,15 @@ class AppConfig extends App.ControllerModal
     )
     content
 
+  copyToClipboard: (e) =>
+    e.preventDefault()
+
+    button = $(e.target).parents('[role="button"]')
+    field_name = button.data('targetField')
+    value = $(@container).find("input[name='#{jQuery.escapeSelector(field_name)}']").val()
+
+    @copyToClipboardWithTooltip(value, e.target,'.modal-body', true)
+
   onClosed: =>
     return if !@isChanged
     @isChanged = false

+ 19 - 4
app/assets/javascripts/app/controllers/_channel/microsoft_graph.coffee

@@ -258,6 +258,8 @@ class ChannelAccountOverview extends App.ControllerSubContent
     )
 
 class ChannelGroupEdit extends App.ControllerModal
+  @include App.DestinationGroupEmailAddressesMixin
+
   buttonClose: true
   buttonCancel: true
   buttonSubmit: true
@@ -266,12 +268,14 @@ class ChannelGroupEdit extends App.ControllerModal
   content: =>
     configureAttributesBase = [
       { name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
+      { name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', options: @emailAddressOptions(@item.id, @item.group_id) },
     ]
     @form = new App.ControllerForm(
       model:
         configure_attributes: configureAttributesBase
         className: ''
       params: @item
+      handlers: [@destinationGroupEmailAddressFormHandler(@item)]
     )
     @form.form
 
@@ -289,6 +293,8 @@ class ChannelGroupEdit extends App.ControllerModal
       @formValidate(form: e.target, errors: errors)
       return false
 
+    @processDestinationGroupEmailAddressParams(params)
+
     # disable form
     @formDisable(e)
 
@@ -411,6 +417,8 @@ class ChannelInboundNew extends App.ControllerModal
     window.location.href = "#{@apiPath}/external_credentials/microsoft_graph/link_account#{query_string}"
 
 class ChannelInboundEdit extends App.ControllerModal
+  @include App.DestinationGroupEmailAddressesMixin
+
   buttonClose: true
   buttonCancel: true
   buttonSubmit: __('Save')
@@ -469,9 +477,10 @@ class ChannelInboundEdit extends App.ControllerModal
       return App.view('microsoft_graph/error_message')(error: @error)
 
     configureAttributesBase = [
-      { name: 'group_id',                 display: __('Destination Group'),       tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
-      { name: 'options::folder_id',       display: __('Folder'),                  tag: 'tree_select', null: true, options: @folderOptions, nulloption: true, default: '', help: __('Specify which folder to fetch from, or leave empty to fetch from ||inbox||.') },
-      { name: 'options::keep_on_server',  display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false },
+      { name: 'group_id',                display: __('Destination Group'),       tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
+      { name: 'group_email_address_id',  display: __('Destination Group Email Address'), tag: 'select', null: false, options: @emailAddressOptions(@item.id, @item.group_id) },
+      { name: 'options::folder_id',      display: __('Folder'),                  tag: 'tree_select', null: true, options: @folderOptions, nulloption: true, default: '', help: __('Specify which folder to fetch from, or leave empty to fetch from ||inbox||.') },
+      { name: 'options::keep_on_server', display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false },
     ]
     @form = new App.ControllerForm(
       model:
@@ -482,6 +491,7 @@ class ChannelInboundEdit extends App.ControllerModal
         options:
           folder_id: @item.options.inbound.options.folder_id,
           keep_on_server: @item.options.inbound.options.keep_on_server,
+      handlers: [@destinationGroupEmailAddressFormHandler(@item)]
     )
     @form.form
 
@@ -498,6 +508,9 @@ class ChannelInboundEdit extends App.ControllerModal
       @formValidate(form: e.target, errors: errors)
       return false
 
+    data =
+      options: params.options
+
     # disable form
     @formDisable(e)
 
@@ -508,7 +521,7 @@ class ChannelInboundEdit extends App.ControllerModal
       id:   'channel_email_inbound'
       type: 'POST'
       url:  "#{@apiPath}/channels/admin/microsoft_graph/inbound/#{@item.id}"
-      data: JSON.stringify(params)
+      data: JSON.stringify(data)
       processData: true
       success: (data, status, xhr) =>
         if data.content_messages or not @set_active
@@ -538,6 +551,8 @@ class ChannelInboundEdit extends App.ControllerModal
     if @set_active
       params['active'] = true
 
+    @processDestinationGroupEmailAddressParams(params)
+
     # update
     @ajax(
       id:   'channel_email_verify'

+ 44 - 0
app/assets/javascripts/app/lib/mixins/destination_group_email_address.coffee

@@ -0,0 +1,44 @@
+# Common event handlers for destination group email address field.
+App.DestinationGroupEmailAddressesMixin =
+  destinationGroupEmailAddressFormHandler: (item) ->
+    formHandler = (params, attribute, attributes, classname, form, ui) =>
+      return if not item?.id or attribute.name isnt 'group_id'
+
+      return if ui.FormHandlerGroupEmailAddressDone
+      ui.FormHandlerGroupEmailAddressDone = true
+      $(form).find('[name=group_id]').off('change.group_id').on('change.group_id', (e) =>
+        group_id = $(e.target).val()
+        for attr in attributes
+          continue if attr.name isnt 'group_email_address_id'
+          attr.options = @emailAddressOptions(item.id, group_id)
+          newElement = ui.formGenItem(attr, classname, form)
+          form.find('div.form-group[data-attribute-name="' + attr.name + '"]').replaceWith(newElement)
+      )
+
+    formHandler
+
+  emailAddressOptions: (id, group_id) ->
+    if !id
+      return {
+        false: App.i18n.translatePlain('Do not change email address')
+        true: App.i18n.translatePlain('Change to channel email address')
+      }
+
+    group = App.Group.find(group_id)
+    emailAddresses = App.EmailAddress.findAllByAttribute('channel_id', id)
+
+    _.reduce(
+      emailAddresses,
+      (acc, emailAddress) ->
+        return acc if emailAddress.id is group?.email_address_id
+        acc[emailAddress.id] = App.i18n.translatePlain('Change to %s', emailAddress.email)
+        acc
+      { false: App.i18n.translatePlain('Do not change email address') }
+    )
+
+  processDestinationGroupEmailAddressParams: (params) ->
+    return if _.isUndefined(params.group_email_address_id)
+    params.group_email_address = params.group_email_address_id isnt 'false'
+
+    return if params.group_email_address and params.group_email_address_id isnt 'true'
+    delete params.group_email_address_id

+ 1 - 1
app/assets/javascripts/app/views/channel/email_account_wizard.jst.eco

@@ -15,7 +15,7 @@
     </div>
     <div class="modal-footer">
         <div class="modal-leftFooter">
-          <button class="btn btn--text btn--secondary align-left js-expert"><%- @T('Experts') %></button>
+          <button class="btn btn--text btn--secondary align-left js-expert" type="button"><%- @T('Experts') %></button>
         </div>
         <div class="modal-rightFooter">
           <button class="btn btn--primary align-right js-submit"><%- @T('Connect') %></button>

+ 8 - 3
app/assets/javascripts/app/views/google/app_config.jst.eco

@@ -17,13 +17,18 @@
       <label for="client_secret"><%- @T('Google Client Secret') %> <span>*</span></label>
     </div>
     <div class="controls">
-      <input id="client_secret" type="text" name="client_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.client_secret %><% end %>" class="form-control" required autocomplete="off" >
+      <input id="client_secret" type="password" name="client_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.client_secret %><% end %>" class="form-control" required autocomplete="off" >
     </div>
   </div>
   <h2><%- @T('Your callback URL') %></h2>
   <div class="input form-group">
-    <div class="controls">
-      <input class="form-control js-select" readonly value="<%= @callbackUrl %>" name="callback_url">
+    <div class="controls controls--button ignore-readonly">
+      <input readonly id="callback_url" name="callback_url" type="text" value="<%= @callbackUrl %>" class="form-control">
+      <div class="controls-button u-clickable js-copy" role="button" data-target-field="callback_url" aria-label="<%- @T('Copy to clipboard') %>">
+        <span class="controls-button-inner">
+          <%- @Icon('clipboard') %>
+        </span>
+      </div>
     </div>
   </div>
 </fieldset>

+ 8 - 3
app/assets/javascripts/app/views/microsoft365/app_config.jst.eco

@@ -17,7 +17,7 @@
       <label for="client_secret"><%- @T('Client Secret') %> <span>*</span></label>
     </div>
     <div class="controls">
-      <input id="client_secret" type="text" name="client_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.client_secret %><% end %>" class="form-control" required autocomplete="off" >
+      <input id="client_secret" type="password" name="client_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.client_secret %><% end %>" class="form-control" required autocomplete="off" >
     </div>
   </div>
   <div class="input form-group">
@@ -30,8 +30,13 @@
   </div>
   <h2><%- @T('Your callback URL') %></h2>
   <div class="input form-group">
-    <div class="controls">
-      <input class="form-control js-select" readonly value="<%= @callbackUrl %>" name="callback_url">
+    <div class="controls controls--button ignore-readonly">
+      <input readonly id="callback_url" name="callback_url" type="text" value="<%= @callbackUrl %>" class="form-control">
+      <div class="controls-button u-clickable js-copy" role="button" data-target-field="callback_url" aria-label="<%- @T('Copy to clipboard') %>">
+        <span class="controls-button-inner">
+          <%- @Icon('clipboard') %>
+        </span>
+      </div>
     </div>
   </div>
 </fieldset>

+ 1 - 1
app/assets/javascripts/app/views/microsoft_graph/app_config.jst.eco

@@ -31,7 +31,7 @@
   <h2><%- @T('Your callback URL') %></h2>
   <div class="input form-group">
     <div class="controls controls--button ignore-readonly">
-      <input readonly id="callback_url" name="callback_url" name="callback_url" type="text" value="<%= @callbackUrl %>" class="form-control">
+      <input readonly id="callback_url" name="callback_url" type="text" value="<%= @callbackUrl %>" class="form-control">
       <div class="controls-button u-clickable js-copy" role="button" data-target-field="callback_url" aria-label="<%- @T('Copy to clipboard') %>">
         <span class="controls-button-inner">
           <%- @Icon('clipboard') %>

+ 4 - 95
app/controllers/channels_admin/microsoft_graph_controller.rb

@@ -1,74 +1,14 @@
 # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
 
 class ChannelsAdmin::MicrosoftGraphController < ChannelsAdmin::BaseController
+  include CanXoauth2EmailChannel
+
   def area
     'MicrosoftGraph::Account'.freeze
   end
 
-  def index
-    system_online_service = Setting.get('system_online_service')
-
-    assets = {}
-    external_credential_ids = []
-    ExternalCredential.where(name: 'microsoft_graph').each do |external_credential|
-      assets = external_credential.assets(assets)
-      external_credential_ids.push external_credential.id
-    end
-
-    channels = Service::Channel::Admin::List.new(area: area).execute
-    channel_ids = []
-    channels.each do |channel|
-      assets = channel.assets(assets)
-      channel_ids.push channel.id
-    end
-
-    not_used_email_address_ids = []
-    EmailAddress.find_each do |email_address|
-      next if system_online_service && email_address.preferences && email_address.preferences['online_service_disable']
-
-      assets = email_address.assets(assets)
-      if !email_address.channel_id || !email_address.active || !Channel.exists?(email_address.channel_id)
-        not_used_email_address_ids.push email_address.id
-      end
-    end
-
-    render json: {
-      assets:                     assets,
-      not_used_email_address_ids: not_used_email_address_ids,
-      channel_ids:                channel_ids,
-      external_credential_ids:    external_credential_ids,
-      callback_url:               ExternalCredential.callback_url('microsoft_graph'),
-    }
-  end
-
-  def inbound
-    channel = Channel.find_by(id: params[:id], area:)
-
-    channel.refresh_xoauth2!
-
-    inbound_prepare_channel(channel, params)
-
-    result = EmailHelper::Probe.inbound(channel.options[:inbound])
-    raise Exceptions::UnprocessableEntity, (result[:message_human] || result[:message]) if result[:result] == 'invalid'
-
-    render json: result
-  end
-
-  def verify
-    channel = Channel.find_by(id: params[:id], area:)
-
-    verify_prepare_channel(channel, params)
-
-    channel.save!
-
-    render json: {}
-  end
-
-  def group
-    channel = Channel.find_by(id: params[:id], area:)
-    channel.group_id = params[:group_id]
-    channel.save!
-    render json: {}
+  def external_credential_name
+    'microsoft_graph'.freeze
   end
 
   def folders
@@ -93,35 +33,4 @@ class ChannelsAdmin::MicrosoftGraphController < ChannelsAdmin::BaseController
 
     render json: { folders:, error: }
   end
-
-  private
-
-  def inbound_prepare_channel(channel, params)
-    channel.group_id = params[:group_id] if params[:group_id].present?
-    channel.active   = params[:active] if params.key?(:active)
-
-    channel.options[:inbound] ||= {}
-    channel.options[:inbound][:options] ||= {}
-
-    %w[folder_id keep_on_server].each do |key|
-      next if params.dig(:options, key).nil?
-
-      channel.options[:inbound][:options][key] = params[:options][key]
-    end
-  end
-
-  def verify_prepare_channel(channel, params)
-    inbound_prepare_channel(channel, params)
-
-    %w[archive archive_before archive_state_id].each do |key|
-      next if params.dig(:options, key).nil?
-
-      channel.options[:inbound][:options][key] = params[:options][key]
-    end
-
-    channel.status_in    = 'ok'
-    channel.status_out   = 'ok'
-    channel.last_log_in  = nil
-    channel.last_log_out = nil
-  end
 end

Некоторые файлы не были показаны из-за большого количества измененных файлов