Fixes #629 - Draft Sharing.
This commit is contained in:
parent
92b7c39879
commit
c1d467aa3d
59 changed files with 2717 additions and 51 deletions
|
@ -132,6 +132,14 @@ class App.TicketCreate extends App.Controller
|
||||||
@$('[name="cc"], [name="group_id"], [name="customer_id"]').on('change', =>
|
@$('[name="cc"], [name="group_id"], [name="customer_id"]').on('change', =>
|
||||||
@updateSecurityOptions()
|
@updateSecurityOptions()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@listenTo(App.Group, 'refresh', =>
|
||||||
|
@sidebarWidget.render(@params())
|
||||||
|
)
|
||||||
|
|
||||||
|
@$('[name="group_id"]').bind('change', =>
|
||||||
|
@sidebarWidget.render(@params())
|
||||||
|
)
|
||||||
@updateSecurityOptions()
|
@updateSecurityOptions()
|
||||||
|
|
||||||
# show cc
|
# show cc
|
||||||
|
@ -174,6 +182,7 @@ class App.TicketCreate extends App.Controller
|
||||||
@navupdate("#ticket/create/id/#{@id}#{@split}", type: 'menu')
|
@navupdate("#ticket/create/id/#{@id}#{@split}", type: 'menu')
|
||||||
@autosaveStart()
|
@autosaveStart()
|
||||||
@controllerBind('ticket_create_rerender', (template) => @renderQueue(template))
|
@controllerBind('ticket_create_rerender', (template) => @renderQueue(template))
|
||||||
|
@controllerBind('ticket_create_import_draft_attachments', @importDraftAttachments)
|
||||||
|
|
||||||
# initially hide sidebar on mobile
|
# initially hide sidebar on mobile
|
||||||
if window.matchMedia('(max-width: 767px)').matches
|
if window.matchMedia('(max-width: 767px)').matches
|
||||||
|
@ -183,6 +192,7 @@ class App.TicketCreate extends App.Controller
|
||||||
hide: =>
|
hide: =>
|
||||||
@autosaveStop()
|
@autosaveStop()
|
||||||
@controllerUnbind('ticket_create_rerender', (template) => @renderQueue(template))
|
@controllerUnbind('ticket_create_rerender', (template) => @renderQueue(template))
|
||||||
|
@controllerUnbind('ticket_create_import_draft_attachments')
|
||||||
|
|
||||||
changed: =>
|
changed: =>
|
||||||
return true if @hasAttachments()
|
return true if @hasAttachments()
|
||||||
|
@ -283,6 +293,18 @@ class App.TicketCreate extends App.Controller
|
||||||
return if !@formMeta
|
return if !@formMeta
|
||||||
App.QueueManager.run(@queueKey)
|
App.QueueManager.run(@queueKey)
|
||||||
|
|
||||||
|
importDraftAttachments: (options = {}) =>
|
||||||
|
@ajax
|
||||||
|
id: 'import_attachments'
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/tickets/shared_drafts/#{options.shared_draft_id}/import_attachments"
|
||||||
|
data: JSON.stringify({ form_id: @formId })
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) ->
|
||||||
|
App.Event.trigger(options.callbackName, { success: true, attachments: data.attachments })
|
||||||
|
error: ->
|
||||||
|
App.Event.trigger(options.callbackName, { success: false })
|
||||||
|
|
||||||
updateTaskManagerAttachments: (attribute, attachments) =>
|
updateTaskManagerAttachments: (attribute, attachments) =>
|
||||||
taskData = App.TaskManager.get(@taskKey)
|
taskData = App.TaskManager.get(@taskKey)
|
||||||
return if _.isEmpty(taskData)
|
return if _.isEmpty(taskData)
|
||||||
|
@ -306,12 +328,13 @@ class App.TicketCreate extends App.Controller
|
||||||
params.priority_id ||= App.TicketPriority.findByAttribute( 'default_create', true )?.id
|
params.priority_id ||= App.TicketPriority.findByAttribute( 'default_create', true )?.id
|
||||||
|
|
||||||
@html(App.view('agent_ticket_create')(
|
@html(App.view('agent_ticket_create')(
|
||||||
head: __('New Ticket')
|
head: __('New Ticket')
|
||||||
agent: @permissionCheck('ticket.agent')
|
agent: @permissionCheck('ticket.agent')
|
||||||
admin: @permissionCheck('admin')
|
admin: @permissionCheck('admin')
|
||||||
types: @types,
|
types: @types,
|
||||||
availableTypes: @availableTypes
|
availableTypes: @availableTypes
|
||||||
form_id: @formId
|
form_id: @formId
|
||||||
|
shared_draft_id: template.shared_draft_id || params.shared_draft_id
|
||||||
))
|
))
|
||||||
|
|
||||||
App.Ticket.configure_attributes.push {
|
App.Ticket.configure_attributes.push {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
class SidebarSharedDraft extends App.Controller
|
||||||
|
sidebarItem: =>
|
||||||
|
return if !@permissionCheck('ticket.agent')
|
||||||
|
|
||||||
|
group = App.Group.find @params.group_id
|
||||||
|
|
||||||
|
return if !group?.shared_drafts
|
||||||
|
|
||||||
|
@item = {
|
||||||
|
name: 'shared_draft'
|
||||||
|
badgeIcon: 'note'
|
||||||
|
sidebarHead: __('Shared Drafts')
|
||||||
|
sidebarActions: []
|
||||||
|
sidebarCallback: @showDrafts
|
||||||
|
}
|
||||||
|
@item
|
||||||
|
|
||||||
|
showDrafts: (el) =>
|
||||||
|
@el = el
|
||||||
|
|
||||||
|
# show template UI
|
||||||
|
new App.WidgetSharedDraft(
|
||||||
|
el: el
|
||||||
|
taskKey: @taskKey
|
||||||
|
group_id: @params.group_id
|
||||||
|
active_draft_id: @params.shared_draft_id
|
||||||
|
)
|
||||||
|
|
||||||
|
App.Config.set('400-SharedDraft', SidebarSharedDraft, 'TicketCreateSidebar')
|
|
@ -2,14 +2,15 @@ class App.TicketZoom extends App.Controller
|
||||||
@include App.TicketNavigable
|
@include App.TicketNavigable
|
||||||
|
|
||||||
elements:
|
elements:
|
||||||
'.main': 'main'
|
'.main': 'main'
|
||||||
'.ticketZoom': 'ticketZoom'
|
'.ticketZoom': 'ticketZoom'
|
||||||
'.scrollPageHeader': 'scrollPageHeader'
|
'.scrollPageHeader': 'scrollPageHeader'
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click .js-submit': 'submit'
|
'click .js-submit': 'submit'
|
||||||
'click .js-bookmark': 'bookmark'
|
'click .js-bookmark': 'bookmark'
|
||||||
'click .js-reset': 'reset'
|
'click .js-reset': 'reset'
|
||||||
|
'click .js-draft': 'draft'
|
||||||
'click .main': 'muteTask'
|
'click .main': 'muteTask'
|
||||||
|
|
||||||
constructor: (params) ->
|
constructor: (params) ->
|
||||||
|
@ -187,6 +188,9 @@ class App.TicketZoom extends App.Controller
|
||||||
# remember mentions
|
# remember mentions
|
||||||
@mentions = data.mentions
|
@mentions = data.mentions
|
||||||
|
|
||||||
|
if draft = App.TicketSharedDraftZoom.findByAttribute 'ticket_id', @ticket_id
|
||||||
|
draft.remove(clear: true)
|
||||||
|
|
||||||
App.Collection.loadAssets(data.assets, targetModel: 'Ticket')
|
App.Collection.loadAssets(data.assets, targetModel: 'Ticket')
|
||||||
|
|
||||||
# get ticket
|
# get ticket
|
||||||
|
@ -481,11 +485,13 @@ class App.TicketZoom extends App.Controller
|
||||||
)
|
)
|
||||||
|
|
||||||
@attributeBar = new App.TicketZoomAttributeBar(
|
@attributeBar = new App.TicketZoomAttributeBar(
|
||||||
ticket: @ticket
|
ticket: @ticket
|
||||||
el: elLocal.find('.js-attributeBar')
|
el: elLocal.find('.js-attributeBar')
|
||||||
overview_id: @overview_id
|
overview_id: @overview_id
|
||||||
callback: @submit
|
macroCallback: @submit
|
||||||
taskKey: @taskKey
|
draftCallback: @saveDraft
|
||||||
|
draftState: @draftState()
|
||||||
|
taskKey: @taskKey
|
||||||
)
|
)
|
||||||
#if @shown
|
#if @shown
|
||||||
# @attributeBar.start()
|
# @attributeBar.start()
|
||||||
|
@ -965,6 +971,55 @@ class App.TicketZoom extends App.Controller
|
||||||
@submitPost(e, ticket, macro)
|
@submitPost(e, ticket, macro)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
saveDraft: (e) =>
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
params =
|
||||||
|
new_article: @articleNew.params()
|
||||||
|
ticket_attributes: @ticketParams()
|
||||||
|
|
||||||
|
loaded_draft_id = params.new_article.shared_draft_id
|
||||||
|
|
||||||
|
params.form_id = params.new_article['form_id']
|
||||||
|
delete params.new_article['form_id']
|
||||||
|
delete params.new_article['shared_draft_id']
|
||||||
|
|
||||||
|
sharedDraft = @sharedDraft()
|
||||||
|
|
||||||
|
draftExists = sharedDraft?
|
||||||
|
isLoaded = loaded_draft_id == String(sharedDraft?.id)
|
||||||
|
|
||||||
|
matches = draftExists &&
|
||||||
|
_.isEqual(sharedDraft.new_article, params.new_article) &&
|
||||||
|
_.isEqual(sharedDraft.ticket_attributes, params.ticket_attributes)
|
||||||
|
|
||||||
|
if draftExists && !(isLoaded && matches)
|
||||||
|
new App.TicketSharedDraftOverwriteModal(
|
||||||
|
onShowDraft: @draft
|
||||||
|
onSaveDraft: =>
|
||||||
|
@draftSaveToServer(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
@draftSaveToServer(params)
|
||||||
|
|
||||||
|
draftSaveToServer: (params) =>
|
||||||
|
@draftSaving()
|
||||||
|
|
||||||
|
@ajax
|
||||||
|
id: 'ticket_shared_draft_update'
|
||||||
|
type: 'PUT'
|
||||||
|
url: @apiPath + '/tickets/' + @ticket_id + '/shared_draft'
|
||||||
|
processData: true
|
||||||
|
data: JSON.stringify(params)
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
@draftFetched()
|
||||||
|
error: =>
|
||||||
|
@draftFetched()
|
||||||
|
|
||||||
submitPost: (e, ticket, macro) =>
|
submitPost: (e, ticket, macro) =>
|
||||||
taskAction = @$('.js-secondaryActionButtonLabel').data('type')
|
taskAction = @$('.js-secondaryActionButtonLabel').data('type')
|
||||||
|
|
||||||
|
@ -1034,6 +1089,49 @@ class App.TicketZoom extends App.Controller
|
||||||
bookmark: (e) ->
|
bookmark: (e) ->
|
||||||
$(e.currentTarget).find('.bookmark.icon').toggleClass('filled')
|
$(e.currentTarget).find('.bookmark.icon').toggleClass('filled')
|
||||||
|
|
||||||
|
draft: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
new App.TicketSharedDraftModal(
|
||||||
|
container: @el.closest('.content')
|
||||||
|
hasChanges: App.TaskManager.worker(@taskKey).changed()
|
||||||
|
parent: @
|
||||||
|
shared_draft: @sharedDraft()
|
||||||
|
)
|
||||||
|
|
||||||
|
fetchDraft: ->
|
||||||
|
@ajax(
|
||||||
|
id: "ticket_#{@ticket_id}_shared_draft"
|
||||||
|
type: 'GET'
|
||||||
|
url: "#{@apiPath}/tickets/#{@ticket_id}/shared_draft"
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
@draftFetched()
|
||||||
|
)
|
||||||
|
|
||||||
|
draftSaving: ->
|
||||||
|
@updateDraftButton(true, 'saving')
|
||||||
|
|
||||||
|
updateDraftButton: (visible, state) ->
|
||||||
|
button = @el.find('.js-draft')
|
||||||
|
|
||||||
|
button.toggleClass('hide', !visible)
|
||||||
|
button.find('.attributeBar-draft--available').toggleClass('hide', state != 'available')
|
||||||
|
button.find('.attributeBar-draft--saving').toggleClass('hide', state != 'saving')
|
||||||
|
button.attr('disabled', state == 'saving')
|
||||||
|
|
||||||
|
@el.find('.js-dropdownActionSaveDraft').attr('disabled', state == 'saving')
|
||||||
|
|
||||||
|
draftFetched: ->
|
||||||
|
@updateDraftButton(@sharedDraft()?, 'available')
|
||||||
|
|
||||||
|
draftState: ->
|
||||||
|
@sharedDraft()?
|
||||||
|
|
||||||
|
sharedDraft: ->
|
||||||
|
App.TicketSharedDraftZoom.findByAttribute 'ticket_id', @ticket_id
|
||||||
|
|
||||||
reset: (e) =>
|
reset: (e) =>
|
||||||
if e
|
if e
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
|
@ -53,13 +53,16 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||||
|
|
||||||
@setArticleTypePre(data.type.name, data.signaturePosition)
|
@setArticleTypePre(data.type.name, data.signaturePosition)
|
||||||
|
|
||||||
@openTextarea(null, true, true)
|
@openTextarea(null, true, !data.nofocus)
|
||||||
for key, value of data.article
|
for key, value of data.article
|
||||||
if key is 'body'
|
if key is 'body'
|
||||||
@$("[data-name=\"#{key}\"]").html(value)
|
@$("[data-name=\"#{key}\"]").html(value)
|
||||||
else
|
else
|
||||||
@$("[name=\"#{key}\"]").val(value).trigger('change')
|
@$("[name=\"#{key}\"]").val(value).trigger('change')
|
||||||
|
|
||||||
|
|
||||||
|
@$('[name=shared_draft_id]').val(data.shared_draft_id)
|
||||||
|
|
||||||
@setArticleTypePost(data.type.name, data.signaturePosition)
|
@setArticleTypePost(data.type.name, data.signaturePosition)
|
||||||
|
|
||||||
# set focus into field
|
# set focus into field
|
||||||
|
@ -76,6 +79,8 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||||
@tokanice(data.type.name)
|
@tokanice(data.type.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@controllerBind('ui::ticket::import_draft_attachments', @importDraftAttachments)
|
||||||
|
|
||||||
# add article attachment
|
# add article attachment
|
||||||
@controllerBind('ui::ticket::addArticleAttachent', (data) =>
|
@controllerBind('ui::ticket::addArticleAttachent', (data) =>
|
||||||
return if data.ticket?.id?.toString() isnt @ticket_id.toString() && data.form_id isnt @form_id
|
return if data.ticket?.id?.toString() isnt @ticket_id.toString() && data.form_id isnt @form_id
|
||||||
|
@ -634,6 +639,26 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||||
@richTextUploadDeleteCallback?(@attachments)
|
@richTextUploadDeleteCallback?(@attachments)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
importDraftAttachments: (options) =>
|
||||||
|
return if @ticket.id != options.ticket_id
|
||||||
|
|
||||||
|
@ajax
|
||||||
|
id: 'import_attachments'
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/tickets/#{@ticket.id}/shared_draft/import_attachments"
|
||||||
|
data: JSON.stringify({ form_id: @form_id })
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Event.trigger('ui::ticket::addArticleAttachent', {
|
||||||
|
ticket: @ticket
|
||||||
|
attachments: data.attachments
|
||||||
|
form_id: @form_id
|
||||||
|
})
|
||||||
|
|
||||||
|
App.Event.trigger(options.callbackName, { success: true })
|
||||||
|
error: ->
|
||||||
|
App.Event.trigger(options.callbackName, { success: false })
|
||||||
|
|
||||||
actions: ->
|
actions: ->
|
||||||
actionConfig = App.Config.get('TicketZoomArticleAction')
|
actionConfig = App.Config.get('TicketZoomArticleAction')
|
||||||
keys = _.keys(actionConfig).sort()
|
keys = _.keys(actionConfig).sort()
|
||||||
|
|
|
@ -9,6 +9,9 @@ class App.TicketZoomAttributeBar extends App.Controller
|
||||||
'mouseup .js-dropdownActionMacro': 'performTicketMacro'
|
'mouseup .js-dropdownActionMacro': 'performTicketMacro'
|
||||||
'mouseenter .js-dropdownActionMacro': 'onActionMacroMouseEnter'
|
'mouseenter .js-dropdownActionMacro': 'onActionMacroMouseEnter'
|
||||||
'mouseleave .js-dropdownActionMacro': 'onActionMacroMouseLeave'
|
'mouseleave .js-dropdownActionMacro': 'onActionMacroMouseLeave'
|
||||||
|
'mouseup .js-dropdownActionSaveDraft': 'saveDraft'
|
||||||
|
'mouseenter .js-dropdownActionSaveDraft': 'onActionMacroMouseEnter'
|
||||||
|
'mouseleave .js-dropdownActionSaveDraft': 'onActionMacroMouseLeave'
|
||||||
'click .js-secondaryAction': 'chooseSecondaryAction'
|
'click .js-secondaryAction': 'chooseSecondaryAction'
|
||||||
|
|
||||||
searchCondition: {}
|
searchCondition: {}
|
||||||
|
@ -31,19 +34,45 @@ class App.TicketZoomAttributeBar extends App.Controller
|
||||||
@render()
|
@render()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@controllerBind('ui::ticket::updateSharedDraft', (data) =>
|
||||||
|
return if data.taskKey isnt @taskKey
|
||||||
|
@render(data)
|
||||||
|
)
|
||||||
|
|
||||||
|
@listenTo(App.Group, 'refresh', (refreshed_group) =>
|
||||||
|
selected_group_id = @el.closest('.content').find('[name=group_id]').val()
|
||||||
|
selected_group = App.Group.find selected_group_id
|
||||||
|
|
||||||
|
return if !selected_group
|
||||||
|
return if !refreshed_group
|
||||||
|
return if refreshed_group.id != selected_group.id
|
||||||
|
|
||||||
|
return if @sharedDraftsEnabled == selected_group.shared_drafts
|
||||||
|
|
||||||
|
@render({ newGroupId: selected_group.id })
|
||||||
|
)
|
||||||
|
|
||||||
getAction: ->
|
getAction: ->
|
||||||
return App.Session.get().preferences.secondaryAction || App.Config.get('ticket_secondary_action') || 'stayOnTab'
|
return App.Session.get().preferences.secondaryAction || App.Config.get('ticket_secondary_action') || 'stayOnTab'
|
||||||
|
|
||||||
release: =>
|
release: =>
|
||||||
App.Macro.unsubscribe(@subscribeId)
|
App.Macro.unsubscribe(@subscribeId)
|
||||||
|
|
||||||
render: =>
|
render: (options = {}) =>
|
||||||
|
|
||||||
# remember current reset state
|
# remember current reset state
|
||||||
resetButtonShown = false
|
resetButtonShown = false
|
||||||
if @resetButton.get(0) && !@resetButton.hasClass('hide')
|
if @resetButton.get(0) && !@resetButton.hasClass('hide')
|
||||||
resetButtonShown = true
|
resetButtonShown = true
|
||||||
|
|
||||||
|
group = App.Group.find options?.newGroupId || @ticket.group_id
|
||||||
|
draft = App.TicketSharedDraftZoom.findByAttribute 'ticket_id', @ticket.id
|
||||||
|
accessibleGroups = App.User.current().allGroupIds('change')
|
||||||
|
sharedDraftButtonShown = group?.shared_drafts && _.contains(accessibleGroups, String(group.id))
|
||||||
|
sharedDraftsEnabled = group?.shared_drafts && _.contains(accessibleGroups, String(group.id))
|
||||||
|
sharedButtonVisible = sharedDraftsEnabled && draft?
|
||||||
|
|
||||||
|
@sharedDraftsEnabled = sharedDraftsEnabled
|
||||||
|
|
||||||
macros = App.Macro.getList()
|
macros = App.Macro.getList()
|
||||||
|
|
||||||
@macroLastUpdated = App.Macro.lastUpdatedAt()
|
@macroLastUpdated = App.Macro.lastUpdatedAt()
|
||||||
|
@ -59,11 +88,15 @@ class App.TicketZoomAttributeBar extends App.Controller
|
||||||
@possibleMacros.push macro
|
@possibleMacros.push macro
|
||||||
|
|
||||||
localeEl = $(App.view('ticket_zoom/attribute_bar')(
|
localeEl = $(App.view('ticket_zoom/attribute_bar')(
|
||||||
macros: @possibleMacros
|
macros: @possibleMacros
|
||||||
macroDisabled: macroDisabled
|
macroDisabled: macroDisabled
|
||||||
overview_id: @overview_id
|
sharedButtonVisible: sharedButtonVisible
|
||||||
resetButtonShown: resetButtonShown
|
sharedDraftsDisabled: !sharedDraftsEnabled
|
||||||
|
overview_id: @overview_id
|
||||||
|
resetButtonShown: resetButtonShown
|
||||||
|
sharedDraftButtonShown: sharedDraftButtonShown
|
||||||
))
|
))
|
||||||
|
|
||||||
@setSecondaryAction(@secondaryAction, localeEl)
|
@setSecondaryAction(@secondaryAction, localeEl)
|
||||||
|
|
||||||
if @ticket.currentView() is 'agent'
|
if @ticket.currentView() is 'agent'
|
||||||
|
@ -74,6 +107,26 @@ class App.TicketZoomAttributeBar extends App.Controller
|
||||||
|
|
||||||
@html localeEl
|
@html localeEl
|
||||||
|
|
||||||
|
@el.find('.js-draft').popover(
|
||||||
|
trigger: 'hover'
|
||||||
|
container: 'body'
|
||||||
|
html: true
|
||||||
|
animation: false
|
||||||
|
delay: 100
|
||||||
|
placement: 'auto'
|
||||||
|
sanitize: false
|
||||||
|
content: =>
|
||||||
|
draft = App.TicketSharedDraftZoom.findByAttribute 'ticket_id', @ticket?.id
|
||||||
|
timestamp = App.ViewHelpers.humanTime(draft?.updated_at)
|
||||||
|
user = App.User.find draft?.updated_by_id
|
||||||
|
name = user?.displayName()
|
||||||
|
|
||||||
|
content = App.i18n.translatePlain('Last change %s<br>by %s', timestamp, name)
|
||||||
|
|
||||||
|
# needs linebreak to align vertically without title
|
||||||
|
'<br>' + content
|
||||||
|
)
|
||||||
|
|
||||||
start: =>
|
start: =>
|
||||||
return if !@taskbarWatcher
|
return if !@taskbarWatcher
|
||||||
@taskbarWatcher.start()
|
@taskbarWatcher.start()
|
||||||
|
@ -106,9 +159,12 @@ class App.TicketZoomAttributeBar extends App.Controller
|
||||||
macroId = $(e.currentTarget).data('id')
|
macroId = $(e.currentTarget).data('id')
|
||||||
macro = App.Macro.find(macroId)
|
macro = App.Macro.find(macroId)
|
||||||
|
|
||||||
@callback(e, macro)
|
@macroCallback(e, macro)
|
||||||
@closeMacroMenu()
|
@closeMacroMenu()
|
||||||
|
|
||||||
|
saveDraft: (e) =>
|
||||||
|
@draftCallback(e)
|
||||||
|
|
||||||
onActionMacroMouseEnter: (e) =>
|
onActionMacroMouseEnter: (e) =>
|
||||||
@$(e.currentTarget).addClass('is-active')
|
@$(e.currentTarget).addClass('is-active')
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
class TicketZoomFormHandlerSharedDraft
|
||||||
|
|
||||||
|
# central method, is getting called on every ticket form change
|
||||||
|
# but only trigger event for group_id changes
|
||||||
|
@run: (params, attribute, attributes, classname, form, ui) ->
|
||||||
|
return if attribute.name isnt 'group_id'
|
||||||
|
App.Event.trigger('ui::ticket::updateSharedDraft', { taskKey: ui.taskKey, newGroupId: params.group_id })
|
||||||
|
|
||||||
|
App.Config.set('150-ticketFormSharedDraft', TicketZoomFormHandlerSharedDraft, 'TicketZoomFormHandler')
|
|
@ -9,12 +9,13 @@ class Edit extends App.Controller
|
||||||
return if data.ticket_id.toString() isnt @ticket.id.toString()
|
return if data.ticket_id.toString() isnt @ticket.id.toString()
|
||||||
|
|
||||||
@ticket = App.Ticket.find(@ticket.id)
|
@ticket = App.Ticket.find(@ticket.id)
|
||||||
@formMeta = data.form_meta
|
if data.form_meta
|
||||||
@render()
|
@formMeta = data.form_meta
|
||||||
|
@render(data.draft)
|
||||||
)
|
)
|
||||||
@render()
|
@render()
|
||||||
|
|
||||||
render: =>
|
render: (draft = {}) =>
|
||||||
defaults = @ticket.attributes()
|
defaults = @ticket.attributes()
|
||||||
delete defaults.article # ignore article infos
|
delete defaults.article # ignore article infos
|
||||||
followUpPossible = App.Group.find(defaults.group_id).follow_up_possible
|
followUpPossible = App.Group.find(defaults.group_id).follow_up_possible
|
||||||
|
@ -45,7 +46,7 @@ class Edit extends App.Controller
|
||||||
handlersConfig: handlers
|
handlersConfig: handlers
|
||||||
filter: @formMeta.filter
|
filter: @formMeta.filter
|
||||||
formMeta: @formMeta
|
formMeta: @formMeta
|
||||||
params: defaults
|
params: _.extend(defaults, draft)
|
||||||
isDisabled: editable
|
isDisabled: editable
|
||||||
taskKey: @taskKey
|
taskKey: @taskKey
|
||||||
core_workflow: {
|
core_workflow: {
|
||||||
|
|
|
@ -4,7 +4,7 @@ class App.TicketZoomTimeAccounting extends App.ControllerModal
|
||||||
buttonSubmit: __('Account Time')
|
buttonSubmit: __('Account Time')
|
||||||
buttonClass: 'btn--success'
|
buttonClass: 'btn--success'
|
||||||
leftButtons: [{
|
leftButtons: [{
|
||||||
className: 'btn--text btn--subtle js-skip',
|
className: 'js-skip',
|
||||||
text: __('skip')
|
text: __('skip')
|
||||||
}]
|
}]
|
||||||
head: __('Time Accounting')
|
head: __('Time Accounting')
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
class App.WidgetSharedDraft extends App.Controller
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@load()
|
||||||
|
@subscribeId = App.TicketSharedDraftStart.subscribe(@render)
|
||||||
|
@render()
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .shared-draft-item': 'clicked'
|
||||||
|
'click .js-create': 'create'
|
||||||
|
'click .js-update': 'update'
|
||||||
|
'input #shared_draft_name': 'sharedDraftNameChanged'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'#shared_draft_name': 'sharedDraftNameInput'
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
active_draft = App.TicketSharedDraftStart.find(@active_draft_id)
|
||||||
|
|
||||||
|
@html App.view('widget/shared_draft')(
|
||||||
|
shared_drafts: @visibleDrafts()
|
||||||
|
active_draft: active_draft
|
||||||
|
)
|
||||||
|
|
||||||
|
load: =>
|
||||||
|
@ajax
|
||||||
|
id: 'shared_drafts_index'
|
||||||
|
type: 'GET'
|
||||||
|
url: @apiPath + '/tickets/shared_drafts'
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.TicketSharedDraftStart.deleteAll()
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
@render()
|
||||||
|
|
||||||
|
visibleDrafts: ->
|
||||||
|
App.TicketSharedDraftStart.findAllByAttribute 'group_id', parseInt(@group_id)
|
||||||
|
|
||||||
|
clicked: (e) ->
|
||||||
|
shared_draft_id = e.currentTarget.getAttribute('shared-draft-id')
|
||||||
|
draft = App.TicketSharedDraftStart.find shared_draft_id
|
||||||
|
hasChanges = App.TaskManager.worker(@taskKey).changed()
|
||||||
|
|
||||||
|
new App.TicketSharedDraftModal(
|
||||||
|
container: @el.closest('.content')
|
||||||
|
shared_draft: draft
|
||||||
|
hasChanges: hasChanges
|
||||||
|
parent: @
|
||||||
|
)
|
||||||
|
|
||||||
|
getParams: ->
|
||||||
|
form = @formParam(@el.closest('.content').find('.ticket-create'))
|
||||||
|
meta = @formParam(@el)
|
||||||
|
form_id = form.form_id
|
||||||
|
|
||||||
|
delete form.form_id
|
||||||
|
|
||||||
|
return false if meta.name.trim() == ''
|
||||||
|
|
||||||
|
JSON.stringify({
|
||||||
|
name: meta.name
|
||||||
|
group_id: form.group_id
|
||||||
|
form_id: form_id
|
||||||
|
content: form
|
||||||
|
})
|
||||||
|
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
@render()
|
||||||
|
|
||||||
|
highlightError: ->
|
||||||
|
@sharedDraftNameInput
|
||||||
|
.addClass('has-error')
|
||||||
|
.focus()
|
||||||
|
|
||||||
|
false
|
||||||
|
|
||||||
|
sharedDraftNameChanged: (e) ->
|
||||||
|
@sharedDraftNameInput.removeClass('has-error')
|
||||||
|
|
||||||
|
create: (e) ->
|
||||||
|
@onAction(e,
|
||||||
|
id: 'shared_drafts_create'
|
||||||
|
type: 'POST'
|
||||||
|
url: @apiPath + '/tickets/shared_drafts'
|
||||||
|
)
|
||||||
|
|
||||||
|
update: (e) ->
|
||||||
|
@onAction(e,
|
||||||
|
id: 'shared_drafts_update'
|
||||||
|
type: 'PATCH'
|
||||||
|
url: @apiPath + '/tickets/shared_drafts/' + @active_draft_id
|
||||||
|
)
|
||||||
|
|
||||||
|
onAction: (e, options) ->
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
params = @getParams()
|
||||||
|
|
||||||
|
return @highlightError() if !params
|
||||||
|
|
||||||
|
@ajax _.extend(options, { data: params, success: @success })
|
||||||
|
|
||||||
|
release: =>
|
||||||
|
if @subscribeId
|
||||||
|
App.TicketSharedDraftStart.unsubscribe(@subscribeId)
|
||||||
|
|
||||||
|
super
|
|
@ -102,13 +102,22 @@ class App.Sidebar extends App.Controller
|
||||||
toggleTabAction: (name) =>
|
toggleTabAction: (name) =>
|
||||||
return if !name
|
return if !name
|
||||||
|
|
||||||
|
# remove active state
|
||||||
|
@tabs.removeClass('active')
|
||||||
|
|
||||||
|
if name == 'shared_draft'
|
||||||
|
draft_enabled = _.find @items, (elem) -> elem?.item?.name == 'shared_draft' and elem.sidebarItem()?
|
||||||
|
|
||||||
|
if !draft_enabled?
|
||||||
|
name = 'template'
|
||||||
|
|
||||||
|
available_sidebar = _.first @items, (elem) -> elem.sidebarItem()?
|
||||||
|
available_sidebar?.shown = true
|
||||||
|
|
||||||
# remember sidebarState for outsite
|
# remember sidebarState for outsite
|
||||||
if @sidebarState
|
if @sidebarState
|
||||||
@sidebarState.active = name
|
@sidebarState.active = name
|
||||||
|
|
||||||
# remove active state
|
|
||||||
@tabs.removeClass('active')
|
|
||||||
|
|
||||||
# add active state
|
# add active state
|
||||||
@$('.tabsSidebar-tab[data-tab=' + name + ']').addClass('active')
|
@$('.tabsSidebar-tab[data-tab=' + name + ']').addClass('active')
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class App.Group extends App.Model
|
class App.Group extends App.Model
|
||||||
@configure 'Group', 'name', 'assignment_timeout', 'follow_up_possible', 'follow_up_assignment', 'email_address_id', 'signature_id', 'note', 'active', 'updated_at'
|
@configure 'Group', 'name', 'assignment_timeout', 'follow_up_possible', 'follow_up_assignment', 'email_address_id', 'signature_id', 'note', 'active', 'shared_drafts', 'updated_at'
|
||||||
@extend Spine.Model.Ajax
|
@extend Spine.Model.Ajax
|
||||||
@url: @apiPath + '/groups'
|
@url: @apiPath + '/groups'
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ class App.Group extends App.Model
|
||||||
{ name: 'note', display: __('Note'), tag: 'textarea', note: __('Notes are visible to agents only, never to customers.'), limit: 250, null: true },
|
{ name: 'note', display: __('Note'), tag: 'textarea', note: __('Notes are visible to agents only, never to customers.'), limit: 250, null: true },
|
||||||
{ name: 'updated_at', display: __('Updated'), tag: 'datetime', readonly: 1 },
|
{ name: 'updated_at', display: __('Updated'), tag: 'datetime', readonly: 1 },
|
||||||
{ name: 'active', display: __('Active'), tag: 'active', default: true },
|
{ name: 'active', display: __('Active'), tag: 'active', default: true },
|
||||||
|
{ name: 'shared_drafts', display: __('Shared Drafts'), tag: 'active' },
|
||||||
]
|
]
|
||||||
@configure_clone = true
|
@configure_clone = true
|
||||||
@configure_overview = [
|
@configure_overview = [
|
||||||
|
|
|
@ -374,3 +374,11 @@ class App.Ticket extends App.Model
|
||||||
return false if !user.permission('ticket.agent')
|
return false if !user.permission('ticket.agent')
|
||||||
return true if @isAccessibleByOwner(user)
|
return true if @isAccessibleByOwner(user)
|
||||||
return @isAccessibleByGroup(user, permission)
|
return @isAccessibleByGroup(user, permission)
|
||||||
|
|
||||||
|
attributes: ->
|
||||||
|
attrs = super
|
||||||
|
|
||||||
|
if @shared_draft_id
|
||||||
|
attrs.shared_draft_id = @shared_draft_id
|
||||||
|
|
||||||
|
attrs
|
||||||
|
|
|
@ -59,3 +59,11 @@ class App.TicketArticle extends App.Model
|
||||||
if attachment && (!attachment.preferences || attachment.preferences && attachment.preferences['original-format'] isnt true)
|
if attachment && (!attachment.preferences || attachment.preferences && attachment.preferences['original-format'] isnt true)
|
||||||
attachments.push attachment
|
attachments.push attachment
|
||||||
attachments
|
attachments
|
||||||
|
|
||||||
|
attributes: ->
|
||||||
|
attrs = super
|
||||||
|
|
||||||
|
if @shared_draft_id
|
||||||
|
attrs.shared_draft_id = @shared_draft_id
|
||||||
|
|
||||||
|
attrs
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class App.TicketSharedDraftStart extends App.Model
|
||||||
|
@configure 'TicketSharedDraftStart', 'name', 'group_id'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@url: @apiPath + '/tickets/shared_drafts'
|
||||||
|
|
||||||
|
@needsLoading: true
|
|
@ -0,0 +1,4 @@
|
||||||
|
class App.TicketSharedDraftZoom extends App.Model
|
||||||
|
@configure 'TicketSharedDraftZoom', 'ticket_id', 'new_article', 'ticket_attributes'
|
||||||
|
|
||||||
|
@needsLoading: false
|
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
<form role="form" class="ticket-create">
|
<form role="form" class="ticket-create">
|
||||||
<input type="hidden" name="formSenderType"/>
|
<input type="hidden" name="formSenderType"/>
|
||||||
|
<input type="hidden" name="shared_draft_id" value="<%= @shared_draft_id %>"/>
|
||||||
<input type="hidden" name="form_id" value="<%= @form_id %>"/>
|
<input type="hidden" name="form_id" value="<%= @form_id %>"/>
|
||||||
<div class="ticket-form-top"></div>
|
<div class="ticket-form-top"></div>
|
||||||
<div class="form-group js-securityOptions hide">
|
<div class="form-group js-securityOptions hide">
|
||||||
|
|
|
@ -21,16 +21,18 @@
|
||||||
<div class="loading icon"></div>
|
<div class="loading icon"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<% if @buttonCancel: %>
|
<% if @buttonCancel || @leftButtons: %>
|
||||||
<div class="modal-leftFooter">
|
<div class="modal-leftFooter align-left">
|
||||||
<a class="btn <%= @buttonCancelClass %> js-cancel align-left" href="#"><%- @T(@buttonCancel) %></a>
|
<% if @buttonCancel: %>
|
||||||
</div>
|
<a class="btn <%= @buttonCancelClass %> js-cancel" href="#"><%- @T(@buttonCancel) %></a>
|
||||||
<% else if @leftButtons: %>
|
<% end %>
|
||||||
<% for button in @leftButtons: %>
|
|
||||||
<div class="modal-leftFooter">
|
<% if @leftButtons: %>
|
||||||
<div class="btn <%= button.className %> align-left" href="#"><%- @T(button.text) %></div>
|
<% for button in @leftButtons: %>
|
||||||
</div>
|
<a class="btn btn--text btn--subtle <%= button.className %>" href="#"><%- @T(button.text) %></a>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% for button in @centerButtons: %>
|
<% for button in @centerButtons: %>
|
||||||
<div class="modal-centerFooter">
|
<div class="modal-centerFooter">
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
class App.TicketSharedDraftModal extends App.ControllerModal
|
||||||
|
head: __('Apply Shared Draft')
|
||||||
|
events:
|
||||||
|
'click .js-delete': 'onDelete'
|
||||||
|
|
||||||
|
buttonClose: true
|
||||||
|
buttonCancel: true
|
||||||
|
buttonSubmit: 'Apply'
|
||||||
|
leftButtons: [
|
||||||
|
{
|
||||||
|
text: 'Delete',
|
||||||
|
className: 'js-delete'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@contentView = new Content(shared_draft: arguments[0].shared_draft)
|
||||||
|
super
|
||||||
|
|
||||||
|
if @shared_draft.constructor.needsLoading
|
||||||
|
@load()
|
||||||
|
|
||||||
|
@controllerBind(@importCallbackName(), @attachmentsImported)
|
||||||
|
|
||||||
|
content: ->
|
||||||
|
@contentView.el
|
||||||
|
|
||||||
|
importCallbackName: ->
|
||||||
|
"import_attachments_done-#{@controllerId}"
|
||||||
|
|
||||||
|
load: ->
|
||||||
|
@startLoading()
|
||||||
|
|
||||||
|
@ajax
|
||||||
|
id: "shared_draft_#{@shared_draft.id}"
|
||||||
|
type: 'GET'
|
||||||
|
url: @apiPath + '/tickets/shared_drafts/' + @shared_draft.id
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
@shared_draft_content = data.shared_draft_content
|
||||||
|
|
||||||
|
@contentView.loadContent(@shared_draft_content)
|
||||||
|
|
||||||
|
@stopLoading()
|
||||||
|
error: =>
|
||||||
|
@stopLoading()
|
||||||
|
|
||||||
|
onSubmit: (e) ->
|
||||||
|
if !@hasChanges
|
||||||
|
@disable(true)
|
||||||
|
@applyAttachments()
|
||||||
|
return
|
||||||
|
|
||||||
|
new App.TicketSharedDraftOverwriteModal(
|
||||||
|
head: __('Apply Draft')
|
||||||
|
message: __('There is existing content. Do you want to overwrite it?')
|
||||||
|
onSaveDraft: =>
|
||||||
|
@disable(true)
|
||||||
|
@applyAttachments()
|
||||||
|
)
|
||||||
|
|
||||||
|
attachmentsImported: (options) =>
|
||||||
|
if !options.success
|
||||||
|
@disable(false)
|
||||||
|
return
|
||||||
|
|
||||||
|
@applyMeta(options)
|
||||||
|
@cancel()
|
||||||
|
|
||||||
|
disable: (toggle) ->
|
||||||
|
@el.find('.js-submit').attr('disabled', toggle)
|
||||||
|
|
||||||
|
applyAttachments: ->
|
||||||
|
switch @shared_draft.constructor
|
||||||
|
when App.TicketSharedDraftZoom
|
||||||
|
App.Event.trigger('ui::ticket::import_draft_attachments', {
|
||||||
|
shared_draft_id: @shared_draft.id,
|
||||||
|
ticket_id: @parent.ticket_id
|
||||||
|
callbackName: @importCallbackName()
|
||||||
|
})
|
||||||
|
when App.TicketSharedDraftStart
|
||||||
|
App.Event.trigger('ticket_create_import_draft_attachments', {
|
||||||
|
shared_draft_id: @shared_draft.id,
|
||||||
|
callbackName: @importCallbackName()
|
||||||
|
})
|
||||||
|
|
||||||
|
applyMeta: (options) ->
|
||||||
|
switch @shared_draft.constructor
|
||||||
|
when App.TicketSharedDraftZoom
|
||||||
|
container = @parent.$('.article-add')
|
||||||
|
newArticleAttrs = @shared_draft.new_article
|
||||||
|
|
||||||
|
App.Event.trigger('ui::ticket::setArticleType', {
|
||||||
|
ticket: { id: @parent.ticket_id }
|
||||||
|
type: { name: newArticleAttrs.type }
|
||||||
|
article: newArticleAttrs
|
||||||
|
nofocus: true
|
||||||
|
shared_draft_id: @shared_draft.id
|
||||||
|
})
|
||||||
|
|
||||||
|
App.Event.trigger('ui::ticket::load', {
|
||||||
|
ticket_id: @parent.ticket_id
|
||||||
|
draft: @shared_draft.ticket_attributes
|
||||||
|
})
|
||||||
|
when App.TicketSharedDraftStart
|
||||||
|
content = _.clone @shared_draft_content
|
||||||
|
content.group_id = @shared_draft.group_id
|
||||||
|
content.attachments = options.attachments
|
||||||
|
App.Event.trigger 'ticket_create_rerender', { options: content, shared_draft_id: @shared_draft.id }
|
||||||
|
|
||||||
|
onDelete: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
parent = @
|
||||||
|
|
||||||
|
new App.ControllerModal
|
||||||
|
container: @container
|
||||||
|
buttonClose: true
|
||||||
|
buttonCancel: true
|
||||||
|
buttonSubmit: __('Yes')
|
||||||
|
buttonClass: 'btn--danger'
|
||||||
|
head: __('Are you sure?')
|
||||||
|
small: true
|
||||||
|
|
||||||
|
content: ->
|
||||||
|
App.i18n.translateContent('Are you sure to delete this draft?')
|
||||||
|
|
||||||
|
onSubmit: ->
|
||||||
|
@startLoading()
|
||||||
|
|
||||||
|
switch parent.shared_draft.constructor
|
||||||
|
when App.TicketSharedDraftZoom
|
||||||
|
@ajax
|
||||||
|
id: 'ticket_shared_draft_delete'
|
||||||
|
type: 'DELETE'
|
||||||
|
url: @apiPath + '/tickets/' + parent.parent.ticket_id + '/shared_draft'
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@stopLoading()
|
||||||
|
@cancel()
|
||||||
|
parent.cancel()
|
||||||
|
parent.shared_draft.remove(clear: true)
|
||||||
|
parent.parent.draftFetched()
|
||||||
|
when App.TicketSharedDraftStart
|
||||||
|
@ajax
|
||||||
|
id: 'ticket_shared_draft_delete'
|
||||||
|
type: 'DELETE'
|
||||||
|
url: @apiPath + '/tickets/shared_drafts/' + parent.shared_draft.id
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@stopLoading()
|
||||||
|
@cancel()
|
||||||
|
parent.cancel()
|
||||||
|
parent.shared_draft.remove(clear: true)
|
||||||
|
parent.parent.render()
|
||||||
|
|
||||||
|
class Content extends App.Controller
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@render()
|
||||||
|
|
||||||
|
body: ->
|
||||||
|
switch @shared_draft.constructor
|
||||||
|
when App.TicketSharedDraftZoom
|
||||||
|
@shared_draft.new_article.body
|
||||||
|
when App.TicketSharedDraftStart
|
||||||
|
@shared_draft_content?.body
|
||||||
|
|
||||||
|
author: ->
|
||||||
|
App.User.find @shared_draft.updated_by_id
|
||||||
|
|
||||||
|
timestamp: ->
|
||||||
|
new Date(@shared_draft.updated_at)
|
||||||
|
|
||||||
|
loadContent: (content) =>
|
||||||
|
@shared_draft_content = content
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('ticket_shared_draft_modal')(
|
||||||
|
body: @body()
|
||||||
|
name: @author().displayName()
|
||||||
|
timestamp: @timestamp()
|
||||||
|
)
|
||||||
|
|
||||||
|
new App.WidgetAvatar(
|
||||||
|
el: @$('.js-avatar')
|
||||||
|
object_id: @shared_draft.updated_by_id
|
||||||
|
size: 40
|
||||||
|
)
|
|
@ -0,0 +1,23 @@
|
||||||
|
<div style="display:flex">
|
||||||
|
<div style="margin-right: 5em">
|
||||||
|
<label>Author</label>
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span class="js-avatar" style="margin-right: 0.5em"></span>
|
||||||
|
<%= @name %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>Last changed</label>
|
||||||
|
<div>
|
||||||
|
<%- @humanTime(@timestamp) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<label>Text</label>
|
||||||
|
<div class='richtext-content'>
|
||||||
|
<%- @body %>
|
||||||
|
</div>
|
|
@ -0,0 +1,27 @@
|
||||||
|
class App.TicketSharedDraftOverwriteModal extends App.ControllerModal
|
||||||
|
head: __('Save Draft')
|
||||||
|
message: __('There is an existing draft. Do you want to overwrite it?')
|
||||||
|
buttonCancel: true
|
||||||
|
buttonSubmit: __('Overwrite Draft')
|
||||||
|
buttonClass: 'btn--danger'
|
||||||
|
|
||||||
|
onShowDraft: null
|
||||||
|
onSaveDraft: null
|
||||||
|
|
||||||
|
showDraft: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
@cancel()
|
||||||
|
@onShowDraft(e)
|
||||||
|
|
||||||
|
onSubmit: ->
|
||||||
|
@onSaveDraft()
|
||||||
|
@close()
|
||||||
|
|
||||||
|
post: =>
|
||||||
|
return if !@onShowDraft
|
||||||
|
|
||||||
|
button = $("<div class='btn'>#{App.Utils.icon('note')} #{__('Show Draft')}</div>")
|
||||||
|
|
||||||
|
button.click(@showDraft)
|
||||||
|
|
||||||
|
@el.find('.modal-rightFooter').prepend(button)
|
|
@ -2,6 +2,7 @@
|
||||||
<input type="hidden" name="type" value="<%= @article.type %>">
|
<input type="hidden" name="type" value="<%= @article.type %>">
|
||||||
<input type="hidden" name="internal" value="<%= @article.internal %>">
|
<input type="hidden" name="internal" value="<%= @article.internal %>">
|
||||||
<input type="hidden" name="form_id" value="<%= @form_id %>">
|
<input type="hidden" name="form_id" value="<%= @form_id %>">
|
||||||
|
<input type="hidden" name="shared_draft_id" value="<%= @article.shared_draft_id %>">
|
||||||
<input type="hidden" name="subtype" value="<%= @article.subtype %>">
|
<input type="hidden" name="subtype" value="<%= @article.subtype %>">
|
||||||
<input type="hidden" name="in_reply_to" value="<%= @article.in_reply_to %>">
|
<input type="hidden" name="in_reply_to" value="<%= @article.in_reply_to %>">
|
||||||
<div class="editControls">
|
<div class="editControls">
|
||||||
|
|
|
@ -1,4 +1,27 @@
|
||||||
<div class="attributeBar-avatars flex horizontal js-avatars hidden-xs"></div>
|
<div class="attributeBar-avatars horizontal js-avatars hidden-xs"></div>
|
||||||
|
|
||||||
|
<div class="attributeBar-draft-spacer content hidden-xs"></div>
|
||||||
|
|
||||||
|
<div class="attributeBar-draft <% if !@sharedButtonVisible: %>hide<% end %> buttonDropdown btn js-draft align-left">
|
||||||
|
<span class="attributeBar-draft--available">
|
||||||
|
<span class="hidden-xs">
|
||||||
|
<%- @Icon('note') %>
|
||||||
|
<%- @T('Draft available') %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="visible-xs">
|
||||||
|
<%- @Icon('note') %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="attributeBar-draft--saving hide">
|
||||||
|
<%- @Icon('reload') %>
|
||||||
|
<%- @T('Sharing draft...') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex"></div>
|
||||||
|
|
||||||
<div class="attributeBar-reset buttonDropdown btn js-reset <% if !@resetButtonShown: %>hide<% end %>"><span><%- @T('Discard your unsaved changes.') %></span></div>
|
<div class="attributeBar-reset buttonDropdown btn js-reset <% if !@resetButtonShown: %>hide<% end %>"><span><%- @T('Discard your unsaved changes.') %></span></div>
|
||||||
<div class="buttonDropdown dropdown dropdown--actions dropup">
|
<div class="buttonDropdown dropdown dropdown--actions dropup">
|
||||||
<div class="btn btn--text btn--icon--last" data-toggle="dropdown">
|
<div class="btn btn--text btn--icon--last" data-toggle="dropdown">
|
||||||
|
@ -30,16 +53,30 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<form class="buttonDropdown">
|
<form class="buttonDropdown">
|
||||||
<% if @macroDisabled: %>
|
<% if @macroDisabled && @sharedDraftsDisabled: %>
|
||||||
<button class="btn btn--primary js-submit"><span><%- @T('Update') %></span></button>
|
<button class="btn btn--primary js-submit"><span><%- @T('Update') %></span></button>
|
||||||
<% else: %>
|
<% else: %>
|
||||||
<div class="buttonDropdown dropdown dropup js-submitDropdown">
|
<div class="buttonDropdown dropdown dropup js-submitDropdown">
|
||||||
<button class="btn btn--primary btn--split--first js-submit"><span><%- @T('Update') %></span></button>
|
<button class="btn btn--primary btn--split--first js-submit"><span><%- @T('Update') %></span></button>
|
||||||
<button class="btn btn--primary btn--slim btn--only-icon btn--split--last js-openDropdownMacro"><%- @Icon('arrow-up') %></button>
|
<button class="btn btn--primary btn--slim btn--only-icon btn--split--last js-openDropdownMacro"><%- @Icon('arrow-up') %></button>
|
||||||
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="userAction">
|
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="userAction">
|
||||||
<% for macro in @macros: %>
|
<% if !@sharedDraftsDisabled: %>
|
||||||
<li class="js-dropdownActionMacro" role="menuitem" data-id="<%= macro.id %>"><%- macro.displayName() %>
|
<li class="label" role="menuitem">
|
||||||
<% end %>
|
<label>
|
||||||
|
Draft
|
||||||
|
|
||||||
|
<li class="js-dropdownActionSaveDraft" role="menuitem">Save Draft
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if !@macroDisabled: %>
|
||||||
|
<li class="label" role="menuitem">
|
||||||
|
<label>
|
||||||
|
Macros
|
||||||
|
|
||||||
|
<% for macro in @macros: %>
|
||||||
|
<li class="js-dropdownActionMacro" role="menuitem" data-id="<%= macro.id %>"><%- macro.displayName() %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
38
app/assets/javascripts/app/views/widget/shared_draft.jst.eco
Normal file
38
app/assets/javascripts/app/views/widget/shared_draft.jst.eco
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<div class="shared-drafts-manage">
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="" for="template_name"><%- @T('Create a shared draft') %></label>
|
||||||
|
<input type="text" name="name" id="shared_draft_name" class="form-control js-name"
|
||||||
|
placeholder="Name" value="<%= @active_draft?.name %>">
|
||||||
|
</div>
|
||||||
|
<div class="horizontal" style="justify-content: flex-end">
|
||||||
|
<button type="submit" class="btn btn--action js-update btn--primary <%= if !@active_draft? then 'hide' %>"><%- @T('Update') %></button>
|
||||||
|
<button type="submit" class="btn btn--action js-create btn--create"><%- @T('Create') %></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="" for="template_name"><%- @T('Select Shared Draft') %></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if _.isEmpty(@shared_drafts): %>
|
||||||
|
no drafts
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% for draft in @shared_drafts: %>
|
||||||
|
<div class="shared-draft-item" shared-draft-id="<%= draft.id %>">
|
||||||
|
<div class="u-highlight u-clickable"><%= draft.name %></div>
|
||||||
|
<div class="label-subtle" style="display: flex;flex-wrap:wrap;column-gap:0.2em">
|
||||||
|
<div>
|
||||||
|
<%= App.User.find(draft.updated_by_id).displayName() %>
|
||||||
|
⸱
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<%- @humanTime draft.updated_at %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
|
@ -8157,6 +8157,16 @@ a.list-group-item.active > .badge,
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-draft-spacer {
|
||||||
|
width: 2px;
|
||||||
|
height: 60px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-avatars:empty + &-draft-spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
&--border {
|
&--border {
|
||||||
border-top: 1px solid hsl(0, 0%, 94%);
|
border-top: 1px solid hsl(0, 0%, 94%);
|
||||||
}
|
}
|
||||||
|
@ -8779,6 +8789,10 @@ a.list-group-item.active > .badge,
|
||||||
@include phone {
|
@include phone {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn--text + .btn--text {
|
||||||
|
margin-bottom: -10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-leftFooter,
|
.modal-leftFooter,
|
||||||
|
@ -8931,6 +8945,14 @@ a.list-group-item.active > .badge,
|
||||||
box-shadow: 0 1px rgba(255, 255, 255, 0.13) inset;
|
box-shadow: 0 1px rgba(255, 255, 255, 0.13) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown li.label {
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
+ li {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown li:hover,
|
.dropdown li:hover,
|
||||||
.dropdown li.is-active {
|
.dropdown li.is-active {
|
||||||
background: hsl(205, 90%, 60%);
|
background: hsl(205, 90%, 60%);
|
||||||
|
|
59
app/controllers/ticket_shared_draft_zoom_controller.rb
Normal file
59
app/controllers/ticket_shared_draft_zoom_controller.rb
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
class TicketSharedDraftZoomController < ApplicationController
|
||||||
|
prepend_before_action :authorize!
|
||||||
|
prepend_before_action :authentication_check
|
||||||
|
|
||||||
|
def show
|
||||||
|
object = ticket.shared_draft
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
shared_draft_id: object&.id,
|
||||||
|
assets: object&.assets,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if ticket.shared_draft.present?
|
||||||
|
object = ticket.shared_draft
|
||||||
|
object.update! draft_params
|
||||||
|
else
|
||||||
|
object = ticket.create_shared_draft! draft_params
|
||||||
|
end
|
||||||
|
|
||||||
|
object.attach_upload_cache params[:form_id]
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
shared_draft_id: object.id,
|
||||||
|
assets: object.assets,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
object = ticket.shared_draft
|
||||||
|
|
||||||
|
object.destroy!
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
shared_draft_id: object.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_attachments
|
||||||
|
new_attachments = ticket.shared_draft.clone_attachments 'UploadCache', params[:form_id]
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
attachments: new_attachments
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ticket
|
||||||
|
Ticket.find params[:ticket_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def draft_params
|
||||||
|
params.permit ticket_attributes: {}, new_article: {}
|
||||||
|
end
|
||||||
|
end
|
|
@ -81,6 +81,16 @@ class TicketsController < ApplicationController
|
||||||
params.delete(:customer)
|
params.delete(:customer)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if (shared_draft_id = params[:shared_draft_id])
|
||||||
|
shared_draft = Ticket::SharedDraftStart.find_by id: shared_draft_id
|
||||||
|
|
||||||
|
if shared_draft && (shared_draft.group_id.to_s != params[:group_id]&.to_s || !shared_draft.group.shared_drafts?)
|
||||||
|
raise Exceptions::UnprocessableEntity, __('Shared draft not eligible for this ticket')
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_draft&.destroy
|
||||||
|
end
|
||||||
|
|
||||||
clean_params = Ticket.association_name_to_id_convert(params)
|
clean_params = Ticket.association_name_to_id_convert(params)
|
||||||
|
|
||||||
# overwrite params
|
# overwrite params
|
||||||
|
@ -92,7 +102,7 @@ class TicketsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
# The parameter :customer_id is 'abused' in cases where it is not an integer, but a string like
|
# The parameter :customer_id is 'abused' in cases where it is not an integer, but a string like
|
||||||
# 'guess:customers.email@domain.com' which implies that the customer should be looked up.
|
# 'guess:customers.email@domain.cm' which implies that the customer should be looked up.
|
||||||
if clean_params[:customer_id].is_a?(String) && clean_params[:customer_id] =~ %r{^guess:(.+?)$}
|
if clean_params[:customer_id].is_a?(String) && clean_params[:customer_id] =~ %r{^guess:(.+?)$}
|
||||||
email_address = $1
|
email_address = $1
|
||||||
email_address_validation = EmailAddressValidation.new(email_address)
|
email_address_validation = EmailAddressValidation.new(email_address)
|
||||||
|
@ -245,6 +255,16 @@ class TicketsController < ApplicationController
|
||||||
ticket.with_lock do
|
ticket.with_lock do
|
||||||
ticket.update!(clean_params)
|
ticket.update!(clean_params)
|
||||||
if params[:article].present?
|
if params[:article].present?
|
||||||
|
if (shared_draft_id = params[:article][:shared_draft_id])
|
||||||
|
shared_draft = Ticket::SharedDraftZoom.find_by id: shared_draft_id
|
||||||
|
|
||||||
|
if shared_draft && shared_draft.ticket != ticket
|
||||||
|
raise Exceptions::UnprocessableEntity, __('Shared draft not eligible for this ticket')
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_draft&.destroy
|
||||||
|
end
|
||||||
|
|
||||||
article_create(ticket, params[:article])
|
article_create(ticket, params[:article])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -694,6 +714,10 @@ class TicketsController < ApplicationController
|
||||||
assets = mention.assets(assets)
|
assets = mention.assets(assets)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if (draft = ticket.shared_draft) && authorized?(draft, :show?)
|
||||||
|
assets = draft.assets(assets)
|
||||||
|
end
|
||||||
|
|
||||||
# return result
|
# return result
|
||||||
{
|
{
|
||||||
ticket_id: ticket.id,
|
ticket_id: ticket.id,
|
||||||
|
|
90
app/controllers/tickets_shared_draft_starts_controller.rb
Normal file
90
app/controllers/tickets_shared_draft_starts_controller.rb
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
class TicketsSharedDraftStartsController < ApplicationController
|
||||||
|
prepend_before_action :authorize!
|
||||||
|
prepend_before_action :authentication_check
|
||||||
|
|
||||||
|
def index
|
||||||
|
drafts = scope
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
shared_draft_ids: drafts.map(&:id),
|
||||||
|
assets: ApplicationModel::CanAssets.reduce(drafts),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
object = scope.find params[:id]
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
shared_draft_id: object.id,
|
||||||
|
shared_draft_content: object.content,
|
||||||
|
assets: object.assets,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
object = scope.create! safe_params
|
||||||
|
object.attach_upload_cache params[:form_id]
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
shared_draft_id: object.id,
|
||||||
|
assets: object.assets,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
object = scope.find params[:id]
|
||||||
|
|
||||||
|
object.update! safe_params
|
||||||
|
object.attach_upload_cache params[:form_id]
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
shared_draft_id: object.id,
|
||||||
|
assets: object.assets,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
object = scope.find params[:id]
|
||||||
|
|
||||||
|
object.destroy!
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
shared_draft_id: object.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_attachments
|
||||||
|
object = scope.find params[:id]
|
||||||
|
|
||||||
|
new_attachments = object.clone_attachments 'UploadCache', params[:form_id]
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
attachments: new_attachments
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def scope
|
||||||
|
Ticket::SharedDraftStartPolicy::Scope
|
||||||
|
.new(current_user, Ticket::SharedDraftStart)
|
||||||
|
.resolve
|
||||||
|
end
|
||||||
|
|
||||||
|
def safe_params
|
||||||
|
safe_params = params.permit :name, :group_id, content: {}
|
||||||
|
|
||||||
|
safe_params[:content].delete :group_id
|
||||||
|
|
||||||
|
allowed_groups = current_user.groups.access('create').map(&:id).map(&:to_s)
|
||||||
|
group_id = safe_params[:group_id]&.to_s
|
||||||
|
|
||||||
|
if allowed_groups.exclude? group_id
|
||||||
|
raise Exceptions::UnprocessableEntity, __("User does not have access to one of given group IDs: #{group_id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
safe_params
|
||||||
|
end
|
||||||
|
end
|
|
@ -76,4 +76,20 @@ returns
|
||||||
|
|
||||||
new_attachments
|
new_attachments
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attach_upload_cache(form_id, source_object_name: 'UploadCache')
|
||||||
|
attachments_remove_all
|
||||||
|
|
||||||
|
Store
|
||||||
|
.list(object: source_object_name, o_id: form_id)
|
||||||
|
.map do |old_attachment|
|
||||||
|
Store.add(
|
||||||
|
object: self.class.name,
|
||||||
|
o_id: id,
|
||||||
|
data: old_attachment.content,
|
||||||
|
filename: old_attachment.filename,
|
||||||
|
preferences: old_attachment.preferences,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -81,6 +81,7 @@ class Ticket < ApplicationModel
|
||||||
has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
|
has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
|
||||||
has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
|
has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
|
||||||
has_many :mentions, as: :mentionable, dependent: :destroy
|
has_many :mentions, as: :mentionable, dependent: :destroy
|
||||||
|
has_one :shared_draft, class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy
|
||||||
belongs_to :state, class_name: 'Ticket::State', optional: true
|
belongs_to :state, class_name: 'Ticket::State', optional: true
|
||||||
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
|
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
|
||||||
belongs_to :owner, class_name: 'User', optional: true
|
belongs_to :owner, class_name: 'User', optional: true
|
||||||
|
|
24
app/models/ticket/shared_draft_start.rb
Normal file
24
app/models/ticket/shared_draft_start.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Ticket::SharedDraftStart < ApplicationModel
|
||||||
|
include CanCloneAttachments
|
||||||
|
include ChecksClientNotification
|
||||||
|
|
||||||
|
belongs_to :group
|
||||||
|
|
||||||
|
validates :name, presence: true
|
||||||
|
|
||||||
|
store :content
|
||||||
|
|
||||||
|
# don't include content into assets which may be huge
|
||||||
|
# assets are used to load the whole list of available drafts
|
||||||
|
# content is loaded separately
|
||||||
|
def filter_attributes(attributes)
|
||||||
|
super.except! 'content'
|
||||||
|
end
|
||||||
|
|
||||||
|
# required by CanCloneAttachments
|
||||||
|
def content_type
|
||||||
|
'text/html'
|
||||||
|
end
|
||||||
|
end
|
16
app/models/ticket/shared_draft_zoom.rb
Normal file
16
app/models/ticket/shared_draft_zoom.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Ticket::SharedDraftZoom < ApplicationModel
|
||||||
|
include CanCloneAttachments
|
||||||
|
include ChecksClientNotification
|
||||||
|
|
||||||
|
belongs_to :ticket, touch: true
|
||||||
|
|
||||||
|
store :new_article
|
||||||
|
store :ticket_attributes
|
||||||
|
|
||||||
|
# required by CanCloneAttachments
|
||||||
|
def content_type
|
||||||
|
'text/html'
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Controllers::TicketSharedDraftZoomControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||||
|
def show?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_attachments?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def access?(_method)
|
||||||
|
ticket_id = record.params[:ticket_id]
|
||||||
|
ticket = Ticket.find ticket_id
|
||||||
|
|
||||||
|
TicketPolicy.new(user, ticket).update?
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Controllers::TicketsSharedDraftStartsControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||||
|
def index?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_attachments?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def access?(_method)
|
||||||
|
user.permissions?('ticket.agent') && user.groups.access(:create).any?
|
||||||
|
end
|
||||||
|
end
|
27
app/policies/ticket/shared_draft_start_policy.rb
Normal file
27
app/policies/ticket/shared_draft_start_policy.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Ticket::SharedDraftStartPolicy < ApplicationPolicy
|
||||||
|
def create?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def access?(_method)
|
||||||
|
return if !user.permissions?('ticket.agent')
|
||||||
|
|
||||||
|
user.groups.access(:create).include? record.group
|
||||||
|
end
|
||||||
|
end
|
12
app/policies/ticket/shared_draft_start_policy/scope.rb
Normal file
12
app/policies/ticket/shared_draft_start_policy/scope.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Ticket::SharedDraftStartPolicy < ApplicationPolicy
|
||||||
|
class Scope < ApplicationPolicy::Scope
|
||||||
|
|
||||||
|
def resolve
|
||||||
|
scope.none if !user.permissions?(['ticket.agent'])
|
||||||
|
|
||||||
|
scope.where group_id: user.groups.access('change').map(&:id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
app/policies/ticket/shared_draft_zoom_policy.rb
Normal file
21
app/policies/ticket/shared_draft_zoom_policy.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Ticket::SharedDraftZoomPolicy < ApplicationPolicy
|
||||||
|
def update?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
access?(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def access?(_method)
|
||||||
|
TicketPolicy.new(user, record.ticket).update?
|
||||||
|
end
|
||||||
|
end
|
1
app/views/tickets_shared_draft_modal.jst.eco
Normal file
1
app/views/tickets_shared_draft_modal.jst.eco
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div class='richtext-content'><% @body()}</div>
|
|
@ -3,6 +3,20 @@
|
||||||
Zammad::Application.routes.draw do
|
Zammad::Application.routes.draw do
|
||||||
api_path = Rails.configuration.api_path
|
api_path = Rails.configuration.api_path
|
||||||
|
|
||||||
|
# ticket shared drafts
|
||||||
|
|
||||||
|
resource api_path + '/tickets/:ticket_id/shared_draft', controller: 'ticket_shared_draft_zoom', except: %w[new edit create] do
|
||||||
|
collection do
|
||||||
|
post :import_attachments, as: nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
resources api_path + '/tickets/shared_drafts', controller: 'tickets_shared_draft_starts', except: %w[new edit] do
|
||||||
|
member do
|
||||||
|
post :import_attachments, as: nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# tickets
|
# tickets
|
||||||
match api_path + '/tickets/search', to: 'tickets#search', via: %i[get post]
|
match api_path + '/tickets/search', to: 'tickets#search', via: %i[get post]
|
||||||
match api_path + '/tickets/selector', to: 'tickets#selector', via: :post
|
match api_path + '/tickets/selector', to: 'tickets#selector', via: :post
|
||||||
|
|
|
@ -104,6 +104,7 @@ class CreateBase < ActiveRecord::Migration[4.2]
|
||||||
t.string :follow_up_possible, limit: 100, null: false, default: 'yes'
|
t.string :follow_up_possible, limit: 100, null: false, default: 'yes'
|
||||||
t.boolean :follow_up_assignment, null: false, default: true
|
t.boolean :follow_up_assignment, null: false, default: true
|
||||||
t.boolean :active, null: false, default: true
|
t.boolean :active, null: false, default: true
|
||||||
|
t.boolean :shared_drafts, null: false, default: true
|
||||||
t.string :note, limit: 250, null: true
|
t.string :note, limit: 250, null: true
|
||||||
t.integer :updated_by_id, null: false
|
t.integer :updated_by_id, null: false
|
||||||
t.integer :created_by_id, null: false
|
t.integer :created_by_id, null: false
|
||||||
|
|
|
@ -589,6 +589,27 @@ class CreateTicket < ActiveRecord::Migration[4.2]
|
||||||
t.timestamps limit: 3, null: false
|
t.timestamps limit: 3, null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table :ticket_shared_draft_zooms do |t|
|
||||||
|
t.references :ticket, null: false, foreign_key: { to_table: :tickets }
|
||||||
|
t.text :new_article
|
||||||
|
t.text :ticket_attributes
|
||||||
|
|
||||||
|
t.column :created_by_id, :integer, null: true
|
||||||
|
t.column :updated_by_id, :integer, null: true
|
||||||
|
|
||||||
|
t.timestamps limit: 3
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :ticket_shared_draft_starts do |t|
|
||||||
|
t.references :group, null: false, foreign_key: { to_table: :groups }
|
||||||
|
t.string :name
|
||||||
|
t.text :content
|
||||||
|
|
||||||
|
t.column :created_by_id, :integer, null: true
|
||||||
|
t.column :updated_by_id, :integer, null: true
|
||||||
|
|
||||||
|
t.timestamps limit: 3
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.down
|
def self.down
|
||||||
|
@ -626,5 +647,7 @@ class CreateTicket < ActiveRecord::Migration[4.2]
|
||||||
drop_table :ticket_states
|
drop_table :ticket_states
|
||||||
drop_table :ticket_state_types
|
drop_table :ticket_state_types
|
||||||
drop_table :webhooks
|
drop_table :webhooks
|
||||||
|
drop_table :ticket_shared_draft_zooms
|
||||||
|
drop_table :ticket_shared_draft_starts
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
73
db/migrate/20211220145252_create_ticket_shared_drafts.rb
Normal file
73
db/migrate/20211220145252_create_ticket_shared_drafts.rb
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
class CreateTicketSharedDrafts < ActiveRecord::Migration[5.0]
|
||||||
|
def change # rubocop:disable Metrics/AbcSize
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.exists?(name: 'system_init_done')
|
||||||
|
|
||||||
|
create_table :ticket_shared_draft_zooms do |t|
|
||||||
|
t.references :ticket, null: false, foreign_key: { to_table: :tickets }
|
||||||
|
t.text :new_article
|
||||||
|
t.text :ticket_attributes
|
||||||
|
|
||||||
|
t.column :created_by_id, :integer, null: true
|
||||||
|
t.column :updated_by_id, :integer, null: true
|
||||||
|
|
||||||
|
t.timestamps limit: 3
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :ticket_shared_draft_starts do |t|
|
||||||
|
t.references :group, null: false, foreign_key: { to_table: :groups }
|
||||||
|
t.string :name
|
||||||
|
t.text :content
|
||||||
|
|
||||||
|
t.column :created_by_id, :integer, null: true
|
||||||
|
t.column :updated_by_id, :integer, null: true
|
||||||
|
|
||||||
|
t.timestamps limit: 3
|
||||||
|
end
|
||||||
|
|
||||||
|
change_table :groups do |t|
|
||||||
|
t.boolean :shared_drafts, null: false, default: true
|
||||||
|
end
|
||||||
|
|
||||||
|
Group.reset_column_information
|
||||||
|
|
||||||
|
UserInfo.current_user_id = 1
|
||||||
|
ObjectManager::Attribute.add(
|
||||||
|
force: true,
|
||||||
|
object: 'Group',
|
||||||
|
name: 'shared_drafts',
|
||||||
|
display: 'Shared Drafts',
|
||||||
|
data_type: 'active',
|
||||||
|
data_option: {
|
||||||
|
null: false,
|
||||||
|
default: true,
|
||||||
|
permission: ['admin.group'],
|
||||||
|
},
|
||||||
|
editable: true,
|
||||||
|
active: true,
|
||||||
|
screens: {
|
||||||
|
create: {
|
||||||
|
'-all-' => {
|
||||||
|
null: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
'-all-': {
|
||||||
|
null: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
'-all-' => {
|
||||||
|
shown: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_create: false,
|
||||||
|
to_migrate: false,
|
||||||
|
to_delete: false,
|
||||||
|
position: 1400,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1810,6 +1810,42 @@ ObjectManager::Attribute.add(
|
||||||
position: 600,
|
position: 600,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ObjectManager::Attribute.add(
|
||||||
|
force: true,
|
||||||
|
object: 'Group',
|
||||||
|
name: 'shared_drafts',
|
||||||
|
display: __('Shared Drafts'),
|
||||||
|
data_type: 'active',
|
||||||
|
data_option: {
|
||||||
|
null: false,
|
||||||
|
default: true,
|
||||||
|
permission: ['admin.group'],
|
||||||
|
},
|
||||||
|
editable: true,
|
||||||
|
active: true,
|
||||||
|
screens: {
|
||||||
|
create: {
|
||||||
|
'-all-' => {
|
||||||
|
null: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
'-all-': {
|
||||||
|
null: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
'-all-' => {
|
||||||
|
shown: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_create: false,
|
||||||
|
to_migrate: false,
|
||||||
|
to_delete: false,
|
||||||
|
position: 1400,
|
||||||
|
)
|
||||||
|
|
||||||
ObjectManager::Attribute.add(
|
ObjectManager::Attribute.add(
|
||||||
force: true,
|
force: true,
|
||||||
object: 'Group',
|
object: 'Group',
|
||||||
|
|
|
@ -807,6 +807,14 @@ msgstr ""
|
||||||
msgid "Apply"
|
msgid "Apply"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/ticket_shared_draft_modal.coffee
|
||||||
|
msgid "Apply Draft"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/ticket_shared_draft_modal.coffee
|
||||||
|
msgid "Apply Shared Draft"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_ui_element/basedate.coffee
|
#: app/assets/javascripts/app/controllers/_ui_element/basedate.coffee
|
||||||
#: app/assets/javascripts/app/controllers/report.coffee
|
#: app/assets/javascripts/app/controllers/report.coffee
|
||||||
#: app/assets/javascripts/app/controllers/time_accounting.coffee
|
#: app/assets/javascripts/app/controllers/time_accounting.coffee
|
||||||
|
@ -829,6 +837,10 @@ msgstr ""
|
||||||
msgid "Archived at"
|
msgid "Archived at"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/ticket_shared_draft_modal.coffee
|
||||||
|
msgid "Are you sure to delete this draft?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/knowledge_base/content_controller.coffee
|
#: app/assets/javascripts/app/controllers/knowledge_base/content_controller.coffee
|
||||||
msgid "Are you sure you want to reload? You have unsaved changes that will get lost"
|
msgid "Are you sure you want to reload? You have unsaved changes that will get lost"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -844,6 +856,7 @@ msgstr ""
|
||||||
#: app/assets/javascripts/app/controllers/maintenance.coffee
|
#: app/assets/javascripts/app/controllers/maintenance.coffee
|
||||||
#: app/assets/javascripts/app/controllers/ticket_zoom/article_action/delete.coffee
|
#: app/assets/javascripts/app/controllers/ticket_zoom/article_action/delete.coffee
|
||||||
#: app/assets/javascripts/app/controllers/widget/template.coffee
|
#: app/assets/javascripts/app/controllers/widget/template.coffee
|
||||||
|
#: app/assets/javascripts/app/views/ticket_shared_draft_modal.coffee
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -2115,6 +2128,7 @@ msgstr ""
|
||||||
#: app/assets/javascripts/app/views/object_manager/index.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/index.jst.eco
|
||||||
#: app/assets/javascripts/app/views/profile/token_access.jst.eco
|
#: app/assets/javascripts/app/views/profile/token_access.jst.eco
|
||||||
#: app/assets/javascripts/app/views/translation/todo.jst.eco
|
#: app/assets/javascripts/app/views/translation/todo.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/widget/shared_draft.jst.eco
|
||||||
msgid "Create"
|
msgid "Create"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -2150,6 +2164,10 @@ msgstr ""
|
||||||
msgid "Create a Test Ticket"
|
msgid "Create a Test Ticket"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/widget/shared_draft.jst.eco
|
||||||
|
msgid "Create a shared draft"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/knowledge_base/reader_controller.coffee
|
#: app/assets/javascripts/app/controllers/knowledge_base/reader_controller.coffee
|
||||||
msgid "Create a translation"
|
msgid "Create a translation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -3330,6 +3348,10 @@ msgstr ""
|
||||||
msgid "Draft"
|
msgid "Draft"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/ticket_zoom/attribute_bar.jst.eco
|
||||||
|
msgid "Draft available"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_application_controller/reorder_modal.coffee
|
#: app/assets/javascripts/app/controllers/_application_controller/reorder_modal.coffee
|
||||||
msgid "Drag to reorder"
|
msgid "Drag to reorder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -5389,6 +5411,10 @@ msgstr ""
|
||||||
msgid "Last Contact Customer At"
|
msgid "Last Contact Customer At"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/controllers/ticket_zoom/attribute_bar.coffee
|
||||||
|
msgid "Last change %s<br>by %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/models/ticket.coffee
|
#: app/assets/javascripts/app/models/ticket.coffee
|
||||||
msgid "Last contact"
|
msgid "Last contact"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -6902,6 +6928,10 @@ msgstr ""
|
||||||
msgid "Overviews are …"
|
msgid "Overviews are …"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/ticket_shared_draft_overwrite_modal.coffee
|
||||||
|
msgid "Overwrite Draft"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
|
#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
|
||||||
#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
|
#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
|
||||||
#: app/assets/javascripts/app/models/ticket.coffee
|
#: app/assets/javascripts/app/models/ticket.coffee
|
||||||
|
@ -7778,6 +7808,10 @@ msgstr ""
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/ticket_shared_draft_overwrite_modal.coffee
|
||||||
|
msgid "Save Draft"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/views/widget/text_module.jst.eco
|
#: app/assets/javascripts/app/views/widget/text_module.jst.eco
|
||||||
msgid "Save as"
|
msgid "Save as"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -7908,6 +7942,10 @@ msgstr ""
|
||||||
msgid "Select CSV file"
|
msgid "Select CSV file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/widget/shared_draft.jst.eco
|
||||||
|
msgid "Select Shared Draft"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/views/widget/template.jst.eco
|
#: app/assets/javascripts/app/views/widget/template.jst.eco
|
||||||
msgid "Select Template"
|
msgid "Select Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -8155,11 +8193,25 @@ msgstr ""
|
||||||
msgid "Shared"
|
msgid "Shared"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_shared_draft.coffee
|
||||||
|
#: app/assets/javascripts/app/models/group.coffee
|
||||||
|
#: db/seeds/object_manager_attributes.rb
|
||||||
|
msgid "Shared Drafts"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/controllers/tickets_controller.rb
|
||||||
|
msgid "Shared draft not eligible for this ticket"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/models/organization.coffee
|
#: app/assets/javascripts/app/models/organization.coffee
|
||||||
#: db/seeds/object_manager_attributes.rb
|
#: db/seeds/object_manager_attributes.rb
|
||||||
msgid "Shared organization"
|
msgid "Shared organization"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/ticket_zoom/attribute_bar.jst.eco
|
||||||
|
msgid "Sharing draft..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_profile/avatar.coffee
|
#: app/assets/javascripts/app/controllers/_profile/avatar.coffee
|
||||||
msgid "Shoot"
|
msgid "Shoot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -8173,6 +8225,10 @@ msgstr ""
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/ticket_shared_draft_overwrite_modal.coffee
|
||||||
|
msgid "Show Draft"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/views/navigation.jst.eco
|
#: app/assets/javascripts/app/views/navigation.jst.eco
|
||||||
msgid "Show Search Details"
|
msgid "Show Search Details"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -9001,6 +9057,14 @@ msgstr ""
|
||||||
msgid "There are too many people in the chat queue."
|
msgid "There are too many people in the chat queue."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/ticket_shared_draft_overwrite_modal.coffee
|
||||||
|
msgid "There is an existing draft. Do you want to overwrite it?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/ticket_shared_draft_modal.coffee
|
||||||
|
msgid "There is existing content. Do you want to overwrite it?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_manage/knowledge_base.coffee
|
#: app/assets/javascripts/app/controllers/_manage/knowledge_base.coffee
|
||||||
msgid "There is no Knowledge Base yet. Please create one."
|
msgid "There is no Knowledge Base yet. Please create one."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -9617,6 +9681,7 @@ msgstr ""
|
||||||
#: app/assets/javascripts/app/views/knowledge_base/content.jst.eco
|
#: app/assets/javascripts/app/views/knowledge_base/content.jst.eco
|
||||||
#: app/assets/javascripts/app/views/session.jst.eco
|
#: app/assets/javascripts/app/views/session.jst.eco
|
||||||
#: app/assets/javascripts/app/views/ticket_zoom/attribute_bar.jst.eco
|
#: app/assets/javascripts/app/views/ticket_zoom/attribute_bar.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/widget/shared_draft.jst.eco
|
||||||
msgid "Update"
|
msgid "Update"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -10337,6 +10402,7 @@ msgstr ""
|
||||||
#: app/assets/javascripts/app/controllers/_application_controller/_modal_generic_confirm.coffee
|
#: app/assets/javascripts/app/controllers/_application_controller/_modal_generic_confirm.coffee
|
||||||
#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
|
#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
|
||||||
#: app/assets/javascripts/app/controllers/tag.coffee
|
#: app/assets/javascripts/app/controllers/tag.coffee
|
||||||
|
#: app/assets/javascripts/app/views/ticket_shared_draft_modal.coffee
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
9
spec/factories/ticket/shared_draft/start.rb
Normal file
9
spec/factories/ticket/shared_draft/start.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :ticket_shared_draft_start, class: 'Ticket::SharedDraftStart' do
|
||||||
|
name { Faker::Name.unique.name }
|
||||||
|
group { create(:group) }
|
||||||
|
content { { content: true } }
|
||||||
|
end
|
||||||
|
end
|
9
spec/factories/ticket/shared_draft/zooms.rb
Normal file
9
spec/factories/ticket/shared_draft/zooms.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :ticket_shared_draft_zoom, class: 'Ticket::SharedDraftZoom' do
|
||||||
|
ticket { create(:ticket) }
|
||||||
|
new_article { { new_article: true } }
|
||||||
|
ticket_attributes { { ticket_attributes: true } }
|
||||||
|
end
|
||||||
|
end
|
11
spec/models/ticket/shared_draft_start_spec.rb
Normal file
11
spec/models/ticket/shared_draft_start_spec.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Ticket::SharedDraftStart, type: :model do
|
||||||
|
subject(:shared_draft_start) { create :ticket_shared_draft_start }
|
||||||
|
|
||||||
|
it { is_expected.to belong_to :group }
|
||||||
|
it { is_expected.to validate_presence_of :name }
|
||||||
|
it { expect(shared_draft_start.content).to be_a(Hash) }
|
||||||
|
end
|
11
spec/models/ticket/shared_draft_zoom_spec.rb
Normal file
11
spec/models/ticket/shared_draft_zoom_spec.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Ticket::SharedDraftZoom, type: :model do
|
||||||
|
subject(:shared_draft_zoom) { create :ticket_shared_draft_zoom }
|
||||||
|
|
||||||
|
it { is_expected.to belong_to :ticket }
|
||||||
|
it { expect(shared_draft_zoom.new_article).to be_a(Hash) }
|
||||||
|
it { expect(shared_draft_zoom.ticket_attributes).to be_a(Hash) }
|
||||||
|
end
|
|
@ -1778,9 +1778,10 @@ RSpec.describe Ticket, type: :model do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'destroys all related dependencies' do
|
it 'destroys all related dependencies' do
|
||||||
refs_known = { 'Ticket::Article' => { 'ticket_id'=>1 },
|
refs_known = { 'Ticket::Article' => { 'ticket_id'=>1 },
|
||||||
'Ticket::TimeAccounting' => { 'ticket_id'=>1 },
|
'Ticket::TimeAccounting' => { 'ticket_id'=>1 },
|
||||||
'Ticket::Flag' => { 'ticket_id'=>1 } }
|
'Ticket::SharedDraftZoom' => { 'ticket_id'=>0 },
|
||||||
|
'Ticket::Flag' => { 'ticket_id'=>1 } }
|
||||||
|
|
||||||
ticket = create(:ticket)
|
ticket = create(:ticket)
|
||||||
article = create(:ticket_article, ticket: ticket)
|
article = create(:ticket_article, ticket: ticket)
|
||||||
|
|
|
@ -961,6 +961,8 @@ RSpec.describe User, type: :model do
|
||||||
'Ticket::Article::Type' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
'Ticket::Article::Type' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
'Ticket::Article::Flag' => { 'created_by_id' => 0 },
|
'Ticket::Article::Flag' => { 'created_by_id' => 0 },
|
||||||
'Ticket::Priority' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
'Ticket::Priority' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
|
'Ticket::SharedDraftStart' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
|
'Ticket::SharedDraftZoom' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
'Ticket::TimeAccounting' => { 'created_by_id' => 0 },
|
'Ticket::TimeAccounting' => { 'created_by_id' => 0 },
|
||||||
'Ticket::State' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
'Ticket::State' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
'Ticket::Flag' => { 'created_by_id' => 0 },
|
'Ticket::Flag' => { 'created_by_id' => 0 },
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Controllers::TicketSharedDraftZoomControllerPolicy do
|
||||||
|
subject { described_class.new(user, record) }
|
||||||
|
|
||||||
|
let(:record_class) { TicketSharedDraftZoomController }
|
||||||
|
let(:ticket) { create(:ticket) }
|
||||||
|
let(:user) { create(:agent) }
|
||||||
|
|
||||||
|
let(:record) do
|
||||||
|
rec = record_class.new
|
||||||
|
rec.action_name = action_name
|
||||||
|
rec.params = params
|
||||||
|
|
||||||
|
rec
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'basic checks' do
|
||||||
|
let(:params) { { ticket_id: ticket.id } }
|
||||||
|
|
||||||
|
context 'when has access to ticket' do
|
||||||
|
before do
|
||||||
|
user.user_groups.create! group: ticket.group, access: :full
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to permit_action(action_name) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when has no access to ticket' do
|
||||||
|
it { is_expected.not_to permit_action(action_name) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#show?' do
|
||||||
|
let(:action_name) { :show }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#create?' do
|
||||||
|
let(:action_name) { :create }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#update?' do
|
||||||
|
let(:action_name) { :update }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#destroy?' do
|
||||||
|
let(:action_name) { :destroy }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#import_attachments?' do
|
||||||
|
let(:action_name) { :import_attachments }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Controllers::TicketsSharedDraftStartsControllerPolicy do
|
||||||
|
subject { described_class.new(user, record) }
|
||||||
|
|
||||||
|
let(:record_class) { TicketsSharedDraftStartsController }
|
||||||
|
|
||||||
|
let(:record) do
|
||||||
|
rec = record_class.new
|
||||||
|
rec.action_name = action_name
|
||||||
|
|
||||||
|
rec
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'basic checks' do
|
||||||
|
context 'when has access to tickets' do
|
||||||
|
let(:user) do
|
||||||
|
user = create(:agent)
|
||||||
|
user.user_groups.create! group: create(:group), access: :full
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to permit_action(action_name) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when has no access to tickets' do
|
||||||
|
let(:user) { create(:customer) }
|
||||||
|
|
||||||
|
it { is_expected.not_to permit_action(action_name) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#index?' do
|
||||||
|
let(:action_name) { :index }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#show?' do
|
||||||
|
let(:action_name) { :show }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#create?' do
|
||||||
|
let(:action_name) { :create }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#update?' do
|
||||||
|
let(:action_name) { :update }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#destroy?' do
|
||||||
|
let(:action_name) { :destroy }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#import_attachments?' do
|
||||||
|
let(:action_name) { :import_attachments }
|
||||||
|
|
||||||
|
include_examples 'basic checks'
|
||||||
|
end
|
||||||
|
end
|
46
spec/policies/ticket/shared_draft_start_policy/scope_spec.rb
Normal file
46
spec/policies/ticket/shared_draft_start_policy/scope_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Ticket::SharedDraftStartPolicy::Scope do
|
||||||
|
subject(:scope) { described_class.new(user, original_collection) }
|
||||||
|
|
||||||
|
let(:original_collection) { Ticket::SharedDraftStart }
|
||||||
|
|
||||||
|
let(:group_a) { create(:group) }
|
||||||
|
let(:draft_a) { create(:ticket_shared_draft_start, group: group_a) }
|
||||||
|
let(:group_b) { create(:group) }
|
||||||
|
let(:draft_b) { create(:ticket_shared_draft_start, group: group_b) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
draft_a && draft_b
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#resolve' do
|
||||||
|
context 'without user' do
|
||||||
|
let(:user) { nil }
|
||||||
|
|
||||||
|
it 'throws exception' do
|
||||||
|
expect { scope.resolve }.to raise_error %r{Authentication required}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with customer' do
|
||||||
|
let(:user) { create(:customer) }
|
||||||
|
|
||||||
|
it 'returns empty' do
|
||||||
|
expect(scope.resolve).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with agent' do
|
||||||
|
let(:user) { create(:agent) }
|
||||||
|
|
||||||
|
before { user.groups << group_a }
|
||||||
|
|
||||||
|
it 'returns group a' do
|
||||||
|
expect(scope.resolve).to match_array [draft_a]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
58
spec/policies/ticket/shared_draft_start_policy_spec.rb
Normal file
58
spec/policies/ticket/shared_draft_start_policy_spec.rb
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Ticket::SharedDraftStartPolicy do
|
||||||
|
subject { described_class.new(user, record) }
|
||||||
|
|
||||||
|
let(:group) { record.group }
|
||||||
|
let(:record) { create(:ticket_shared_draft_start) }
|
||||||
|
let(:user) { create(:agent) }
|
||||||
|
|
||||||
|
shared_examples 'access allowed' do
|
||||||
|
it { is_expected.to be_create }
|
||||||
|
it { is_expected.to be_update }
|
||||||
|
it { is_expected.to be_show }
|
||||||
|
it { is_expected.to be_destroy }
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'access denied' do
|
||||||
|
it { is_expected.not_to be_create }
|
||||||
|
it { is_expected.not_to be_update }
|
||||||
|
it { is_expected.not_to be_show }
|
||||||
|
it { is_expected.not_to be_destroy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has no tickets access' do
|
||||||
|
let(:user) { create(:customer) }
|
||||||
|
|
||||||
|
include_examples 'access denied'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has ticket access' do
|
||||||
|
context 'when draft has same group as user' do
|
||||||
|
before do
|
||||||
|
user.user_groups.create! group: group, access: :full
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'access allowed'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when draft has same group as user but read-only' do
|
||||||
|
before do
|
||||||
|
user.user_groups.create! group: group, access: :read
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'access denied'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when draft has one of the groups of the user' do
|
||||||
|
before do
|
||||||
|
user.user_groups.create! group: group, access: :full
|
||||||
|
user.user_groups.create! group: create(:group), access: :full
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'access allowed'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
55
spec/policies/ticket/shared_draft_zoom_policy_spec.rb
Normal file
55
spec/policies/ticket/shared_draft_zoom_policy_spec.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Ticket::SharedDraftZoomPolicy do
|
||||||
|
subject { described_class.new(user, record) }
|
||||||
|
|
||||||
|
let(:ticket) { create(:ticket) }
|
||||||
|
let(:record) { create(:ticket_shared_draft_zoom, ticket: ticket) }
|
||||||
|
let(:user) { create(:agent) }
|
||||||
|
|
||||||
|
shared_examples 'access allowed' do
|
||||||
|
it { is_expected.to be_update }
|
||||||
|
it { is_expected.to be_show }
|
||||||
|
it { is_expected.to be_destroy }
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'access denied' do
|
||||||
|
it { is_expected.not_to be_update }
|
||||||
|
it { is_expected.not_to be_show }
|
||||||
|
it { is_expected.not_to be_destroy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has no tickets access' do
|
||||||
|
let(:user) { create(:customer) }
|
||||||
|
|
||||||
|
include_examples 'access denied'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has ticket access' do
|
||||||
|
context 'when user has access to the ticket' do
|
||||||
|
before do
|
||||||
|
user.user_groups.create! group: ticket.group, access: :full
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'access allowed'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has read-only access to the ticket' do
|
||||||
|
before do
|
||||||
|
user.user_groups.create! group: ticket.group, access: :read
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'access denied'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has no access to the ticket' do
|
||||||
|
before do
|
||||||
|
user.user_groups.create! group: create(:group), access: :full
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'access denied'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
303
spec/requests/ticket/shared_draft/starts_controller_spec.rb
Normal file
303
spec/requests/ticket/shared_draft/starts_controller_spec.rb
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Ticket Shared Drafts Start API endpoints', authenticated_as: :agent, type: :request do
|
||||||
|
|
||||||
|
let(:group_a) { create(:group, shared_drafts: true) }
|
||||||
|
let(:group_b) { create(:group, shared_drafts: true) }
|
||||||
|
let(:group_c) { create(:group, shared_drafts: false) }
|
||||||
|
|
||||||
|
let(:draft_a) { create(:ticket_shared_draft_start, group: group_a) }
|
||||||
|
let(:draft_b) { create(:ticket_shared_draft_start, group: group_b) }
|
||||||
|
let(:draft_c) { create(:ticket_shared_draft_start, group: group_c) }
|
||||||
|
|
||||||
|
let(:agent) do
|
||||||
|
user = create(:agent)
|
||||||
|
user.user_groups.create! group: group_a, access: :full
|
||||||
|
user.user_groups.create! group: group_c, access: :full
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:other_agent) { create(:agent) }
|
||||||
|
let(:customer) { create(:customer) }
|
||||||
|
|
||||||
|
let(:form_id) { 12_345 }
|
||||||
|
|
||||||
|
let(:base_params) do
|
||||||
|
{
|
||||||
|
name: 'draft name',
|
||||||
|
group_id: group_a.id,
|
||||||
|
form_id: form_id,
|
||||||
|
content: { attrs: true }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:path) { '/api/v1/tickets/shared_drafts' }
|
||||||
|
let(:path_draft_a) { "#{path}/#{draft_a.id}" }
|
||||||
|
let(:path_draft_b) { "#{path}/#{draft_b.id}" }
|
||||||
|
let(:path_draft_nonexistant) { "#{path}/asd" }
|
||||||
|
|
||||||
|
describe 'request handling' do
|
||||||
|
describe '#index' do
|
||||||
|
it 'returns drafts that user has access to' do
|
||||||
|
draft_a && draft_b
|
||||||
|
|
||||||
|
get path, as: :json
|
||||||
|
|
||||||
|
expect(json_response).to include('shared_draft_ids' => [draft_a.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns empty array when no drafts available' do
|
||||||
|
get path, as: :json
|
||||||
|
|
||||||
|
expect(json_response).to include('shared_draft_ids' => [])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error when user has no permissions', authenticated_as: :customer do
|
||||||
|
get path, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#show' do
|
||||||
|
it 'returns draft' do
|
||||||
|
get path_draft_a, as: :json
|
||||||
|
|
||||||
|
expect(json_response).to include('shared_draft_id' => draft_a.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when draft does not exist' do
|
||||||
|
get path_draft_nonexistant, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when user has no permissions to drafts', authenticated_as: :other_agent do
|
||||||
|
get path_draft_b, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when user has no permissions to this draft' do
|
||||||
|
get path_draft_b, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns error when user has no permissions', authenticated_as: :customer do
|
||||||
|
get path_draft_b, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#create' do
|
||||||
|
it 'creates draft' do
|
||||||
|
post path, params: base_params, as: :json
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftStart).to be_exist json_response['shared_draft_id']
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates draft with attachment' do
|
||||||
|
attach(id: form_id)
|
||||||
|
|
||||||
|
post path, params: base_params, as: :json
|
||||||
|
|
||||||
|
new_draft = Ticket::SharedDraftStart.find json_response['shared_draft_id']
|
||||||
|
|
||||||
|
expect(new_draft.attachments).to be_one
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'verifies user has access to given group' do
|
||||||
|
post path, params: base_params.merge(group_id: group_b.id), as: :json
|
||||||
|
|
||||||
|
expect(json_response).to include 'error_human' => %r{does not have access}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error when user has no create permission on any group', authenticated_as: :other_agent do
|
||||||
|
post path, params: base_params, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error when user has no permissions', authenticated_as: :customer do
|
||||||
|
post path, params: base_params, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#update' do
|
||||||
|
it 'updates draft' do
|
||||||
|
patch path_draft_a, params: base_params, as: :json
|
||||||
|
|
||||||
|
expect(draft_a.reload).to have_attributes(content: { attrs: true })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates draft with attachment' do
|
||||||
|
attach(id: form_id)
|
||||||
|
|
||||||
|
expect { patch path_draft_a, params: base_params, as: :json }
|
||||||
|
.to change { draft_a.attachments.count }
|
||||||
|
.by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates draft to have no attachments' do
|
||||||
|
attach(id: draft_a.id, object_name: draft_a.class.name)
|
||||||
|
|
||||||
|
expect { patch path_draft_a, params: base_params, as: :json }
|
||||||
|
.to change { draft_a.attachments.count }
|
||||||
|
.by(-1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when draft does not exist' do
|
||||||
|
patch path_draft_nonexistant, params: base_params, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes draft group' do
|
||||||
|
agent.user_groups.create! group: group_b, access: :full
|
||||||
|
|
||||||
|
patch path_draft_b, params: base_params.merge(group_id: group_b.id), as: :json
|
||||||
|
|
||||||
|
expect(draft_b.reload.group).to eq group_b
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns updated draft ID' do
|
||||||
|
patch path_draft_a, params: base_params, as: :json
|
||||||
|
|
||||||
|
expect(json_response).to include 'shared_draft_id' => draft_a.id
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'verifies user has access to given groups' do
|
||||||
|
patch path_draft_a, params: base_params.merge(group_id: group_b.id), as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns error when user has no permissions', authenticated_as: :customer do
|
||||||
|
patch path_draft_a, params: base_params, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#destroy' do
|
||||||
|
it 'destroys draft' do
|
||||||
|
delete path_draft_a, as: :json
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftStart).not_to be_exist draft_a.id
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when draft does not exist' do
|
||||||
|
delete path_draft_nonexistant, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when user has no permissions to this draft' do
|
||||||
|
delete path_draft_b, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns error when user has no permissions', authenticated_as: :customer do
|
||||||
|
delete path_draft_b, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#import_attachments' do
|
||||||
|
let(:import_path) { "#{path_draft_a}/import_attachments" }
|
||||||
|
let(:import_params) do
|
||||||
|
{
|
||||||
|
form_id: form_id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'imports attachments from draft to given form ID' do
|
||||||
|
attach(id: draft_a.id, object_name: draft_a.class.name)
|
||||||
|
|
||||||
|
expect { post import_path, params: import_params, as: :json }
|
||||||
|
.to change { Store.list(object: 'UploadCache', o_id: form_id).count }
|
||||||
|
.by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns success if draft has no attachments' do
|
||||||
|
post import_path, params: import_params, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'clean up' do
|
||||||
|
it 'removes draft when creating a ticket' do
|
||||||
|
post_new_ticket group_a.id, draft_a.id
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftStart).not_to be_exist(draft_a.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'not removes draft when fails creating a ticket' do
|
||||||
|
post_new_ticket group_a.id, draft_a.id, valid: false
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftStart).to be_exist(draft_a.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error if draft is not applicable in this context' do
|
||||||
|
post_new_ticket group_b.id, draft_a.id
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps draft if not applicable in this context' do
|
||||||
|
post_new_ticket group_b.id, draft_a.id
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftStart).to be_exist(draft_a.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error if group does not support drafts' do
|
||||||
|
post_new_ticket group_c.id, draft_c.id
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'succeeds when draft does not exist' do
|
||||||
|
post_new_ticket group_a.id, 1_234
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_new_ticket(group_id, shared_draft_id, valid: true)
|
||||||
|
params = {
|
||||||
|
title: 'a new ticket #1',
|
||||||
|
group_id: group_id,
|
||||||
|
customer_id: create(:customer).id,
|
||||||
|
shared_draft_id: shared_draft_id,
|
||||||
|
article: {
|
||||||
|
content_type: 'text/plain',
|
||||||
|
body: valid ? 'some body' : nil,
|
||||||
|
sender: 'Customer',
|
||||||
|
type: 'note',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
post '/api/v1/tickets', params: params, as: :json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attach(id:, object_name: 'UploadCache')
|
||||||
|
Store.add(
|
||||||
|
object: object_name,
|
||||||
|
o_id: id,
|
||||||
|
data: File.binread(Rails.root.join('test/data/image/1x1.png')),
|
||||||
|
filename: '1x1.png',
|
||||||
|
preferences: {},
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
236
spec/requests/ticket/shared_draft/zoom_controller_spec.rb
Normal file
236
spec/requests/ticket/shared_draft/zoom_controller_spec.rb
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Ticket Shared Drafts Zoom API endpoints', authenticated_as: :agent, type: :request do
|
||||||
|
|
||||||
|
let(:group) { create(:group, shared_drafts: true) }
|
||||||
|
let(:ticket) { create(:ticket, group: group) }
|
||||||
|
|
||||||
|
let(:agent) do
|
||||||
|
user = create(:agent)
|
||||||
|
user.user_groups.create! group: ticket.group, access: :full
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:other_agent) { create(:agent) }
|
||||||
|
|
||||||
|
let(:path) { "/api/v1/tickets/#{ticket.id}/shared_draft" }
|
||||||
|
let(:non_existant_path) { "/api/v1/tickets/a#{ticket.id}/shared_draft" }
|
||||||
|
|
||||||
|
describe 'request handling' do
|
||||||
|
describe '#show' do
|
||||||
|
it 'returns draft' do
|
||||||
|
draft = create(:ticket_shared_draft_zoom, ticket: ticket)
|
||||||
|
|
||||||
|
get path, as: :json
|
||||||
|
|
||||||
|
expect(json_response).to include('shared_draft_id' => draft.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns empty when draft does not exist' do
|
||||||
|
get path, as: :json
|
||||||
|
|
||||||
|
expect(json_response).to include('shared_draft_id' => nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error when ticket does not exist' do
|
||||||
|
get non_existant_path, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error when user has no permissions', authenticated_as: :other_agent do
|
||||||
|
get path, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#update' do
|
||||||
|
let(:form_id) { 12_345 }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
form_id: form_id,
|
||||||
|
ticket_attributes: { attrs: true }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates draft if does not exist' do
|
||||||
|
put path, params: params, as: :json
|
||||||
|
|
||||||
|
shared_draft = ticket.reload_shared_draft
|
||||||
|
|
||||||
|
expect(shared_draft).to have_attributes(ticket_attributes: { attrs: true })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates draft with attachment if does not exist' do
|
||||||
|
attach(id: form_id)
|
||||||
|
|
||||||
|
put path, params: params, as: :json
|
||||||
|
|
||||||
|
shared_draft = ticket.reload_shared_draft
|
||||||
|
|
||||||
|
expect(shared_draft.attachments).to be_one
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates draft' do
|
||||||
|
shared_draft = create(:ticket_shared_draft_zoom, ticket: ticket)
|
||||||
|
|
||||||
|
put path, params: params, as: :json
|
||||||
|
|
||||||
|
expect(shared_draft.reload).to have_attributes(ticket_attributes: { attrs: true })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates draft with attachment' do
|
||||||
|
shared_draft = create(:ticket_shared_draft_zoom, ticket: ticket)
|
||||||
|
attach(id: form_id)
|
||||||
|
|
||||||
|
put path, params: params, as: :json
|
||||||
|
|
||||||
|
expect(shared_draft.attachments).to be_one
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates draft to have no attachments' do
|
||||||
|
shared_draft = create(:ticket_shared_draft_zoom, ticket: ticket)
|
||||||
|
attach(id: shared_draft.id, object_name: shared_draft.class.name)
|
||||||
|
attach(id: form_id)
|
||||||
|
|
||||||
|
put path, params: params, as: :json
|
||||||
|
|
||||||
|
expect(shared_draft.attachments).to be_one
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error when user has no permissions', authenticated_as: :other_agent do
|
||||||
|
put path, params: params, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'destroy' do
|
||||||
|
it 'destroys draft' do
|
||||||
|
draft = create(:ticket_shared_draft_zoom, ticket: ticket)
|
||||||
|
|
||||||
|
delete path, as: :json
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftZoom).not_to be_exists(draft.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error if draft does not exist' do
|
||||||
|
delete non_existant_path, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error when user has no permissions', authenticated_as: :other_agent do
|
||||||
|
delete path, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'import_attachments' do
|
||||||
|
let(:import_path) { "#{path}/import_attachments" }
|
||||||
|
let(:form_id) { 456 }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
form_id: form_id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'imports attachments from draft to given form ID' do
|
||||||
|
draft = create(:ticket_shared_draft_zoom, ticket: ticket)
|
||||||
|
attach(id: draft.id, object_name: draft.class.name)
|
||||||
|
|
||||||
|
expect { post import_path, params: params, as: :json }
|
||||||
|
.to change { Store.list(object: 'UploadCache', o_id: form_id).count }
|
||||||
|
.by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns success if draft has no attachments' do
|
||||||
|
create(:ticket_shared_draft_zoom, ticket: ticket)
|
||||||
|
|
||||||
|
post import_path, params: params, as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'clean up' do
|
||||||
|
let(:group) { create(:group, shared_drafts: true) }
|
||||||
|
let(:draft_a) { create(:ticket_shared_draft_zoom, ticket: create(:ticket, group: group)) }
|
||||||
|
let(:draft_b) { create(:ticket_shared_draft_zoom, ticket: create(:ticket, group: group)) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
agent.user_groups.create! group: group, access: :full
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes draft when updating a ticket' do
|
||||||
|
put_ticket_update(draft_a.ticket.id, draft_a.id)
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftZoom).not_to be_exist(draft_a.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'not removes draft when fails updating a ticket' do
|
||||||
|
put_ticket_update(draft_a.ticket.id, draft_a.id, valid: false)
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftZoom).to be_exist(draft_a.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error if draft is not applicable in this context' do
|
||||||
|
put_ticket_update(draft_a.ticket.id, draft_b.id)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps draft if not applicable in this context' do
|
||||||
|
put_ticket_update(draft_a.ticket.id, draft_b.id)
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftZoom).to be_exist(draft_b.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'succeeds even if group is not eligible anymore' do
|
||||||
|
group.update(shared_drafts: false)
|
||||||
|
put_ticket_update(draft_a.ticket.id, 1234)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes given draft even if group is not eligible anymore' do
|
||||||
|
group.update(shared_drafts: false)
|
||||||
|
put_ticket_update(draft_a.ticket.id, draft_a.id)
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftZoom).not_to be_exist(draft_a.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'succeeds when draft does not exist' do
|
||||||
|
put_ticket_update(draft_a.ticket.id, 1234)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
def put_ticket_update(ticket_id, shared_draft_id, valid: true)
|
||||||
|
params = {
|
||||||
|
article: {
|
||||||
|
body: valid ? 'some body' : nil,
|
||||||
|
shared_draft_id: shared_draft_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
put "/api/v1/tickets/#{ticket_id}", params: params, as: :json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attach(id:, object_name: 'UploadCache')
|
||||||
|
Store.add(
|
||||||
|
object: object_name,
|
||||||
|
o_id: id,
|
||||||
|
data: File.binread(Rails.root.join('test/data/image/1x1.png')),
|
||||||
|
filename: '1x1.png',
|
||||||
|
preferences: {},
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -53,3 +53,19 @@ end
|
||||||
Capybara.add_selector(:task_with) do
|
Capybara.add_selector(:task_with) do
|
||||||
css { |task_key| ".tasks .task[data-key='#{task_key}']" }
|
css { |task_key| ".tasks .task[data-key='#{task_key}']" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Capybara.add_selector(:draft_sidebar_button) do
|
||||||
|
css { '.tabsSidebar-tab[data-tab=shared_draft]' }
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara.add_selector(:draft_sidebar) do
|
||||||
|
css { '.shared-drafts-manage' }
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara.add_selector(:draft_share_button) do
|
||||||
|
css { '.attributeBar-draft' }
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara.add_selector(:draft_save_button) do
|
||||||
|
css { '.js-dropdownActionSaveDraft' }
|
||||||
|
end
|
||||||
|
|
239
spec/system/ticket/shared_draft_start_spec.rb
Normal file
239
spec/system/ticket/shared_draft_start_spec.rb
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Ticket Shared Draft Start', type: :system, authenticated_as: :authenticate do
|
||||||
|
let(:group) { create(:group, shared_drafts: group_shared_drafts) }
|
||||||
|
let(:group_access) { :full }
|
||||||
|
let(:group_shared_drafts) { true }
|
||||||
|
let(:draft) { create(:ticket_shared_draft_start, group: group, content: draft_content) }
|
||||||
|
let(:draft_body) { 'draft body' }
|
||||||
|
let(:draft_options) { { priority_id: '3' } }
|
||||||
|
|
||||||
|
let(:draft_content) do
|
||||||
|
{
|
||||||
|
body: draft_body
|
||||||
|
}.merge draft_options
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:user) do
|
||||||
|
user = create(:agent)
|
||||||
|
user.user_groups.create! group: group, access: group_access
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate
|
||||||
|
draft
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
click '.settings.add'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'sidebar' do
|
||||||
|
context 'given multiple groups' do
|
||||||
|
let(:another_group) { create(:group, shared_drafts: false) }
|
||||||
|
|
||||||
|
def authenticate
|
||||||
|
user.user_groups.create! group: another_group, access: :full
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'not visible without group selected' do
|
||||||
|
expect(page).to have_no_selector :draft_sidebar_button
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'not visible when group with disabled draft selected' do
|
||||||
|
within(:active_content) do
|
||||||
|
select another_group.name, from: 'group_id'
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_no_selector :draft_sidebar_button
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'visible when group with active draft selected' do
|
||||||
|
within(:active_content) do
|
||||||
|
select group.name, from: 'group_id'
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_selector :draft_sidebar_button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when single group' do
|
||||||
|
it 'visible' do
|
||||||
|
expect(page).to have_selector :draft_sidebar_button
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when drafts disabled' do
|
||||||
|
let(:group_shared_drafts) { false }
|
||||||
|
|
||||||
|
it 'not visible' do
|
||||||
|
expect(page).to have_no_selector :draft_sidebar_button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'create' do
|
||||||
|
before { click :draft_sidebar_button }
|
||||||
|
|
||||||
|
it 'prevents a draft creation without name' do
|
||||||
|
within :draft_sidebar do
|
||||||
|
expect { click '.js-create' }
|
||||||
|
.to change { has_css? '.has-error', wait: false }
|
||||||
|
.to true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'create a draft with name' do
|
||||||
|
within :draft_sidebar do
|
||||||
|
find('.js-name').fill_in with: 'Draft Name'
|
||||||
|
|
||||||
|
expect { click '.js-create' }
|
||||||
|
.to change { Ticket::SharedDraftStart.count }
|
||||||
|
.by 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'update' do
|
||||||
|
before do
|
||||||
|
attach(id: draft.id, object_name: draft.class.name)
|
||||||
|
click :draft_sidebar_button
|
||||||
|
|
||||||
|
within :draft_sidebar do
|
||||||
|
click '.label-subtle'
|
||||||
|
end
|
||||||
|
|
||||||
|
in_modal do
|
||||||
|
click '.js-submit'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes content' do
|
||||||
|
within :active_content do
|
||||||
|
find(:richtext).send_keys('add update')
|
||||||
|
click '.js-update'
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(draft.reload.content['body']).to match %r{add update}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes name' do
|
||||||
|
within :active_content do
|
||||||
|
find('.js-name').fill_in with: 'new name'
|
||||||
|
click '.js-update'
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(draft.reload.name).to eq 'new name'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'requires name' do
|
||||||
|
within :draft_sidebar do
|
||||||
|
find('.js-name').fill_in with: ''
|
||||||
|
|
||||||
|
expect { click '.js-update' }
|
||||||
|
.to change { has_css? '.has-error', wait: false }
|
||||||
|
.to true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'saves as copy' do
|
||||||
|
within :draft_sidebar do
|
||||||
|
expect { click '.js-create' }
|
||||||
|
.to change { Ticket::SharedDraftStart.count }
|
||||||
|
.by 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'delete' do
|
||||||
|
it 'works' do
|
||||||
|
click :draft_sidebar_button
|
||||||
|
|
||||||
|
within :draft_sidebar do
|
||||||
|
click '.label-subtle'
|
||||||
|
end
|
||||||
|
|
||||||
|
in_modal do
|
||||||
|
click '.js-delete'
|
||||||
|
end
|
||||||
|
|
||||||
|
click_on 'Yes'
|
||||||
|
|
||||||
|
expect(Ticket::SharedDraftStart).not_to be_exist(draft.id)
|
||||||
|
|
||||||
|
within :draft_sidebar do
|
||||||
|
expect(page).to have_no_text(draft.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'preview' do
|
||||||
|
before do
|
||||||
|
click :draft_sidebar_button
|
||||||
|
|
||||||
|
within :draft_sidebar do
|
||||||
|
click '.label-subtle'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows body' do
|
||||||
|
in_modal disappears: false do
|
||||||
|
expect(page).to have_text(draft_body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows author' do
|
||||||
|
in_modal disappears: false do
|
||||||
|
expect(page).to have_text(User.find(draft.created_by_id).fullname)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'apply' do
|
||||||
|
before do
|
||||||
|
attach(id: draft.id, object_name: draft.class.name)
|
||||||
|
click :draft_sidebar_button
|
||||||
|
|
||||||
|
within :draft_sidebar do
|
||||||
|
click '.label-subtle'
|
||||||
|
end
|
||||||
|
|
||||||
|
in_modal do
|
||||||
|
click '.js-submit'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'applies body' do
|
||||||
|
within :active_content do
|
||||||
|
expect(page).to have_text draft_body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'applies meta' do
|
||||||
|
within :active_content do
|
||||||
|
expect(find('[name=priority_id]').value).to eq draft_options[:priority_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'applies attachment' do
|
||||||
|
within :active_content do
|
||||||
|
expect(page).to have_text('1x1.png')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attach(id:, object_name: 'UploadCache')
|
||||||
|
Store.add(
|
||||||
|
object: object_name,
|
||||||
|
o_id: id,
|
||||||
|
data: File.binread(Rails.root.join('test/data/image/1x1.png')),
|
||||||
|
filename: '1x1.png',
|
||||||
|
preferences: {},
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
240
spec/system/ticket/shared_draft_zoom_spec.rb
Normal file
240
spec/system/ticket/shared_draft_zoom_spec.rb
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Ticket Shared Draft Zoom', type: :system, authenticated_as: :authenticate do
|
||||||
|
let(:group) { create(:group, shared_drafts: group_shared_drafts) }
|
||||||
|
let(:group_access) { :full }
|
||||||
|
let(:group_shared_drafts) { true }
|
||||||
|
let(:ticket) { create(:ticket, group: group) }
|
||||||
|
let(:ticket_with_draft) { create(:ticket, group: group) }
|
||||||
|
let(:draft_body) { 'draft here' }
|
||||||
|
|
||||||
|
let(:draft) do
|
||||||
|
create(:ticket_shared_draft_zoom,
|
||||||
|
ticket: ticket_with_draft,
|
||||||
|
new_article: { body: draft_body, type: 'note', internal: true },
|
||||||
|
ticket_attributes: { priority_id: '3' })
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:user) do
|
||||||
|
user = create(:agent)
|
||||||
|
user.user_groups.create! group: group, access: group_access
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate
|
||||||
|
draft
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
visit "ticket/zoom/#{ticket.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'buttons' do
|
||||||
|
context 'when drafts disabled for the group' do
|
||||||
|
let(:group_shared_drafts) { false }
|
||||||
|
|
||||||
|
it 'share button not visible' do
|
||||||
|
expect(page).to have_no_selector :draft_share_button
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'save button not visible' do
|
||||||
|
click '.js-openDropdownMacro'
|
||||||
|
|
||||||
|
expect(page).to have_no_selector :draft_save_button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when drafts enabled for the group' do
|
||||||
|
it 'share button not visible initially' do
|
||||||
|
expect(page).to have_no_selector :draft_share_button
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'save button visible' do
|
||||||
|
expect(page).to have_selector(:draft_save_button, visible: :all)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'share button visible when draft exists' do
|
||||||
|
visit "ticket/zoom/#{ticket_with_draft.id}"
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
expect(page).to have_selector :draft_share_button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'share button appears when other user creates draft' do
|
||||||
|
create(:ticket_shared_draft_zoom, ticket: ticket)
|
||||||
|
|
||||||
|
expect(page).to have_selector :draft_share_button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when insufficient permissions' do
|
||||||
|
let(:group_access) { :read }
|
||||||
|
|
||||||
|
it 'share button not visible when draft exists' do
|
||||||
|
visit "ticket/zoom/#{ticket_with_draft.id}"
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
expect(page).to have_no_selector :draft_share_button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'save button not visible' do
|
||||||
|
click '.js-openDropdownMacro'
|
||||||
|
|
||||||
|
expect(page).to have_no_selector :draft_save_button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'preview' do
|
||||||
|
before do
|
||||||
|
visit "ticket/zoom/#{ticket_with_draft.id}"
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
click :draft_share_button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows content' do
|
||||||
|
in_modal disappears: false do
|
||||||
|
expect(page).to have_text draft_body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows author' do
|
||||||
|
in_modal disappears: false do
|
||||||
|
expect(page).to have_text(User.find(draft.created_by_id).fullname)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'delete' do
|
||||||
|
it 'works' do
|
||||||
|
visit "ticket/zoom/#{ticket_with_draft.id}"
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
click :draft_share_button
|
||||||
|
end
|
||||||
|
|
||||||
|
in_modal do
|
||||||
|
click '.js-delete'
|
||||||
|
end
|
||||||
|
|
||||||
|
click_on 'Yes'
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
expect(page).to have_no_selector :draft_share_button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'hides button when another user deletes' do
|
||||||
|
visit "ticket/zoom/#{ticket_with_draft.id}"
|
||||||
|
|
||||||
|
draft.destroy
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
expect(page).to have_no_selector :draft_share_button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'save' do
|
||||||
|
it 'creates new draft' do
|
||||||
|
find('.articleNewEdit-body').send_keys('Some reply')
|
||||||
|
|
||||||
|
click '.js-openDropdownMacro'
|
||||||
|
|
||||||
|
expect { click :draft_save_button }
|
||||||
|
.to change { ticket.reload.shared_draft.present? }
|
||||||
|
.to true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows overwrite warning when draft exists' do
|
||||||
|
visit "ticket/zoom/#{ticket_with_draft.id}"
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
find('.articleNewEdit-body').send_keys('another reply')
|
||||||
|
click '.js-openDropdownMacro'
|
||||||
|
click :draft_save_button
|
||||||
|
end
|
||||||
|
|
||||||
|
in_modal do
|
||||||
|
click '.js-submit'
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(draft.reload.new_article[:body]).to match %r{another reply}
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'draft loaded' do
|
||||||
|
before do
|
||||||
|
visit "ticket/zoom/#{ticket_with_draft.id}"
|
||||||
|
|
||||||
|
click :draft_share_button
|
||||||
|
|
||||||
|
in_modal do
|
||||||
|
click '.js-submit'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates existing draft' do
|
||||||
|
click '.js-openDropdownMacro'
|
||||||
|
click :draft_save_button
|
||||||
|
|
||||||
|
expect(draft.reload.new_article[:body]).to match %r{draft here}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows overwrite warning when draft edited after loading' do
|
||||||
|
find('.articleNewEdit-body').send_keys('another reply')
|
||||||
|
click '.js-openDropdownMacro'
|
||||||
|
click :draft_save_button
|
||||||
|
|
||||||
|
in_modal do
|
||||||
|
click '.js-submit'
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(draft.reload.new_article[:body]).to match %r{another reply}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'apply' do
|
||||||
|
before do
|
||||||
|
attach(id: draft.id, object_name: draft.class.name)
|
||||||
|
|
||||||
|
visit "ticket/zoom/#{ticket_with_draft.id}"
|
||||||
|
|
||||||
|
click :draft_share_button
|
||||||
|
|
||||||
|
in_modal do
|
||||||
|
click '.js-submit'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'applies new article body' do
|
||||||
|
expect(page).to have_text draft_body
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'applies sidebar changes' do
|
||||||
|
expect(find('[name=priority_id]').value).to eq draft.ticket_attributes[:priority_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'applies attachment' do
|
||||||
|
expect(page).to have_text('1x1.png')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attach(id:, object_name: 'UploadCache')
|
||||||
|
Store.add(
|
||||||
|
object: object_name,
|
||||||
|
o_id: id,
|
||||||
|
data: File.binread(Rails.root.join('test/data/image/1x1.png')),
|
||||||
|
filename: '1x1.png',
|
||||||
|
preferences: {},
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue